Compare commits
No commits in common. "master" and "libxkbcommon" have entirely different histories.
master
...
libxkbcomm
8
.gitmodules
vendored
|
|
@ -1,9 +1,3 @@
|
||||||
[submodule "firmware2/libxkbcommon"]
|
[submodule "firmware2/libxkbcommon"]
|
||||||
path = firmware/libxkbcommon
|
path = firmware2/libxkbcommon
|
||||||
url = https://github.com/xkbcommon/libxkbcommon
|
url = https://github.com/xkbcommon/libxkbcommon
|
||||||
[submodule "firmware2/spectre-api-c"]
|
|
||||||
path = firmware/spectre-api-c
|
|
||||||
url = https://github.com/Limeth/spectre-api.git
|
|
||||||
[submodule "firmware/libsodium"]
|
|
||||||
path = firmware/libsodium
|
|
||||||
url = https://github.com/jedisct1/libsodium
|
|
||||||
|
|
|
||||||
16
firmware/.cargo/config.toml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
[target.xtensa-esp32s3-none-elf]
|
||||||
|
runner = "espflash flash --monitor --chip esp32s3"
|
||||||
|
|
||||||
|
[env]
|
||||||
|
ESP_LOG="info"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
rustflags = [
|
||||||
|
"-C", "link-arg=-nostartfiles",
|
||||||
|
"-Z", "stack-protector=all",
|
||||||
|
]
|
||||||
|
|
||||||
|
target = "xtensa-esp32s3-none-elf"
|
||||||
|
|
||||||
|
[unstable]
|
||||||
|
build-std = ["alloc", "core"]
|
||||||
2
firmware/.gitattributes
vendored
|
|
@ -1,2 +0,0 @@
|
||||||
# Convert to LF line endings on checkout.
|
|
||||||
*.sh text eol=lf
|
|
||||||
42
firmware/.github/workflows/rust_ci.yml
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
name: Continuous Integration
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- "**/README.md"
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
rust-checks:
|
||||||
|
name: Rust Checks
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
action:
|
||||||
|
- command: build
|
||||||
|
args: --release
|
||||||
|
- command: fmt
|
||||||
|
args: --all -- --check
|
||||||
|
- command: clippy
|
||||||
|
args: --all-features --workspace -- -D warnings
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: esp-rs/xtensa-toolchain@v1.5
|
||||||
|
with:
|
||||||
|
default: true
|
||||||
|
buildtargets: esp32s3
|
||||||
|
ldproxy: false
|
||||||
|
- name: Enable caching
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
- name: Run command
|
||||||
|
run: cargo ${{ matrix.action.command }} ${{ matrix.action.args }}
|
||||||
21
firmware/.gitignore
vendored
|
|
@ -1,2 +1,19 @@
|
||||||
/.cargo
|
# will have compiled files and executables
|
||||||
!/acid-firmware/partition-table.csv
|
debug/
|
||||||
|
target/
|
||||||
|
.vscode/
|
||||||
|
.zed/
|
||||||
|
.helix/
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
# RustRover
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
|
||||||
25
firmware/.vscode/settings.json
vendored
|
|
@ -1,25 +0,0 @@
|
||||||
{
|
|
||||||
"rust-analyzer.linkedProjects": [
|
|
||||||
"acid-firmware/Cargo.toml"
|
|
||||||
],
|
|
||||||
"rust-analyzer.cargo.noDefaultFeatures": true,
|
|
||||||
"rust-analyzer.cargo.features": ["develop"],
|
|
||||||
"rust-analyzer.cargo.target": "xtensa-esp32s3-none-elf",
|
|
||||||
"rust-analyzer.cargo.targetDir": "target/rust-analyzer",
|
|
||||||
"rust-analyzer.cargo.extraEnv": {
|
|
||||||
"RUSTUP_TOOLCHAIN": "esp"
|
|
||||||
},
|
|
||||||
"rust-analyzer.check.extraArgs": [
|
|
||||||
"-Zbuild-std=core,alloc"
|
|
||||||
],
|
|
||||||
"rust-analyzer.check.extraEnv": {
|
|
||||||
"EXPLICITLY_INCLUDE_DEFAULT_DIRS": "true",
|
|
||||||
"XKBCOMMON_BUILD_DIR": "../libxkbcommon/build-esp32s3",
|
|
||||||
"SPECTRE_API_BUILD_DIR": "../spectre-api-c/build-esp32s3",
|
|
||||||
"SODIUM_INSTALL_DIR": "../libsodium/install",
|
|
||||||
"XKBCOMMON_BUILD_DIR_NAME": "build-esp32s3",
|
|
||||||
"SPECTRE_API_BUILD_DIR_NAME": "build-esp32s3",
|
|
||||||
"SODIUM_INSTALL_DIR_NAME": "install",
|
|
||||||
"SLINT_FONT_SIZES": "8,11,10,12,13,14,15,16,18,20,22,24,32"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
// Project-local debug tasks
|
|
||||||
//
|
|
||||||
// For more documentation on how to configure debug tasks,
|
|
||||||
// see: https://zed.dev/docs/debugger
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"label": "Launch probe-rs debugging",
|
|
||||||
"adapter": "probe-rs",
|
|
||||||
"request": "launch",
|
|
||||||
"cwd": "$ZED_WORKTREE_ROOT",
|
|
||||||
"server": "127.0.0.1:50000 ",
|
|
||||||
"coreConfigs": [],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
{
|
|
||||||
"lsp": {
|
|
||||||
"rust-analyzer": {
|
|
||||||
"initialization_options": {
|
|
||||||
// Which project to enable
|
|
||||||
"linkedProjects": ["acid-firmware/Cargo.toml"],
|
|
||||||
"cargo": {
|
|
||||||
// This must match the target MCU's target
|
|
||||||
"target": "xtensa-esp32s3-none-elf",
|
|
||||||
// Prevents rust-analyzer from blocking cargo
|
|
||||||
"targetDir": "target/rust-analyzer",
|
|
||||||
"extraEnv": {
|
|
||||||
// ESP32-S3 is an Xtensa MCU, we need to work with the esp toolchain
|
|
||||||
"RUSTUP_TOOLCHAIN": "esp",
|
|
||||||
// Rest of the environment variables must be kept in sync with `acid-firmware/.cargo/config.toml`:
|
|
||||||
"EXPLICITLY_INCLUDE_DEFAULT_DIRS": "true",
|
|
||||||
"XKBCOMMON_BUILD_DIR": "../libxkbcommon/build-esp32s3",
|
|
||||||
"SPECTRE_API_BUILD_DIR": "../spectre-api-c/build-esp32s3",
|
|
||||||
"SODIUM_INSTALL_DIR": "../libsodium/install",
|
|
||||||
"XKBCOMMON_BUILD_DIR_NAME": "build-esp32s3",
|
|
||||||
"SPECTRE_API_BUILD_DIR_NAME": "build-esp32s3",
|
|
||||||
"SODIUM_INSTALL_DIR_NAME": "install",
|
|
||||||
"SPECTRE_API_SYS_CC": "xtensa-esp32s3-elf-cc.exe",
|
|
||||||
"ESP_LOG": "warn",
|
|
||||||
"ESP_BACKTRACE_CONFIG_BACKTRACE_FRAMES": "20",
|
|
||||||
"SLINT_FONT_SIZES": "8,11,10,12,13,14,15,16,18,20,22,24,32",
|
|
||||||
"ACID_KEYMAP_PATH": "../keymaps/cz_coder.xkb",
|
|
||||||
"ACID_COMPOSE_PATH": "../compose/cs_CZ Compose.txt",
|
|
||||||
"ACID_COMPOSE_LOCALE": "cs_CZ.UTF-8",
|
|
||||||
},
|
|
||||||
"extraArgs": ["-Zbuild-std=core,alloc"],
|
|
||||||
// Enable device support and a wide set of features on the esp-rtos crate.
|
|
||||||
"noDefaultFeatures": true,
|
|
||||||
"features": ["develop"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
7774
firmware/Cargo.lock
generated
|
|
@ -1,26 +1,56 @@
|
||||||
[workspace]
|
[package]
|
||||||
resolver = "3"
|
edition = "2021"
|
||||||
members = ["acid-firmware", "password-hash"]
|
name = "acid-firmware"
|
||||||
default-members = ["acid-firmware"]
|
rust-version = "1.86"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[[bin]]
|
||||||
spectre-api-sys = { git = "https://github.com/Limeth/spectre-api-sys", rev = "9e844eb056c3dfee8286ac21ec40fa689a8b8aa2" }
|
name = "acid-firmware"
|
||||||
|
path = "./src/bin/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# TODO: Remove the `git = ...` fields, which are here because IntelliSense is broken in 1.0.0-rc.0
|
||||||
|
esp-bootloader-esp-idf = { version = "0.2.0", git = "https://github.com/esp-rs/esp-hal", features = ["esp32s3"] }
|
||||||
|
esp-hal = { version = "=1.0.0-rc.0", git = "https://github.com/esp-rs/esp-hal", features = [
|
||||||
|
"esp32s3",
|
||||||
|
"log-04",
|
||||||
|
"unstable",
|
||||||
|
"psram",
|
||||||
|
] }
|
||||||
|
log = "0.4.27"
|
||||||
|
|
||||||
|
critical-section = "1.2.0"
|
||||||
|
embassy-executor = { version = "0.9.0", features = [
|
||||||
|
"log",
|
||||||
|
# "task-arena-size-20480",
|
||||||
|
] }
|
||||||
|
embassy-time = { version = "0.4.0", features = ["log"] }
|
||||||
|
esp-alloc = { version = "0.8.0", git = "https://github.com/esp-rs/esp-hal" }
|
||||||
|
esp-backtrace = { version = "0.17.0", git = "https://github.com/esp-rs/esp-hal", features = [
|
||||||
|
"esp32s3",
|
||||||
|
# "exception-handler",
|
||||||
|
"panic-handler",
|
||||||
|
"println",
|
||||||
|
] }
|
||||||
|
esp-hal-embassy = { version = "0.9.0", git = "https://github.com/esp-rs/esp-hal", features = ["esp32s3", "log-04"] }
|
||||||
|
esp-println = { version = "0.15.0", git = "https://github.com/esp-rs/esp-hal", features = ["esp32s3", "log-04"] }
|
||||||
|
static_cell = "2.1.1"
|
||||||
|
itertools = { version = "0.14.0", default-features = false }
|
||||||
|
bitflags = "2.9.4"
|
||||||
|
paste = "1.0.15"
|
||||||
|
lazy_static = { version = "1.5.0", features = ["spin_no_std"], default-features = false }
|
||||||
|
|
||||||
[profile.dev.package.esp-storage]
|
|
||||||
opt-level = 3
|
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
# Rust debug is too slow.
|
# Rust debug is too slow.
|
||||||
# For debug builds always builds with some optimization
|
# For debug builds always builds with some optimization
|
||||||
opt-level = "s"
|
opt-level = "s"
|
||||||
lto = 'thin'
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
codegen-units = 1 # LLVM can perform better optimizations using a single thread
|
codegen-units = 1 # LLVM can perform better optimizations using a single thread
|
||||||
debug = 2
|
debug = 2
|
||||||
debug-assertions = false
|
debug-assertions = false
|
||||||
incremental = false
|
incremental = false
|
||||||
lto = 'thin'
|
lto = 'fat'
|
||||||
opt-level = 3
|
opt-level = 's'
|
||||||
overflow-checks = false
|
overflow-checks = false
|
||||||
|
|
|
||||||
20
firmware/README.md
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Building and running
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo build --release && espflash flash --port COM5 .\target\xtensa-esp32s3-none-elf\release\acid-firmware --monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
A different port may need to be chosen.
|
||||||
|
|
||||||
|
# Monitoring
|
||||||
|
|
||||||
|
```
|
||||||
|
espflash monitor -p COM5
|
||||||
|
```
|
||||||
|
|
||||||
|
A different port may need to be chosen.
|
||||||
|
|
||||||
|
# Debugging
|
||||||
|
|
||||||
|
Sometimes the firmware keeps crashing.
|
||||||
|
Pulling GPIO0 high during reset seems to fix this?
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
[target.'cfg(all(any(target_arch = "riscv32", target_arch = "xtensa"), target_os = "none"))']
|
|
||||||
runner = "espflash flash --partition-table partition-table.csv --monitor"
|
|
||||||
# runner = "probe-rs run --chip esp32s3 --preverify"
|
|
||||||
|
|
||||||
[build]
|
|
||||||
target = "xtensa-esp32s3-none-elf"
|
|
||||||
rustflags = [
|
|
||||||
# Exploit mitigations.
|
|
||||||
"-Zstack-protector=all",
|
|
||||||
|
|
||||||
# Other unsupported exploit mitigations from:
|
|
||||||
# https://doc.rust-lang.org/beta/rustc/exploit-mitigations.html#exploit-mitigations-1
|
|
||||||
# "-Zsanitizer=cfi,shadow-call-stack,safestack"
|
|
||||||
|
|
||||||
# Required to obtain backtraces on riscv (e.g. when using the "esp-backtrace" crate.)
|
|
||||||
# "-C", "force-frame-pointers",
|
|
||||||
]
|
|
||||||
|
|
||||||
[env] # These must be kept in sync with /.zed/settings.json
|
|
||||||
EXPLICITLY_INCLUDE_DEFAULT_DIRS = "true"
|
|
||||||
XKBCOMMON_BUILD_DIR = "../libxkbcommon/build-esp32s3"
|
|
||||||
SPECTRE_API_BUILD_DIR = "../spectre-api-c/build-esp32s3"
|
|
||||||
SODIUM_INSTALL_DIR = "../libsodium/install"
|
|
||||||
XKBCOMMON_BUILD_DIR_NAME = "build-esp32s3"
|
|
||||||
SPECTRE_API_BUILD_DIR_NAME = "build-esp32s3"
|
|
||||||
SODIUM_INSTALL_DIR_NAME = "install"
|
|
||||||
SPECTRE_API_SYS_CC = "xtensa-esp32s3-elf-cc.exe"
|
|
||||||
ESP_LOG = "warn"
|
|
||||||
ESP_BACKTRACE_CONFIG_BACKTRACE_FRAMES = "20"
|
|
||||||
# This is overkill, but we can afford it.
|
|
||||||
SLINT_FONT_SIZES = "8,11,10,12,13,14,15,16,18,20,22,24,32"
|
|
||||||
ACID_KEYMAP_PATH = "../keymaps/cz_coder.xkb"
|
|
||||||
ACID_COMPOSE_PATH = "../compose/cs_CZ Compose.txt"
|
|
||||||
ACID_COMPOSE_LOCALE = "cs_CZ.UTF-8"
|
|
||||||
|
|
||||||
# Xtensa only:
|
|
||||||
# Needed for nightly, until llvm upstream has support for Rust Xtensa.
|
|
||||||
# This can be substituted with a `-Zbuild-std="core,alloc"` cargo flag.
|
|
||||||
[unstable]
|
|
||||||
build-std = ["alloc", "core"]
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "acid-firmware"
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = ['Jakub "Limeth" Hlusička']
|
|
||||||
description = "Firmware for the ACID keyboard"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = ["usb-log", "limit-fps"]
|
|
||||||
# Make RMK not to use USB
|
|
||||||
no-usb = ["rmk/_no_usb"]
|
|
||||||
# Let RMK use BLE
|
|
||||||
ble = ["rmk/esp32s3_ble", "dep:esp-radio", "dep:bt-hci"]
|
|
||||||
# Use alternative logging via GPIO5 as RX and GPIO12 as TX.
|
|
||||||
# Disables default logging via USB.
|
|
||||||
# Does not support esp-println's `println!`.
|
|
||||||
alt-log = []
|
|
||||||
# Standard logging implementation over USB.
|
|
||||||
usb-log = ["esp-backtrace/panic-handler"]
|
|
||||||
# RTT (+ logging) for probe-rs
|
|
||||||
rtt-log = ["dep:rtt-target", "dep:panic-rtt-target"]
|
|
||||||
# Block the main core while it is driving the LCD.
|
|
||||||
# This prevents the main core from accessing PSRAM while the LCD is being driven,
|
|
||||||
# which causes the LCD to glitch. To prevent the main core from spending all its
|
|
||||||
# execution time on just driving the LCD, it will be limited.
|
|
||||||
limit-fps = []
|
|
||||||
# Development profiles
|
|
||||||
develop = ["limit-fps", "alt-log"]
|
|
||||||
develop-usb = ["limit-fps", "usb-log", "no-usb", "ble"]
|
|
||||||
probe = ["limit-fps", "rtt-log", "no-usb", "ble"]
|
|
||||||
# Formats the EKV database on boot.
|
|
||||||
format-db = []
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
rmk = { version = "0.8.2", git = "https://github.com/Limeth/rmk", rev = "1661c55f5c21e7d80ea3f93255df483302c74b84", default-features = false, features = [
|
|
||||||
"log",
|
|
||||||
"storage",
|
|
||||||
"vial",
|
|
||||||
"controller",
|
|
||||||
] }
|
|
||||||
embassy-executor = { version = "0.9", features = ["log"] }
|
|
||||||
embassy-time = { version = "0.5.0", features = ["log"] }
|
|
||||||
embassy-embedded-hal = "0.5.0"
|
|
||||||
embassy-sync = { version = "0.7.2", features = ["log"] }
|
|
||||||
embassy-sync-old = { package = "embassy-sync", version = "0.6.2", features = ["log"] }
|
|
||||||
esp-backtrace = { version = "0.18", default-features = false, features = [
|
|
||||||
"esp32s3",
|
|
||||||
"println",
|
|
||||||
] }
|
|
||||||
esp-hal = { version = "1.0", features = ["esp32s3", "unstable", "psram", "log-04"] }
|
|
||||||
esp-storage = { version = "0.8", features = ["esp32s3"] }
|
|
||||||
esp-alloc = { version = "0.9", features = ["nightly"] }
|
|
||||||
esp-println = { version = "0.16", features = ["esp32s3", "log-04"] }
|
|
||||||
esp-radio = { version = "0.17", features = ["esp32s3", "unstable", "ble"], optional = true }
|
|
||||||
esp-rtos = { version = "0.2", features = ["esp32s3", "esp-radio", "embassy"] }
|
|
||||||
esp-bootloader-esp-idf = { version = "0.4", features = ["esp32s3", "log-04"] }
|
|
||||||
esp-sync = { version = "0.1.1", features = ["esp32s3", "log-04"] }
|
|
||||||
bt-hci = { version = "0.6", optional = true } # Must be updated with esp-radio and rmk
|
|
||||||
rand_core = { version = "0.9", default-features = false }
|
|
||||||
static_cell = "2"
|
|
||||||
lazy_static = { version = "1.5.0", features = ["spin_no_std"], default-features = false }
|
|
||||||
log = "0.4.29"
|
|
||||||
bitflags = "2.10.0"
|
|
||||||
paste = { package = "pastey", version = "0.2.1" }
|
|
||||||
itertools = { version = "0.14.0", default-features = false }
|
|
||||||
bytemuck = "1.24.0"
|
|
||||||
critical-section = "1.2.0"
|
|
||||||
cfg-if = "1.0.4"
|
|
||||||
xkbcommon = { git = "https://github.com/Limeth/xkbcommon-rs", rev = "d91705a7211e294c09abae5e3e64f1df158bc2c5", default-features = false, features = ["c-lib-wrap"] }
|
|
||||||
rtt-target = { version = "0.6.2", features = ["log"], optional = true }
|
|
||||||
panic-rtt-target = { version = "0.2.0", optional = true }
|
|
||||||
enumset = "1.1.10"
|
|
||||||
printf-compat = { version = "0.2.1", default-features = false } # Kept older because of the outdated esp toolchain's VaList
|
|
||||||
spectre-api-sys = { workspace = true }
|
|
||||||
sha2 = { version = "0.10.9", default-features = false }
|
|
||||||
password-hash = { path = "../password-hash", default-features = false }
|
|
||||||
hmac = "0.12.1"
|
|
||||||
data-encoding-macro = "0.1.19"
|
|
||||||
embedded-storage-async = "0.4.1"
|
|
||||||
postcard = { version = "1.1", default-features = false, features = ["alloc", "postcard-derive"] } # TODO: defmt
|
|
||||||
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
|
||||||
# serde_with = { version = "3.16", default-features = false, features = ["alloc", "macros"] }
|
|
||||||
serde_bytes = { version = "0.11.19", default-features = false, features = ["alloc"] }
|
|
||||||
chrono = { version = "0.4.43", default-features = false, features = ["alloc", "serde"] } # TODO: defmt
|
|
||||||
tinyvec = { version = "1.10.0", default-features = false, features = ["alloc"] }
|
|
||||||
esp-metadata-generated = { version = "0.3.0", features = ["esp32s3"] }
|
|
||||||
hex = { version = "0.4.3", default-features = false, features = ["alloc"] }
|
|
||||||
indoc = "2.0.7"
|
|
||||||
|
|
||||||
# A fork of slint with patches for `allocator_api` support.
|
|
||||||
# Don't forget to change `slint-build` in build dependencies, if this is changed.
|
|
||||||
slint = { version = "1.14.1", git = "https://github.com/Limeth/slint", rev = "c2e5d05df2476557a299a78664e148d2fe62427d", default-features = false, features = ["compat-1-2", "libm", "log", "unsafe-single-threaded", "renderer-software", "serde"] }
|
|
||||||
i-slint-common = { version = "1.14.1", git = "https://github.com/Limeth/slint", rev = "c2e5d05df2476557a299a78664e148d2fe62427d" }
|
|
||||||
i-slint-core = { version = "1.14.1", git = "https://github.com/Limeth/slint", rev = "c2e5d05df2476557a299a78664e148d2fe62427d", default-features = false }
|
|
||||||
|
|
||||||
# Crates for serial UART CLI
|
|
||||||
embedded-cli = { version = "0.2.1", default-features = false, features = ["help", "macros"] }
|
|
||||||
embedded-io = "0.6"
|
|
||||||
mutually_exclusive_features = "0.1.0"
|
|
||||||
|
|
||||||
[dependencies.ekv]
|
|
||||||
version = "1.0.0"
|
|
||||||
features = [
|
|
||||||
# TODO: "defmt",
|
|
||||||
"crc",
|
|
||||||
"max-page-count-2048",
|
|
||||||
"max-key-size-256",
|
|
||||||
# "max-value-size-65536",
|
|
||||||
"max-value-size-1024",
|
|
||||||
# These must adhere to `FlashStorage`'s parameters.
|
|
||||||
"align-4",
|
|
||||||
"page-size-4096",
|
|
||||||
]
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
xz2 = "0.1.7"
|
|
||||||
json = "0.12"
|
|
||||||
const-gen = "1.6"
|
|
||||||
embuild = "0.33"
|
|
||||||
cc = "1.2.9"
|
|
||||||
slint-build = { version = "1.14.1", git = "https://github.com/Limeth/slint", rev = "c2e5d05df2476557a299a78664e148d2fe62427d" }
|
|
||||||
gix = { version = "0.78", default-features = false, features = ["max-performance", "status"] }
|
|
||||||
indoc = "2.0.7"
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "acid-firmware"
|
|
||||||
test = false
|
|
||||||
bench = false
|
|
||||||
|
|
@ -1,246 +0,0 @@
|
||||||
use std::env;
|
|
||||||
use std::fs::{File, OpenOptions};
|
|
||||||
use std::io::{Read, Write};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use const_gen::*;
|
|
||||||
use embuild::cmd;
|
|
||||||
use indoc::writedoc;
|
|
||||||
use json::JsonValue;
|
|
||||||
use slint_build::{CompilerConfiguration, EmbedResourcesKind};
|
|
||||||
use xz2::read::XzEncoder;
|
|
||||||
|
|
||||||
fn build_xkbcommon() {
|
|
||||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
|
||||||
let build_script = manifest_dir.join("../libxkbcommon-compile.sh");
|
|
||||||
let build_dir_name = env::var("XKBCOMMON_BUILD_DIR_NAME").unwrap();
|
|
||||||
cmd!(build_script, build_dir_name)
|
|
||||||
.run()
|
|
||||||
.expect("Failed to compile xkbcommon.");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_sodium() {
|
|
||||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
|
||||||
let build_script = manifest_dir.join("../libsodium-compile.sh");
|
|
||||||
let install_dir_name = env::var("SODIUM_INSTALL_DIR_NAME").unwrap();
|
|
||||||
cmd!(build_script, install_dir_name)
|
|
||||||
.run()
|
|
||||||
.expect("Failed to compile sodium.");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_spectre() {
|
|
||||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
|
||||||
let build_script = manifest_dir.join("../spectre-api-compile.sh");
|
|
||||||
let build_dir_name = env::var("SPECTRE_API_BUILD_DIR_NAME").unwrap();
|
|
||||||
let sodium_install_dir = env::var("SODIUM_INSTALL_DIR").unwrap();
|
|
||||||
cmd!(build_script, build_dir_name, sodium_install_dir)
|
|
||||||
.run()
|
|
||||||
.expect("Failed to compile the Spectre API.");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
|
||||||
if let Ok(repo) = gix::discover(&manifest_dir) {
|
|
||||||
let commit_hash = repo.head_commit().unwrap().short_id().unwrap();
|
|
||||||
println!("cargo:rustc-env=GIT_COMMIT_HASH={}", commit_hash);
|
|
||||||
println!(
|
|
||||||
"cargo:rustc-env=GIT_COMMIT={}",
|
|
||||||
repo.find_tag(repo.head_id().unwrap())
|
|
||||||
.ok()
|
|
||||||
.map(|tag| format!("{} ({})", tag.decode().unwrap().name, commit_hash))
|
|
||||||
.unwrap_or_else(|| commit_hash.to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate vial config at the root of project
|
|
||||||
println!("cargo:rerun-if-changed=vial.json");
|
|
||||||
generate_vial_config();
|
|
||||||
|
|
||||||
println!("cargo:rustc-link-arg-bins=-Tlinkall.x");
|
|
||||||
|
|
||||||
// Set the extra linker script from defmt
|
|
||||||
// println!("cargo:rustc-link-arg=-Tdefmt.x");
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct NotBuilt {
|
|
||||||
#[allow(unused)]
|
|
||||||
lib_build_dir: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn link_static_lib(env_var: &str, library: &str) -> Result<(), NotBuilt> {
|
|
||||||
let lib_build_dir_str = env::var(env_var)
|
|
||||||
.unwrap_or_else(|error| panic!("The build directory of lib{library} must be specified using the `{env_var}` environment variable: {error}"));
|
|
||||||
let lib_build_dir = PathBuf::from(&lib_build_dir_str)
|
|
||||||
.canonicalize()
|
|
||||||
.map_err(|_| NotBuilt {
|
|
||||||
lib_build_dir: lib_build_dir_str.clone(),
|
|
||||||
})?;
|
|
||||||
let lib_library_path = lib_build_dir
|
|
||||||
.join(format!("lib{library}.a"))
|
|
||||||
.canonicalize()
|
|
||||||
.map_err(|_| NotBuilt {
|
|
||||||
lib_build_dir: lib_build_dir_str.clone(),
|
|
||||||
})?;
|
|
||||||
let lib_build_dir = lib_build_dir.display();
|
|
||||||
|
|
||||||
if !lib_library_path.is_file() {
|
|
||||||
return Err(NotBuilt {
|
|
||||||
lib_build_dir: lib_build_dir_str,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("cargo:rustc-link-search=native={lib_build_dir}");
|
|
||||||
println!("cargo:rustc-link-lib=static={library}");
|
|
||||||
println!("cargo:rerun-if-changed={lib_build_dir}/lib{library}.a");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn link_static_lib_or_build(env_var: &str, library: &str, build: impl FnOnce()) {
|
|
||||||
if link_static_lib(env_var, library).is_err() {
|
|
||||||
(build)();
|
|
||||||
link_static_lib(env_var, library)
|
|
||||||
.unwrap_or_else(|err| panic!("Failed to link library after building it: {err:?}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
link_static_lib_or_build("XKBCOMMON_BUILD_DIR", "xkbcommon", || {
|
|
||||||
build_xkbcommon();
|
|
||||||
});
|
|
||||||
|
|
||||||
link_static_lib_or_build("SPECTRE_API_BUILD_DIR", "spectre", || {
|
|
||||||
build_sodium();
|
|
||||||
build_spectre();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Slint config and compilation
|
|
||||||
{
|
|
||||||
// Don't think this does anything:
|
|
||||||
// println!("cargo:rerun-if-env-changed=SLINT_FONT_SIZES");
|
|
||||||
|
|
||||||
let slint_config = CompilerConfiguration::new()
|
|
||||||
// .with_scale_factor(2.0)
|
|
||||||
.with_style("cosmic-dark".to_string())
|
|
||||||
.embed_resources(EmbedResourcesKind::EmbedForSoftwareRenderer);
|
|
||||||
slint_build::compile_with_config("ui/main.slint", slint_config)
|
|
||||||
.expect("Slint build failed");
|
|
||||||
slint_build::print_rustc_flags().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_vial_config() {
|
|
||||||
// Generated vial config file
|
|
||||||
let path = Path::new(&env::var_os("OUT_DIR").unwrap()).join("config_generated.rs");
|
|
||||||
let mut out_file = OpenOptions::new()
|
|
||||||
.create(true)
|
|
||||||
.write(true)
|
|
||||||
.truncate(true)
|
|
||||||
.open(path)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let p = Path::new("vial.json");
|
|
||||||
let mut content = String::new();
|
|
||||||
match File::open(p) {
|
|
||||||
Ok(mut file) => {
|
|
||||||
file.read_to_string(&mut content)
|
|
||||||
.expect("Cannot read vial.json");
|
|
||||||
}
|
|
||||||
Err(e) => println!("Cannot find vial.json {p:?}: {e}"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let vial_cfg = json::parse(&content).unwrap();
|
|
||||||
|
|
||||||
let keyboard_def_compressed: Vec<u8> = {
|
|
||||||
let vial_cfg_string = json::stringify(vial_cfg.clone());
|
|
||||||
let mut keyboard_def_compressed = Vec::new();
|
|
||||||
XzEncoder::new(vial_cfg_string.as_bytes(), 6)
|
|
||||||
.read_to_end(&mut keyboard_def_compressed)
|
|
||||||
.unwrap();
|
|
||||||
keyboard_def_compressed
|
|
||||||
};
|
|
||||||
// This is a firmware-unique randomly generated ID.
|
|
||||||
// If you fork this repo to make your own firmware, you should change this.
|
|
||||||
let keyboard_id: Vec<u8> = vec![0x9a, 0x8a, 0x08, 0xae, 0x87, 0xcd, 0xc7, 0xb9];
|
|
||||||
let keyboard_name: &str = {
|
|
||||||
let JsonValue::Object(vial_cfg) = &vial_cfg else {
|
|
||||||
panic!("The root element in `vial.json` is not an object.");
|
|
||||||
};
|
|
||||||
vial_cfg
|
|
||||||
.get("name")
|
|
||||||
.expect("No `name` in `vial.json`.")
|
|
||||||
.as_str()
|
|
||||||
.expect("`name` in `vial.json` is not a string.")
|
|
||||||
};
|
|
||||||
let vendor_id: u16 = {
|
|
||||||
let JsonValue::Object(vial_cfg) = &vial_cfg else {
|
|
||||||
panic!("The root element in `vial.json` is not an object.");
|
|
||||||
};
|
|
||||||
let vendor_id_string = vial_cfg
|
|
||||||
.get("vendorId")
|
|
||||||
.expect("No `vendorId` in `vial.json`.")
|
|
||||||
.as_str()
|
|
||||||
.expect("`vendorId` in `vial.json` is not a string.");
|
|
||||||
assert!(vendor_id_string.starts_with("0x"));
|
|
||||||
u16::from_str_radix(&vendor_id_string[2..], 16).expect("Invalid vendor ID.")
|
|
||||||
};
|
|
||||||
let product_id: u16 = {
|
|
||||||
let JsonValue::Object(vial_cfg) = &vial_cfg else {
|
|
||||||
panic!("The root element in `vial.json` is not an object.");
|
|
||||||
};
|
|
||||||
let product_id_string = vial_cfg
|
|
||||||
.get("productId")
|
|
||||||
.expect("No `productId` in `vial.json`.")
|
|
||||||
.as_str()
|
|
||||||
.expect("`productId` in `vial.json` is not a string.");
|
|
||||||
assert!(product_id_string.starts_with("0x"));
|
|
||||||
u16::from_str_radix(&product_id_string[2..], 16).expect("Invalid product ID.")
|
|
||||||
};
|
|
||||||
let const_declarations = [
|
|
||||||
const_declaration!(pub VIAL_KEYBOARD_DEF = keyboard_def_compressed),
|
|
||||||
const_declaration!(pub VIAL_KEYBOARD_ID = keyboard_id),
|
|
||||||
const_declaration!(pub VIAL_KEYBOARD_NAME = keyboard_name),
|
|
||||||
const_declaration!(pub VIAL_VENDOR_ID = vendor_id),
|
|
||||||
const_declaration!(pub VIAL_PRODUCT_ID = product_id),
|
|
||||||
]
|
|
||||||
.map(|s| "#[allow(clippy::redundant_static_lifetimes)]\n".to_owned() + s.as_str())
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
writeln!(out_file, "{}", const_declarations).unwrap();
|
|
||||||
|
|
||||||
writedoc!(
|
|
||||||
out_file,
|
|
||||||
"
|
|
||||||
#[allow(dead_code, non_camel_case_types, clippy::upper_case_acronyms)]
|
|
||||||
#[repr(u8)]
|
|
||||||
pub enum CustomKeycodes {{
|
|
||||||
"
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// const CUSTOM_KEYCODE_FIRST: u16 = 0x840;
|
|
||||||
|
|
||||||
#[allow(clippy::collapsible_if)]
|
|
||||||
if let JsonValue::Object(vial_cfg) = vial_cfg {
|
|
||||||
if let Some(JsonValue::Array(custom_keycodes)) = vial_cfg.get("customKeycodes") {
|
|
||||||
for (index, custom_keycode) in custom_keycodes.iter().enumerate() {
|
|
||||||
if let JsonValue::Object(custom_keycode) = custom_keycode {
|
|
||||||
let name = custom_keycode
|
|
||||||
.get("name")
|
|
||||||
.expect("A custom keycode in vial.json is missing a name.")
|
|
||||||
.as_str()
|
|
||||||
.expect("A custom keycode's name must be a string.");
|
|
||||||
writeln!(
|
|
||||||
out_file,
|
|
||||||
" {} = {},",
|
|
||||||
name,
|
|
||||||
// CUSTOM_KEYCODE_FIRST + index as u16
|
|
||||||
index as u8
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writeln!(out_file, "}}").unwrap();
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
# Name, Type, SubType, Offset, Size, Flags
|
|
||||||
nvs, data, nvs, 0x9000, 0x6000,
|
|
||||||
phy_init, data, phy, 0xf000, 0x1000,
|
|
||||||
factory, app, factory, 0x10000, 4M,
|
|
||||||
rmk, data, undefined, , 64K,
|
|
||||||
acid, data, undefined, , 0x7E0000,
|
|
||||||
|
|
|
@ -1,130 +0,0 @@
|
||||||
|
|
||||||
use rmk::config::{BehaviorConfig, ForksConfig, PositionalConfig};
|
|
||||||
use rmk::fork::{Fork, StateBits};
|
|
||||||
use rmk::types::action::{Action, KeyAction};
|
|
||||||
use rmk::types::modifier::ModifierCombination;
|
|
||||||
use rmk::{a, heapless, k, layer};
|
|
||||||
|
|
||||||
use crate::vial::CustomKeycodes;
|
|
||||||
|
|
||||||
pub const NUM_LAYER: usize = 8;
|
|
||||||
pub const MATRIX_ROWS: usize = 5;
|
|
||||||
pub const MATRIX_COLS: usize = 12;
|
|
||||||
pub const MATRIX_AREA: usize = MATRIX_ROWS * MATRIX_COLS;
|
|
||||||
|
|
||||||
const T: KeyAction = a!(Transparent);
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
pub const fn get_default_keymap() -> [[[KeyAction; MATRIX_COLS]; MATRIX_ROWS]; NUM_LAYER] {
|
|
||||||
[
|
|
||||||
layer!([
|
|
||||||
[k!(Escape), k!(Kc1), k!(Kc2), k!(Kc3), k!(Kc4), k!(Kc5), k!(Kc6), k!(Kc7), k!(Kc8), k!(Kc9), k!(Kc0), k!(Backspace)],
|
|
||||||
[k!(Tab), k!(Q), k!(W), k!(E), k!(R), k!(T), k!(Z), k!(U), k!(I), k!(O), k!(P), k!(Delete)],
|
|
||||||
[k!(LCtrl), k!(A), k!(S), k!(D), k!(F), k!(G), k!(H), k!(J), k!(K), k!(L), k!(Comma), k!(Enter)],
|
|
||||||
[k!(LShift), k!(Y), k!(X), k!(C), k!(V), k!(B), k!(N), k!(M), a!(No), a!(No), k!(Up), KeyAction::Single(Action::User(CustomKeycodes::FOCUS_LCD as u8))],
|
|
||||||
[a!(No), a!(No), k!(LGui), k!(LAlt), KeyAction::Single(Action::TriLayerLower), k!(Space), k!(Space), KeyAction::Single(Action::TriLayerLower), k!(RAlt), k!(Left), k!(Down), k!(Right)]
|
|
||||||
// [a!(No), a!(No), k!(LGui), k!(LAlt), k!(TriLayerLower), k!(Space), k!(Space), k!(TriLayerLower), k!(RAlt), k!(Left), k!(Down), k!(Right)]
|
|
||||||
]),
|
|
||||||
layer!([[T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T]]),
|
|
||||||
layer!([[T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T]]),
|
|
||||||
layer!([[T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T]]),
|
|
||||||
layer!([[T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T]]),
|
|
||||||
layer!([[T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T]]),
|
|
||||||
layer!([[T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T]]),
|
|
||||||
layer!([[T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T]]),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fork_by_shift(
|
|
||||||
trigger: CustomKeycodes,
|
|
||||||
negative_output: KeyAction,
|
|
||||||
positive_output: KeyAction,
|
|
||||||
keep_shift: bool,
|
|
||||||
) -> Fork {
|
|
||||||
Fork::new(
|
|
||||||
KeyAction::Single(Action::User(trigger as u8)),
|
|
||||||
negative_output,
|
|
||||||
positive_output,
|
|
||||||
StateBits::new_from(
|
|
||||||
ModifierCombination::new()
|
|
||||||
.with_left_shift(true)
|
|
||||||
.with_right_shift(true),
|
|
||||||
Default::default(),
|
|
||||||
Default::default(),
|
|
||||||
),
|
|
||||||
StateBits::default(),
|
|
||||||
ModifierCombination::new()
|
|
||||||
.with_left_shift(keep_shift)
|
|
||||||
.with_right_shift(keep_shift),
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_behavior_config() -> BehaviorConfig {
|
|
||||||
BehaviorConfig {
|
|
||||||
fork: ForksConfig {
|
|
||||||
forks: {
|
|
||||||
let mut forks = heapless::Vec::new();
|
|
||||||
forks
|
|
||||||
.push(fork_by_shift(
|
|
||||||
CustomKeycodes::FORK_ACUTE_ABOVERING_CS,
|
|
||||||
rmk::k!(Equal),
|
|
||||||
rmk::k!(Grave), // Shift is kept
|
|
||||||
true,
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
forks
|
|
||||||
.push(fork_by_shift(
|
|
||||||
CustomKeycodes::FORK_CARON_DIAERESIS_CS,
|
|
||||||
rmk::shifted!(Equal),
|
|
||||||
rmk::wm!(Minus, ModifierCombination::new().with_right_alt(true)),
|
|
||||||
false,
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
forks
|
|
||||||
.push(fork_by_shift(
|
|
||||||
CustomKeycodes::FORK_ACUTE_ABOVERING_CSP,
|
|
||||||
rmk::wm!(Equal, ModifierCombination::new().with_right_alt(true)),
|
|
||||||
rmk::wm!(Grave, ModifierCombination::new().with_right_alt(true)), // Shift is kept
|
|
||||||
true,
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
forks
|
|
||||||
.push(fork_by_shift(
|
|
||||||
CustomKeycodes::FORK_CARON_DIAERESIS_CSP,
|
|
||||||
rmk::wm!(
|
|
||||||
Equal,
|
|
||||||
ModifierCombination::new()
|
|
||||||
.with_left_shift(true)
|
|
||||||
.with_right_alt(true)
|
|
||||||
),
|
|
||||||
rmk::wm!(Backslash, ModifierCombination::new().with_right_alt(true)),
|
|
||||||
false,
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
forks
|
|
||||||
.push(fork_by_shift(
|
|
||||||
CustomKeycodes::FORK_9_FORWARDSLASH_EN_CSP,
|
|
||||||
rmk::k!(Kc9),
|
|
||||||
rmk::k!(Slash),
|
|
||||||
false,
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
forks
|
|
||||||
.push(fork_by_shift(
|
|
||||||
CustomKeycodes::FORK_0_BACKWARDSLASH_EN_CSP,
|
|
||||||
rmk::k!(Kc0),
|
|
||||||
rmk::k!(Backslash),
|
|
||||||
false,
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
forks
|
|
||||||
},
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_positional_config() -> PositionalConfig<MATRIX_ROWS, MATRIX_COLS> {
|
|
||||||
PositionalConfig::default()
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
|
|
||||||
|
|
@ -1,319 +0,0 @@
|
||||||
use core::{
|
|
||||||
iter::Chain,
|
|
||||||
ops::{Deref, DerefMut, Range},
|
|
||||||
};
|
|
||||||
|
|
||||||
use alloc::{borrow::Cow, boxed::Box, vec::Vec};
|
|
||||||
use ekv::{
|
|
||||||
Database, ReadTransaction,
|
|
||||||
flash::{Flash, PageID},
|
|
||||||
};
|
|
||||||
use embassy_embedded_hal::{adapter::BlockingAsync, flash::partition::Partition};
|
|
||||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
|
||||||
use embedded_storage_async::nor_flash::{NorFlash, ReadNorFlash};
|
|
||||||
use esp_hal::rng::Trng;
|
|
||||||
use esp_storage::FlashStorage;
|
|
||||||
use log::{debug, info};
|
|
||||||
|
|
||||||
pub type PartitionAcid =
|
|
||||||
Partition<'static, CriticalSectionRawMutex, BlockingAsync<FlashStorage<'static>>>;
|
|
||||||
|
|
||||||
// Workaround for alignment requirements.
|
|
||||||
#[repr(C, align(4))]
|
|
||||||
struct AlignedBuf<const N: usize>(pub [u8; N]);
|
|
||||||
|
|
||||||
pub struct EkvFlash<T> {
|
|
||||||
flash: T,
|
|
||||||
buffer: Box<AlignedBuf<{ ekv::config::PAGE_SIZE }>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> EkvFlash<T> {
|
|
||||||
fn new(flash: T) -> Self {
|
|
||||||
Self {
|
|
||||||
flash,
|
|
||||||
buffer: {
|
|
||||||
// Allocate the buffer directly on the heap.
|
|
||||||
let buffer = Box::new_zeroed();
|
|
||||||
unsafe { buffer.assume_init() }
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Deref for EkvFlash<T> {
|
|
||||||
type Target = T;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.flash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> DerefMut for EkvFlash<T> {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.flash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: NorFlash + ReadNorFlash> ekv::flash::Flash for EkvFlash<T> {
|
|
||||||
type Error = T::Error;
|
|
||||||
|
|
||||||
fn page_count(&self) -> usize {
|
|
||||||
ekv::config::MAX_PAGE_COUNT
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn erase(
|
|
||||||
&mut self,
|
|
||||||
page_id: PageID,
|
|
||||||
) -> Result<(), <EkvFlash<T> as ekv::flash::Flash>::Error> {
|
|
||||||
self.flash
|
|
||||||
.erase(
|
|
||||||
(page_id.index() * ekv::config::PAGE_SIZE) as u32,
|
|
||||||
((page_id.index() + 1) * ekv::config::PAGE_SIZE) as u32,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn read(
|
|
||||||
&mut self,
|
|
||||||
page_id: PageID,
|
|
||||||
offset: usize,
|
|
||||||
data: &mut [u8],
|
|
||||||
) -> Result<(), <EkvFlash<T> as ekv::flash::Flash>::Error> {
|
|
||||||
let address = page_id.index() * ekv::config::PAGE_SIZE + offset;
|
|
||||||
self.flash
|
|
||||||
.read(address as u32, &mut self.buffer.0[..data.len()])
|
|
||||||
.await?;
|
|
||||||
data.copy_from_slice(&self.buffer.0[..data.len()]);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn write(
|
|
||||||
&mut self,
|
|
||||||
page_id: PageID,
|
|
||||||
offset: usize,
|
|
||||||
data: &[u8],
|
|
||||||
) -> Result<(), <EkvFlash<T> as ekv::flash::Flash>::Error> {
|
|
||||||
let address = page_id.index() * ekv::config::PAGE_SIZE + offset;
|
|
||||||
self.buffer.0[..data.len()].copy_from_slice(data);
|
|
||||||
self.flash
|
|
||||||
.write(address as u32, &self.buffer.0[..data.len()])
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AcidDatabase {
|
|
||||||
db: Database<EkvFlash<PartitionAcid>, esp_sync::RawMutex>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for AcidDatabase {
|
|
||||||
type Target = Database<EkvFlash<PartitionAcid>, esp_sync::RawMutex>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.db
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for AcidDatabase {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.db
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AcidDatabase {
|
|
||||||
pub async fn mount(flash: PartitionAcid) -> AcidDatabase {
|
|
||||||
let mut db_config = ekv::Config::default();
|
|
||||||
db_config.random_seed = Trng::try_new()
|
|
||||||
.expect("A `TrngSource` was not initialized before constructing this `Trng`.")
|
|
||||||
.random();
|
|
||||||
let db = Database::<_, esp_sync::RawMutex>::new(EkvFlash::new(flash), db_config);
|
|
||||||
|
|
||||||
#[cfg(feature = "format-db")]
|
|
||||||
{
|
|
||||||
log::warn!("Formatting EKV database...");
|
|
||||||
db.format()
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|error| panic!("Failed to format the EKV database: {error:?}"));
|
|
||||||
log::warn!("EKV database formatted successfully.");
|
|
||||||
}
|
|
||||||
|
|
||||||
match db.mount().await {
|
|
||||||
Ok(()) => info!("EKV database mounted."),
|
|
||||||
Err(error) => panic!("Failed to mount the EKV database: {error:?}"),
|
|
||||||
};
|
|
||||||
|
|
||||||
Self { db }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type DbPathSegment<'a> = Cow<'a, str>;
|
|
||||||
type DbPathBuf<'a> = Vec<DbPathSegment<'a>>;
|
|
||||||
type DbPath<'a> = [DbPathSegment<'a>];
|
|
||||||
|
|
||||||
pub struct DbKey {
|
|
||||||
/// Segments separated by `0x00`, with the whole thing suffixed with `[0x00, 0xFF]`.
|
|
||||||
bytes: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for DbKey {
|
|
||||||
type Target = [u8];
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.bytes[0..(self.bytes.len() - 2)]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DbKey {
|
|
||||||
pub fn from_raw(mut key: Vec<u8>) -> Self {
|
|
||||||
key.extend_from_slice(&[0x00, 0xFF]);
|
|
||||||
Self { bytes: key }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new<'a>(path: impl IntoIterator<Item = DbPathSegment<'a>>) -> Self {
|
|
||||||
// Null bytes are not allowed within path segments, and will cause a panic.
|
|
||||||
// Bytes of `0xFF` cannot appear in valid UTF-8.
|
|
||||||
// The byte vector stored in a `DbKey` is formed by interspersing the segments with `0x00`, and suffixing the key with `[0x00, 0xFF]`.
|
|
||||||
// This lets us represent three significant keys with a single allocation:
|
|
||||||
// * The key for querying the path itself: `bytes[..bytes.len() - 2]`, e.g. `b"first\x00second"`
|
|
||||||
// * The keys for range-querying the path's children:
|
|
||||||
// * Start (inclusive): `bytes[..bytes.len() - 1]`, e.g. `b"first\x00second\x00"`
|
|
||||||
// * End (exclusive): `bytes[..]`, e.g. `b"first\x00second\x00\xFF"`
|
|
||||||
|
|
||||||
let mut bytes = Vec::new();
|
|
||||||
|
|
||||||
for segment in path {
|
|
||||||
assert!(
|
|
||||||
!segment.as_bytes().contains(&0x00),
|
|
||||||
"A path segment must not contain null bytes."
|
|
||||||
);
|
|
||||||
// No need to check for `0xFF` bytes in UTF-8 strings.
|
|
||||||
|
|
||||||
bytes.extend_from_slice(segment.as_bytes());
|
|
||||||
bytes.push(0x00);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert!(!bytes.is_empty(), "An empty path is not a valid path.");
|
|
||||||
bytes.push(0xFF);
|
|
||||||
|
|
||||||
DbKey { bytes }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn range_of_children(&self) -> Range<&[u8]> {
|
|
||||||
(&self.bytes[0..(self.bytes.len() - 1)])..(&self.bytes[..])
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn segments(&self) -> impl Iterator<Item = DbPathSegment<'_>> {
|
|
||||||
struct SegmentIterator<'a> {
|
|
||||||
rest: &'a [u8],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Iterator for SegmentIterator<'a> {
|
|
||||||
type Item = DbPathSegment<'a>;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
if let Some(end_index) = self.rest.iter().position(|byte| *byte == 0) {
|
|
||||||
let segment = &self.rest[..end_index];
|
|
||||||
let segment = str::from_utf8(segment).unwrap();
|
|
||||||
self.rest = &self.rest[end_index + 1..];
|
|
||||||
|
|
||||||
Some(Cow::Borrowed(segment))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SegmentIterator {
|
|
||||||
rest: self.bytes.as_slice(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DbPathSpectreUsers;
|
|
||||||
|
|
||||||
impl IntoIterator for DbPathSpectreUsers {
|
|
||||||
type Item = DbPathSegment<'static>;
|
|
||||||
type IntoIter = core::array::IntoIter<DbPathSegment<'static>, 2>;
|
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
[
|
|
||||||
DbPathSegment::Borrowed("spectre"),
|
|
||||||
DbPathSegment::Borrowed("users"),
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DbPathSpectreUserSites<'a> {
|
|
||||||
pub username: DbPathSegment<'a>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> IntoIterator for DbPathSpectreUserSites<'a> {
|
|
||||||
type Item = DbPathSegment<'a>;
|
|
||||||
type IntoIter = core::array::IntoIter<DbPathSegment<'a>, 4>;
|
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
[
|
|
||||||
DbPathSegment::Borrowed("spectre"),
|
|
||||||
DbPathSegment::Borrowed("user"),
|
|
||||||
self.username,
|
|
||||||
DbPathSegment::Borrowed("site"),
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DbPathSpectreUserSite<'a> {
|
|
||||||
pub user_sites: DbPathSpectreUserSites<'a>,
|
|
||||||
pub site: DbPathSegment<'a>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> IntoIterator for DbPathSpectreUserSite<'a> {
|
|
||||||
type Item = DbPathSegment<'a>;
|
|
||||||
type IntoIter =
|
|
||||||
Chain<core::array::IntoIter<DbPathSegment<'a>, 4>, core::iter::Once<DbPathSegment<'a>>>;
|
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
self.user_sites
|
|
||||||
.into_iter()
|
|
||||||
.chain(core::iter::once(self.site))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ReadTransactionExt<F>
|
|
||||||
where
|
|
||||||
F: Flash,
|
|
||||||
{
|
|
||||||
async fn read_to_vec<'b>(
|
|
||||||
&self,
|
|
||||||
key: &[u8],
|
|
||||||
buffer: &'b mut Vec<u8>,
|
|
||||||
) -> Result<&'b mut [u8], ekv::ReadError<F::Error>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, F, M> ReadTransactionExt<F> for ReadTransaction<'a, F, M>
|
|
||||||
where
|
|
||||||
F: Flash + 'a,
|
|
||||||
M: embassy_sync_old::blocking_mutex::raw::RawMutex + 'a,
|
|
||||||
{
|
|
||||||
async fn read_to_vec<'b>(
|
|
||||||
&self,
|
|
||||||
key: &[u8],
|
|
||||||
buffer: &'b mut Vec<u8>,
|
|
||||||
) -> Result<&'b mut [u8], ekv::ReadError<F::Error>> {
|
|
||||||
if buffer.is_empty() {
|
|
||||||
buffer.resize(1, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match self.read(key, buffer.as_mut_slice()).await {
|
|
||||||
Ok(size) => break Ok(&mut buffer[..size]),
|
|
||||||
Err(ekv::ReadError::BufferTooSmall) => {
|
|
||||||
let new_size = buffer.len() * 2;
|
|
||||||
debug!("Resizing read buffer to {new_size} bytes.");
|
|
||||||
buffer.resize(new_size, 0)
|
|
||||||
}
|
|
||||||
Err(error) => break Err(error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,211 +0,0 @@
|
||||||
use core::{
|
|
||||||
cell::{Cell, RefCell},
|
|
||||||
ffi::{c_char, c_int, c_size_t, c_uchar, c_ulonglong},
|
|
||||||
};
|
|
||||||
|
|
||||||
use critical_section::Mutex;
|
|
||||||
use embassy_sync::blocking_mutex::{self, raw::CriticalSectionRawMutex};
|
|
||||||
use hmac::digest::{FixedOutput, KeyInit, Update};
|
|
||||||
use password_hash::Key;
|
|
||||||
use sha2::{
|
|
||||||
Digest,
|
|
||||||
digest::{consts::U32, generic_array::GenericArray},
|
|
||||||
};
|
|
||||||
use spectre_api_sys::{SpectreKeyPurpose, spectre_purpose_scope};
|
|
||||||
|
|
||||||
use crate::PSRAM_ALLOCATOR;
|
|
||||||
|
|
||||||
// Just a way to mark non-null pointers.
|
|
||||||
// Rust's `NonNull` is for mutable pointers only.
|
|
||||||
pub type NonNullPtr<P> = P;
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
unsafe extern "C" fn __spre_crypto_hash_sha256(
|
|
||||||
data_out: NonNullPtr<*mut c_uchar>,
|
|
||||||
data_in: *const c_uchar,
|
|
||||||
data_in_len: c_ulonglong,
|
|
||||||
) -> c_int {
|
|
||||||
unsafe {
|
|
||||||
let data_out = &mut *(data_out as *mut GenericArray<u8, U32>);
|
|
||||||
let data_in = core::slice::from_raw_parts(data_in, data_in_len as usize);
|
|
||||||
// TODO: Use SHA peripheral for acceleration
|
|
||||||
let mut digest = sha2::Sha256::new();
|
|
||||||
Digest::update(&mut digest, data_in);
|
|
||||||
digest.finalize_into_reset(data_out);
|
|
||||||
}
|
|
||||||
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This is the encrypted user key currently being used in the key derivation function of spectre.
|
|
||||||
/// It decrypts using the user's password into the key that would be derived with the original password hashing function.
|
|
||||||
pub static ACTIVE_ENCRYPTED_USER_KEY: Mutex<Cell<Key>> = Mutex::new(Cell::new([0; _]));
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
#[must_use]
|
|
||||||
unsafe extern "C" fn __spre_crypto_pwhash_scryptsalsa208sha256_ll(
|
|
||||||
password: NonNullPtr<*const u8>,
|
|
||||||
password_len: c_size_t,
|
|
||||||
salt: NonNullPtr<*const u8>,
|
|
||||||
salt_len: c_size_t,
|
|
||||||
n: u64,
|
|
||||||
r: u32,
|
|
||||||
p: u32,
|
|
||||||
output: NonNullPtr<*mut u8>,
|
|
||||||
output_len: c_size_t,
|
|
||||||
) -> c_int {
|
|
||||||
assert_eq!(output_len, 64);
|
|
||||||
|
|
||||||
let encryption_key = unsafe {
|
|
||||||
let password: &[u8] = core::slice::from_raw_parts(password, password_len);
|
|
||||||
let salt: &[u8] = core::slice::from_raw_parts(salt, salt_len);
|
|
||||||
let purpose = spectre_purpose_scope(SpectreKeyPurpose::Authentication);
|
|
||||||
|
|
||||||
password_hash::derive_encryption_key(salt, password, &PSRAM_ALLOCATOR)
|
|
||||||
};
|
|
||||||
|
|
||||||
let output: &mut [u8] = unsafe { core::slice::from_raw_parts_mut(output, output_len) };
|
|
||||||
let mut user_key = critical_section::with(|cs| ACTIVE_ENCRYPTED_USER_KEY.borrow(cs).get());
|
|
||||||
|
|
||||||
password_hash::decrypt_with(&mut user_key, &encryption_key);
|
|
||||||
output.copy_from_slice(&user_key);
|
|
||||||
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
unsafe extern "C" fn __spre_crypto_generichash_blake2b_salt_personal(
|
|
||||||
data_out: NonNullPtr<*mut c_uchar>,
|
|
||||||
data_out_len: c_size_t,
|
|
||||||
data_in: *const c_uchar,
|
|
||||||
data_in_len: c_ulonglong,
|
|
||||||
key: *const c_uchar,
|
|
||||||
keylen: c_size_t,
|
|
||||||
salt: *const c_uchar,
|
|
||||||
personal: *const c_uchar,
|
|
||||||
) -> c_int {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
#[repr(C)]
|
|
||||||
struct crypto_hash_sha256_state {
|
|
||||||
state: [u32; 8],
|
|
||||||
count: u64,
|
|
||||||
buf: [u8; 64],
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_camel_case_types, unused)]
|
|
||||||
struct crypto_auth_hmacsha256_state {
|
|
||||||
ictx: crypto_hash_sha256_state,
|
|
||||||
octx: crypto_hash_sha256_state,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UnsafeSend<T>(pub T);
|
|
||||||
|
|
||||||
unsafe impl<T> Send for UnsafeSend<T> {}
|
|
||||||
|
|
||||||
// TODO: The software implementation is currently faster.
|
|
||||||
// My branch `sha-traits` of `esp-hal` implements the necessary traits
|
|
||||||
// to make `Sha256Context` work with `SimpleHmac`, but it would likely
|
|
||||||
// have to be optimized for `Hmac` instead to be faster.
|
|
||||||
// type HmacImpl = hmac::SimpleHmac<esp_hal::sha::Sha256Context>;
|
|
||||||
type HmacImpl = hmac::Hmac<sha2::Sha256>;
|
|
||||||
|
|
||||||
static HMAC: blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<Option<UnsafeSend<HmacImpl>>>> =
|
|
||||||
blocking_mutex::Mutex::new(RefCell::new(None));
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
unsafe extern "C" fn __spre_crypto_auth_hmacsha256_init(
|
|
||||||
state: NonNullPtr<*mut crypto_auth_hmacsha256_state>,
|
|
||||||
key: NonNullPtr<*const c_uchar>,
|
|
||||||
key_len: c_size_t,
|
|
||||||
) -> c_int {
|
|
||||||
let key: &[u8] = unsafe { core::slice::from_raw_parts(key, key_len) };
|
|
||||||
// TODO: Hardware-accelerated hashing via the SHA peripheral.
|
|
||||||
// SHA.lock(|sha| {
|
|
||||||
// let sha = sha
|
|
||||||
// .borrow_mut()
|
|
||||||
// .as_mut()
|
|
||||||
// .expect("HMAC peripheral not initialized.");
|
|
||||||
|
|
||||||
// sha.
|
|
||||||
// });
|
|
||||||
HMAC.lock(|hmac| {
|
|
||||||
let mut hmac = hmac.borrow_mut();
|
|
||||||
|
|
||||||
if hmac.is_some() {
|
|
||||||
panic!("HMAC already initialized. Cannot handle multiple HMAC's at once.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Implement a lower level `Sha256BlockContext` that would work with `Hmac`.
|
|
||||||
// This requires the implementation of a few `core_api` traits.
|
|
||||||
*hmac = Some(UnsafeSend(HmacImpl::new_from_slice(key).unwrap()));
|
|
||||||
});
|
|
||||||
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
unsafe extern "C" fn __spre_crypto_auth_hmacsha256_update(
|
|
||||||
state: NonNullPtr<*mut crypto_auth_hmacsha256_state>,
|
|
||||||
data_in: *const c_uchar,
|
|
||||||
data_in_len: c_ulonglong,
|
|
||||||
) -> c_int {
|
|
||||||
let data_in: &[u8] = unsafe { core::slice::from_raw_parts(data_in, data_in_len as usize) };
|
|
||||||
|
|
||||||
HMAC.lock(|hmac| {
|
|
||||||
let mut hmac = hmac.borrow_mut();
|
|
||||||
let UnsafeSend(hmac) = hmac
|
|
||||||
.as_mut()
|
|
||||||
.expect("HMAC must first be initialized before it is updated.");
|
|
||||||
|
|
||||||
hmac.update(data_in);
|
|
||||||
});
|
|
||||||
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
unsafe extern "C" fn __spre_crypto_auth_hmacsha256_final(
|
|
||||||
state: NonNullPtr<*mut crypto_auth_hmacsha256_state>,
|
|
||||||
out: NonNullPtr<*mut c_uchar>,
|
|
||||||
) -> c_int {
|
|
||||||
let out = unsafe { &mut *(out as *mut GenericArray<u8, U32>) };
|
|
||||||
|
|
||||||
HMAC.lock(|hmac| {
|
|
||||||
let mut hmac = hmac.borrow_mut();
|
|
||||||
let UnsafeSend(hmac) = hmac
|
|
||||||
.take()
|
|
||||||
.expect("HMAC must first be initialized before it is updated.");
|
|
||||||
|
|
||||||
hmac.finalize_into(out);
|
|
||||||
});
|
|
||||||
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
unsafe extern "C" fn __spre_sodium_bin2base64(
|
|
||||||
b64: NonNullPtr<*mut c_char>,
|
|
||||||
b64_maxlen: c_size_t,
|
|
||||||
bin: *const c_uchar,
|
|
||||||
bin_len: c_size_t,
|
|
||||||
variant: c_int,
|
|
||||||
) -> *mut c_char {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
unsafe extern "C" fn __spre_sodium_base642bin(
|
|
||||||
bin: NonNullPtr<*mut c_uchar>,
|
|
||||||
bin_maxlen: c_size_t,
|
|
||||||
b64: *const c_char,
|
|
||||||
b64_len: c_size_t,
|
|
||||||
ignore: *const c_char,
|
|
||||||
bin_len: *mut c_size_t,
|
|
||||||
b64_end: *mut *const c_char,
|
|
||||||
variant: c_int,
|
|
||||||
) -> c_int {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
use embassy_time::Instant;
|
|
||||||
|
|
||||||
// TODO: Is this right?
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
pub type time_t = i64;
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub unsafe extern "C" fn __spre_time(time: *mut time_t) -> time_t {
|
|
||||||
// TODO: This currently measures the time since boot,
|
|
||||||
// but it should actually measure the time since the unix epoch.
|
|
||||||
let duration = Instant::now().duration_since(Instant::MIN);
|
|
||||||
let seconds_since_epoch = duration.as_secs() as time_t;
|
|
||||||
|
|
||||||
if !time.is_null() {
|
|
||||||
unsafe {
|
|
||||||
*time = seconds_since_epoch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
seconds_since_epoch
|
|
||||||
}
|
|
||||||
|
|
@ -1,227 +0,0 @@
|
||||||
use core::fmt::Arguments;
|
|
||||||
|
|
||||||
use log::LevelFilter;
|
|
||||||
|
|
||||||
pub const LOG_LEVEL_FILTER: LevelFilter = {
|
|
||||||
if let Some(string) = option_env!("ESP_LOG") {
|
|
||||||
if string.eq_ignore_ascii_case("ERROR") {
|
|
||||||
LevelFilter::Error
|
|
||||||
} else if string.eq_ignore_ascii_case("WARN") {
|
|
||||||
LevelFilter::Warn
|
|
||||||
} else if string.eq_ignore_ascii_case("INFO") {
|
|
||||||
LevelFilter::Info
|
|
||||||
} else if string.eq_ignore_ascii_case("DEBUG") {
|
|
||||||
LevelFilter::Debug
|
|
||||||
} else if string.eq_ignore_ascii_case("TRACE") {
|
|
||||||
LevelFilter::Trace
|
|
||||||
} else {
|
|
||||||
panic!(
|
|
||||||
"Unknown `ESP_LOG` value. Only `ERROR`, `WARN`, `INFO`, `DEBUG`, `TRACE`, or `OFF` may be used."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LevelFilter::Off
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const RESET: &str = "\u{001B}[0m";
|
|
||||||
const RED: &str = "\u{001B}[31m";
|
|
||||||
const GREEN: &str = "\u{001B}[32m";
|
|
||||||
const YELLOW: &str = "\u{001B}[33m";
|
|
||||||
const BLUE: &str = "\u{001B}[34m";
|
|
||||||
const CYAN: &str = "\u{001B}[35m";
|
|
||||||
|
|
||||||
fn with_formatted_log_record<R>(
|
|
||||||
record: &log::Record,
|
|
||||||
callback: impl FnOnce(Arguments<'_>) -> R,
|
|
||||||
) -> R {
|
|
||||||
let color = match record.level() {
|
|
||||||
log::Level::Error => RED,
|
|
||||||
log::Level::Warn => YELLOW,
|
|
||||||
log::Level::Info => GREEN,
|
|
||||||
log::Level::Debug => BLUE,
|
|
||||||
log::Level::Trace => CYAN,
|
|
||||||
};
|
|
||||||
let args = format_args!(
|
|
||||||
"{}{:>5} - {}{}\n",
|
|
||||||
color,
|
|
||||||
record.level(),
|
|
||||||
record.args(),
|
|
||||||
RESET
|
|
||||||
);
|
|
||||||
(callback)(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The default USB logger.
|
|
||||||
#[cfg(feature = "usb-log")]
|
|
||||||
pub mod usb {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
pub fn setup_logging() -> impl Future<Output = ()> {
|
|
||||||
esp_println::logger::init_logger(LOG_LEVEL_FILTER);
|
|
||||||
log::info!("Logger initialized!");
|
|
||||||
async {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Alternative logger via UART.
|
|
||||||
#[cfg(feature = "alt-log")]
|
|
||||||
#[macro_use]
|
|
||||||
pub mod uart {
|
|
||||||
use super::*;
|
|
||||||
use crate::console;
|
|
||||||
use core::{cell::RefCell, fmt::Write};
|
|
||||||
use critical_section::{CriticalSection, Mutex};
|
|
||||||
use esp_hal::{
|
|
||||||
Blocking,
|
|
||||||
gpio::interconnect::{PeripheralInput, PeripheralOutput},
|
|
||||||
uart::{Uart, UartTx},
|
|
||||||
};
|
|
||||||
use log::{Log, info};
|
|
||||||
|
|
||||||
static ALT_LOGGER_UART: Mutex<RefCell<Option<UartTx<'static, Blocking>>>> =
|
|
||||||
Mutex::new(RefCell::new(None));
|
|
||||||
|
|
||||||
pub fn with_uart_tx<R>(
|
|
||||||
f: impl FnOnce(CriticalSection<'_>, &'_ mut UartTx<'static, Blocking>) -> R,
|
|
||||||
) -> R {
|
|
||||||
critical_section::with(|cs| {
|
|
||||||
let mut uart = ALT_LOGGER_UART.borrow(cs).borrow_mut();
|
|
||||||
let uart = uart.as_mut().unwrap();
|
|
||||||
|
|
||||||
(f)(cs, uart)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
macro_rules! println {
|
|
||||||
() => {{
|
|
||||||
do_print(Default::default());
|
|
||||||
}};
|
|
||||||
|
|
||||||
($($arg:tt)*) => {{
|
|
||||||
do_print(::core::format_args!($($arg)*));
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
fn do_print(args: core::fmt::Arguments<'_>) {
|
|
||||||
with_uart_tx(|_, uart| {
|
|
||||||
uart.write_fmt(format_args!("{}\n", args)).unwrap();
|
|
||||||
uart.flush().unwrap();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UartLogger;
|
|
||||||
|
|
||||||
impl Log for UartLogger {
|
|
||||||
#[allow(unused)]
|
|
||||||
fn enabled(&self, _: &log::Metadata) -> bool {
|
|
||||||
// Filtered by `log` already
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
fn log(&self, record: &log::Record) {
|
|
||||||
with_uart_tx(|cs, uart| {
|
|
||||||
with_formatted_log_record(record, |args| uart.write_fmt(args)).unwrap();
|
|
||||||
uart.flush().unwrap();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&self) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[panic_handler]
|
|
||||||
fn panic_handler(info: &core::panic::PanicInfo) -> ! {
|
|
||||||
use super::{RED, RESET};
|
|
||||||
use esp_backtrace::Backtrace;
|
|
||||||
|
|
||||||
println!("{RED}");
|
|
||||||
println!("=============== CUSTOM PANIC HANDLER ==============");
|
|
||||||
println!("{info}{RESET}");
|
|
||||||
println!("");
|
|
||||||
println!("Backtrace:");
|
|
||||||
println!("");
|
|
||||||
|
|
||||||
let backtrace = Backtrace::capture();
|
|
||||||
|
|
||||||
for frame in backtrace.frames() {
|
|
||||||
println!("0x{:x}", frame.program_counter());
|
|
||||||
}
|
|
||||||
|
|
||||||
loop {}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setup_logging(
|
|
||||||
uart: impl esp_hal::uart::Instance + 'static,
|
|
||||||
tx: impl PeripheralOutput<'static>,
|
|
||||||
rx: impl PeripheralInput<'static>,
|
|
||||||
) -> impl Future<Output = ()> {
|
|
||||||
let (uart_rx, uart_tx) = Uart::new(uart, Default::default())
|
|
||||||
.unwrap()
|
|
||||||
.with_tx(tx)
|
|
||||||
.with_rx(rx)
|
|
||||||
.split();
|
|
||||||
|
|
||||||
critical_section::with(|cs| {
|
|
||||||
*ALT_LOGGER_UART.borrow(cs).borrow_mut() = Some(uart_tx);
|
|
||||||
});
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
log::set_logger_racy(&UartLogger).unwrap();
|
|
||||||
log::set_max_level_racy(LOG_LEVEL_FILTER);
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Logger initialized!");
|
|
||||||
console::run_console(uart_rx.into_async())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Logging via RTT for probe-rs.
|
|
||||||
#[cfg(feature = "rtt-log")]
|
|
||||||
pub mod rtt {
|
|
||||||
use super::*;
|
|
||||||
#[allow(unused)]
|
|
||||||
pub use ::rtt_target::{rprint as print, rprintln as println};
|
|
||||||
use panic_rtt_target as _; // Use the RTT panic handler.
|
|
||||||
use rtt_target::ChannelMode;
|
|
||||||
|
|
||||||
pub fn setup_logging() -> impl Future<Output = ()> {
|
|
||||||
rtt_target::rtt_init_log!(LOG_LEVEL_FILTER, ChannelMode::BlockIfFull);
|
|
||||||
log::info!("Logger initialized!");
|
|
||||||
async {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// #[macro_export]
|
|
||||||
// macro_rules! dbg {
|
|
||||||
// () => {
|
|
||||||
// $crate::logging::implementation::println!("[{}:{}:{}]", $crate::file!(), $crate::line!(), $crate::column!())
|
|
||||||
// };
|
|
||||||
// ($val:expr $(,)?) => {
|
|
||||||
// match $val {
|
|
||||||
// tmp => {
|
|
||||||
// $crate::logging::uart::println!("[{}:{}:{}] {} = {:#?}",
|
|
||||||
// file!(),
|
|
||||||
// line!(),
|
|
||||||
// column!(),
|
|
||||||
// stringify!($val),
|
|
||||||
// // The `&T: Debug` check happens here (not in the format literal desugaring)
|
|
||||||
// // to avoid format literal related messages and suggestions.
|
|
||||||
// &&tmp as &dyn ::core::fmt::Debug,
|
|
||||||
// );
|
|
||||||
// tmp
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// ($($val:expr),+ $(,)?) => {
|
|
||||||
// ($($crate::dbg!($val)),+,)
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[cfg(feature = "alt-log")]
|
|
||||||
// pub use uart as implementation;
|
|
||||||
|
|
||||||
// #[cfg(feature = "rtt-log")]
|
|
||||||
// pub use rtt as implementation;
|
|
||||||
|
|
@ -1,615 +0,0 @@
|
||||||
use core::alloc::Layout;
|
|
||||||
use core::fmt::Debug;
|
|
||||||
use core::slice;
|
|
||||||
|
|
||||||
use alloc::alloc::Allocator;
|
|
||||||
use alloc::boxed::Box;
|
|
||||||
use alloc::collections::btree_map::{BTreeMap, Entry};
|
|
||||||
use alloc::string::String;
|
|
||||||
use alloc::sync::Arc;
|
|
||||||
use alloc::vec::Vec;
|
|
||||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
|
||||||
use embassy_sync::channel::Channel;
|
|
||||||
use embassy_time::Instant;
|
|
||||||
use esp_alloc::{EspHeap, MemoryCapability};
|
|
||||||
use log::{debug, error, info, warn};
|
|
||||||
use rmk::descriptor::KeyboardReport;
|
|
||||||
use rmk::hid::Report;
|
|
||||||
use rmk::{heapless, join_all};
|
|
||||||
use slint::platform::Key;
|
|
||||||
use xkbcommon::xkb::{self, FeedResult, KeyDirection, Keysym, ModMask, Status};
|
|
||||||
|
|
||||||
use crate::util::{DurationExt, get_file_name};
|
|
||||||
use crate::{KEYBOARD_REPORT_PROXY, PSRAM_ALLOCATOR};
|
|
||||||
|
|
||||||
pub static KEY_MESSAGE_CHANNEL: Channel<CriticalSectionRawMutex, KeyMessage, 16> = Channel::new();
|
|
||||||
pub static OUTPUT_STRING_CHANNEL: Channel<CriticalSectionRawMutex, String, 16> = Channel::new();
|
|
||||||
|
|
||||||
pub struct KeyMessage {
|
|
||||||
pub keysym: Keysym,
|
|
||||||
pub string: Option<String>,
|
|
||||||
pub direction: KeyDirection,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for KeyMessage {
|
|
||||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
||||||
f.debug_struct("KeyMessage")
|
|
||||||
.field("keysym", &self.keysym)
|
|
||||||
.field("string", &self.string)
|
|
||||||
.field_with("direction", |f| match self.direction {
|
|
||||||
KeyDirection::Down => f.write_str("Down"),
|
|
||||||
KeyDirection::Up => f.write_str("Up"),
|
|
||||||
})
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_hid_report_interceptor() -> impl Future<Output = ()> {
|
|
||||||
// TODO: Use `include_dir` or similar to support multiple keymaps.
|
|
||||||
const KEYMAP_NAME: &str = get_file_name(env!("ACID_KEYMAP_PATH"));
|
|
||||||
const KEYMAP_STRING: &str = include_str!(env!("ACID_KEYMAP_PATH"));
|
|
||||||
const COMPOSE_MAP_NAME: &str = get_file_name(env!("ACID_COMPOSE_PATH"));
|
|
||||||
const COMPOSE_MAP_STRING: &str = include_str!(env!("ACID_COMPOSE_PATH"));
|
|
||||||
// E.g. `cs_CZ.UTF-8`
|
|
||||||
const COMPOSE_MAP_LOCALE: &str = env!("ACID_COMPOSE_LOCALE");
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Keymap: {KEYMAP_NAME:?}, compose: {COMPOSE_MAP_NAME:?}, compose locale: {COMPOSE_MAP_LOCALE:?}"
|
|
||||||
);
|
|
||||||
|
|
||||||
let keymap_string_buffer = unsafe {
|
|
||||||
let allocation = PSRAM_ALLOCATOR.alloc_caps(
|
|
||||||
MemoryCapability::External.into(),
|
|
||||||
Layout::from_size_align(KEYMAP_STRING.len(), 32).unwrap(),
|
|
||||||
);
|
|
||||||
let slice = str::from_utf8_unchecked_mut(slice::from_raw_parts_mut(
|
|
||||||
allocation,
|
|
||||||
KEYMAP_STRING.len(),
|
|
||||||
));
|
|
||||||
|
|
||||||
slice
|
|
||||||
.as_bytes_mut()
|
|
||||||
.copy_from_slice(KEYMAP_STRING.as_bytes());
|
|
||||||
|
|
||||||
Box::from_raw_in(slice as *mut str, &PSRAM_ALLOCATOR)
|
|
||||||
};
|
|
||||||
let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
|
|
||||||
info!("Loading XKB keymap...");
|
|
||||||
let instant_start = Instant::now();
|
|
||||||
let keymap = Arc::new_in(
|
|
||||||
xkb::Keymap::new_from_string(
|
|
||||||
&context,
|
|
||||||
keymap_string_buffer,
|
|
||||||
xkb::KEYMAP_FORMAT_TEXT_V1,
|
|
||||||
xkb::KEYMAP_COMPILE_NO_FLAGS,
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
&PSRAM_ALLOCATOR,
|
|
||||||
);
|
|
||||||
let duration = Instant::now().duration_since(instant_start);
|
|
||||||
info!(
|
|
||||||
"XKB keymap loaded successfully! Took {} seconds.",
|
|
||||||
duration.display_as_secs(),
|
|
||||||
);
|
|
||||||
info!("Loading XKB compose map...");
|
|
||||||
let instant_start = Instant::now();
|
|
||||||
let compose_table = Arc::new_in(
|
|
||||||
xkb::compose::Table::new_from_buffer(
|
|
||||||
&context,
|
|
||||||
COMPOSE_MAP_STRING,
|
|
||||||
COMPOSE_MAP_LOCALE,
|
|
||||||
xkb::compose::FORMAT_TEXT_V1,
|
|
||||||
xkb::compose::COMPILE_NO_FLAGS,
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
&PSRAM_ALLOCATOR,
|
|
||||||
);
|
|
||||||
let duration = Instant::now().duration_since(instant_start);
|
|
||||||
info!(
|
|
||||||
"XKB compose map loaded successfully! Took {} seconds.",
|
|
||||||
duration.display_as_secs()
|
|
||||||
);
|
|
||||||
|
|
||||||
async move {
|
|
||||||
join_all![
|
|
||||||
send_keycodes_to_slint(keymap.clone(), compose_table.clone()),
|
|
||||||
send_strings_to_host(keymap, compose_table)
|
|
||||||
]
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const KEYCODES_LEN_MODIFIERS: usize = 8;
|
|
||||||
const USB_HID_LEFT_CTRL: u8 = 0xE0;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct ModifierDescriptor {
|
|
||||||
name: &'static str,
|
|
||||||
hid_flag: Option<u8>,
|
|
||||||
keycode: Option<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ModifierDescriptor {
|
|
||||||
const fn new(name: &'static str, hid_index: Option<u8>) -> Self {
|
|
||||||
Self {
|
|
||||||
name,
|
|
||||||
hid_flag: if let Some(hid_index) = hid_index {
|
|
||||||
Some(1_u8 << hid_index)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
keycode: if let Some(hid_index) = hid_index {
|
|
||||||
Some(USB_HID_LEFT_CTRL + hid_index)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_strings_to_host<A: Allocator>(
|
|
||||||
keymap: Arc<xkb::Keymap, A>,
|
|
||||||
compose_table: Arc<xkb::compose::Table, A>,
|
|
||||||
) {
|
|
||||||
/// Names of modifiers in the conventional order.
|
|
||||||
const XKB_REAL_MODIFIER_DESCRIPTORS: [ModifierDescriptor; KEYCODES_LEN_MODIFIERS] = [
|
|
||||||
ModifierDescriptor::new(xkb::MOD_NAME_SHIFT, Some(1)),
|
|
||||||
ModifierDescriptor::new(xkb::MOD_NAME_CAPS, None),
|
|
||||||
ModifierDescriptor::new(xkb::MOD_NAME_CTRL, Some(0)),
|
|
||||||
ModifierDescriptor::new(xkb::MOD_NAME_ALT, Some(2)),
|
|
||||||
ModifierDescriptor::new(xkb::MOD_NAME_NUM, None),
|
|
||||||
ModifierDescriptor::new(xkb::MOD_NAME_MOD3, None),
|
|
||||||
ModifierDescriptor::new(xkb::MOD_NAME_LOGO, Some(3)),
|
|
||||||
ModifierDescriptor::new(xkb::MOD_NAME_ISO_LEVEL3_SHIFT, Some(6)),
|
|
||||||
];
|
|
||||||
|
|
||||||
let modifier_xkb_index_to_descriptor = keymap
|
|
||||||
.mods()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(index, modifier)| {
|
|
||||||
let descriptor = XKB_REAL_MODIFIER_DESCRIPTORS
|
|
||||||
.iter()
|
|
||||||
.find(|descriptor| descriptor.name == modifier);
|
|
||||||
debug!("Modifier #{index} {descriptor:02x?}");
|
|
||||||
descriptor
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
info!("modifier_xkb_index_to_hid_flag: {modifier_xkb_index_to_descriptor:02x?}");
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let string = OUTPUT_STRING_CHANNEL.receive().await;
|
|
||||||
let string_keycodes = string_to_hid_keycodes(&keymap, &compose_table, &string);
|
|
||||||
|
|
||||||
warn!("keycodes for {string:?}: {string_keycodes:02x?}");
|
|
||||||
|
|
||||||
if let Ok(string_keycodes) = string_keycodes {
|
|
||||||
for keycode in string_keycodes {
|
|
||||||
let mut keycodes_vec = heapless::Vec::<u8, 6>::new();
|
|
||||||
let modifier = {
|
|
||||||
let mut modifier = 0;
|
|
||||||
let mut remaining_mask = keycode.mod_mask;
|
|
||||||
let mut index = 0;
|
|
||||||
|
|
||||||
while remaining_mask != 0 {
|
|
||||||
if remaining_mask & 1 != 0
|
|
||||||
&& let Some(mod_descriptor) = modifier_xkb_index_to_descriptor[index]
|
|
||||||
{
|
|
||||||
if let Some(hid_flag) = mod_descriptor.hid_flag {
|
|
||||||
modifier |= hid_flag;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(keycode) = mod_descriptor.keycode {
|
|
||||||
let _ = keycodes_vec.push(keycode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
remaining_mask >>= 1;
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
modifier
|
|
||||||
};
|
|
||||||
|
|
||||||
if keycodes_vec.len() + 1 > keycodes_vec.capacity() {
|
|
||||||
warn!("Failed to send key because the keycode buffer is full.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Check whether this is necessary, otherwise remove.
|
|
||||||
rmk::channel::KEYBOARD_REPORT_RECEIVER
|
|
||||||
.send(Report::KeyboardReport(KeyboardReport {
|
|
||||||
modifier,
|
|
||||||
reserved: Default::default(),
|
|
||||||
leds: Default::default(),
|
|
||||||
keycodes: {
|
|
||||||
let mut keycodes = [0; _];
|
|
||||||
keycodes[..keycodes_vec.len()].copy_from_slice(&keycodes_vec);
|
|
||||||
keycodes
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
keycodes_vec
|
|
||||||
.push(keycode.keycode)
|
|
||||||
.expect("no space for the main keycode");
|
|
||||||
warn!(
|
|
||||||
"Sending HID keycode 0x{:02x} with modifier 0x{:02x} (xkb mask 0x{:02x}) as keycode sequence {keycodes_vec:02x?}",
|
|
||||||
keycode.keycode, modifier, keycode.mod_mask
|
|
||||||
);
|
|
||||||
|
|
||||||
rmk::channel::KEYBOARD_REPORT_RECEIVER
|
|
||||||
.send(Report::KeyboardReport(KeyboardReport {
|
|
||||||
modifier,
|
|
||||||
reserved: Default::default(),
|
|
||||||
leds: Default::default(),
|
|
||||||
keycodes: {
|
|
||||||
let mut keycodes = [0; _];
|
|
||||||
keycodes[..keycodes_vec.len()].copy_from_slice(&keycodes_vec);
|
|
||||||
keycodes
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
.await;
|
|
||||||
rmk::channel::KEYBOARD_REPORT_RECEIVER
|
|
||||||
.send(Report::KeyboardReport(KeyboardReport::default()))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_keycodes_to_slint<A: Allocator>(
|
|
||||||
keymap: Arc<xkb::Keymap, A>,
|
|
||||||
compose_table: Arc<xkb::compose::Table, A>,
|
|
||||||
) {
|
|
||||||
let mut state = xkb::State::new(&keymap);
|
|
||||||
let mut previous_state = KeyboardReport::default();
|
|
||||||
let mut compose_state = xkb::compose::State::new(&compose_table, xkb::compose::STATE_NO_FLAGS);
|
|
||||||
// TODO: Use a stack-allocated map instead
|
|
||||||
// This is a map from the basic keysyms (not a composed ones) to the string that should be produced.
|
|
||||||
let mut pressed_keys_to_strings = BTreeMap::<Keysym, Option<String>>::new();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let report = KEYBOARD_REPORT_PROXY.receive().await;
|
|
||||||
|
|
||||||
info!("Intercepted HID keyboard report: {report:#02x?}");
|
|
||||||
|
|
||||||
if let Report::KeyboardReport(report) = &report {
|
|
||||||
const KEYCODES_LEN_REGULAR: usize = 6;
|
|
||||||
const KEYCODES_LEN: usize = KEYCODES_LEN_MODIFIERS + KEYCODES_LEN_REGULAR;
|
|
||||||
|
|
||||||
let mut pressed_keys = rmk::heapless::Vec::<u8, KEYCODES_LEN>::new();
|
|
||||||
let mut released_keys = rmk::heapless::Vec::<u8, KEYCODES_LEN>::new();
|
|
||||||
|
|
||||||
let pressed_mods_bits = !previous_state.modifier & report.modifier;
|
|
||||||
let released_mods_bits = previous_state.modifier & !report.modifier;
|
|
||||||
|
|
||||||
for index in 0..KEYCODES_LEN_MODIFIERS {
|
|
||||||
let mod_bit = 1_u8 << index;
|
|
||||||
let mod_keycode = USB_HID_LEFT_CTRL + index as u8;
|
|
||||||
|
|
||||||
if pressed_mods_bits & mod_bit != 0 {
|
|
||||||
pressed_keys.push(mod_keycode).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
if released_mods_bits & mod_bit != 0 {
|
|
||||||
released_keys.push(mod_keycode).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This currently depends on pressed keys not changing position in the array.
|
|
||||||
// Should be made independent of that.
|
|
||||||
for (&keycode_old, &keycode_new) in
|
|
||||||
core::iter::zip(&previous_state.keycodes, &report.keycodes)
|
|
||||||
{
|
|
||||||
if keycode_old == keycode_new {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if keycode_old != 0 {
|
|
||||||
released_keys.push(keycode_old).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
if keycode_new != 0 {
|
|
||||||
pressed_keys.push(keycode_new).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
previous_state = *report;
|
|
||||||
|
|
||||||
for keycode in released_keys {
|
|
||||||
debug!("Release: 0x{:02x} ({})", keycode, keycode);
|
|
||||||
let keycode_xkb = hid_to_xkb_keycode(keycode);
|
|
||||||
state.update_key(keycode_xkb, KeyDirection::Up);
|
|
||||||
let keysym = state.key_get_one_sym(keycode_xkb);
|
|
||||||
let string = pressed_keys_to_strings.remove(&keysym).unwrap_or_else(|| {
|
|
||||||
warn!("Could not determine the string of a released key: keysym={keysym:?} pressed_keys_to_strings={pressed_keys_to_strings:?}");
|
|
||||||
None
|
|
||||||
});
|
|
||||||
KEY_MESSAGE_CHANNEL
|
|
||||||
.send(KeyMessage {
|
|
||||||
keysym,
|
|
||||||
string,
|
|
||||||
direction: KeyDirection::Up,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
for keycode in pressed_keys {
|
|
||||||
debug!("Pressed: 0x{:02x} ({})", keycode, keycode);
|
|
||||||
let keycode_xkb = hid_to_xkb_keycode(keycode);
|
|
||||||
let sym = state.key_get_one_sym(keycode_xkb);
|
|
||||||
|
|
||||||
let result: Option<(Keysym, Keysym, Option<String>)> = match compose_state.feed(sym)
|
|
||||||
{
|
|
||||||
FeedResult::Ignored => {
|
|
||||||
let string = state.key_get_utf8(keycode_xkb);
|
|
||||||
Some((sym, sym, Some(string)))
|
|
||||||
}
|
|
||||||
FeedResult::Accepted => {
|
|
||||||
let status = compose_state.status();
|
|
||||||
debug!("Compose status: {status:?}");
|
|
||||||
match status {
|
|
||||||
Status::Nothing => {
|
|
||||||
let string = state.key_get_utf8(keycode_xkb);
|
|
||||||
Some((sym, sym, Some(string)))
|
|
||||||
}
|
|
||||||
Status::Composing => None,
|
|
||||||
Status::Composed => {
|
|
||||||
let composed_sym = compose_state.keysym().unwrap_or_default();
|
|
||||||
let string = compose_state.utf8().unwrap_or_default();
|
|
||||||
Some((sym, composed_sym, Some(string)))
|
|
||||||
}
|
|
||||||
Status::Cancelled => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some((basic_keysym, composed_keysym, string)) = result {
|
|
||||||
// Change `Some("")` into `None`.
|
|
||||||
let string = string.filter(|string| !string.is_empty());
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Basic keysym: {basic_keysym:?}, composed keysym: {composed_keysym:?}, string: {:?}",
|
|
||||||
string.as_ref()
|
|
||||||
);
|
|
||||||
|
|
||||||
pressed_keys_to_strings.insert(basic_keysym, string.clone());
|
|
||||||
KEY_MESSAGE_CHANNEL
|
|
||||||
.send(KeyMessage {
|
|
||||||
keysym: composed_keysym,
|
|
||||||
string,
|
|
||||||
direction: KeyDirection::Down,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
state.update_key(keycode_xkb, KeyDirection::Down);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn keysym_to_xkb_keycode() {}
|
|
||||||
|
|
||||||
/// Based on https://github.com/xkbcommon/libxkbcommon/blob/6c67e3d41d3215ab1edd4406de215c7bf1f20c74/tools/how-to-type.c#L389
|
|
||||||
/// Fields reordered for `Ord` based on https://github.com/xkbcommon/libxkbcommon/blob/6c67e3d41d3215ab1edd4406de215c7bf1f20c74/tools/how-to-type.c#L404
|
|
||||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
struct KeysymEntry {
|
|
||||||
layout: xkb::LayoutIndex,
|
|
||||||
level: xkb::LevelIndex,
|
|
||||||
keycode: xkb::Keycode,
|
|
||||||
mask: xkb::ModMask,
|
|
||||||
}
|
|
||||||
|
|
||||||
type KeysymEntries = Vec<KeysymEntry, &'static EspHeap>;
|
|
||||||
type KeysymMap = BTreeMap<Keysym, KeysymEntries, &'static EspHeap>;
|
|
||||||
|
|
||||||
/// Based on https://github.com/xkbcommon/libxkbcommon/blob/6c67e3d41d3215ab1edd4406de215c7bf1f20c74/tools/how-to-type.c#L434
|
|
||||||
fn lookup_keysym_entries(
|
|
||||||
entries: &mut KeysymMap,
|
|
||||||
keysym: xkb::Keysym,
|
|
||||||
insert: bool,
|
|
||||||
) -> Option<&mut KeysymEntries> {
|
|
||||||
match entries.entry(keysym) {
|
|
||||||
Entry::Occupied(occupied) => Some(occupied.into_mut()),
|
|
||||||
Entry::Vacant(vacant) if insert => Some(vacant.insert(Vec::new_in(&PSRAM_ALLOCATOR))),
|
|
||||||
Entry::Vacant(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct HidKeycodeWithMods {
|
|
||||||
keycode: u8,
|
|
||||||
mod_mask: ModMask,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn string_to_hid_keycodes(
|
|
||||||
keymap: &xkb::Keymap,
|
|
||||||
_compose_table: &xkb::compose::Table,
|
|
||||||
string: &str,
|
|
||||||
) -> Result<Vec<HidKeycodeWithMods, &'static EspHeap>, char> {
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
struct KeycodeChoice {
|
|
||||||
mod_mask: ModMask,
|
|
||||||
hid_keycode: u8,
|
|
||||||
xkb_keycode: xkb::Keycode,
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut hid_keycodes = Vec::new_in(&PSRAM_ALLOCATOR);
|
|
||||||
|
|
||||||
for character in string.chars() {
|
|
||||||
let keysym = xkbcommon::xkb::utf32_to_keysym(character as u32);
|
|
||||||
let mut chosen_keycode = None;
|
|
||||||
|
|
||||||
keymap.key_for_each(|keymap, xkb_keycode| {
|
|
||||||
for layout_index in 0..keymap.num_layouts_for_key(xkb_keycode) {
|
|
||||||
for level_index in 0..keymap.num_levels_for_key(xkb_keycode, layout_index) {
|
|
||||||
let [current_keysym] =
|
|
||||||
keymap.key_get_syms_by_level(xkb_keycode, layout_index, level_index)
|
|
||||||
else {
|
|
||||||
// Multi-keysym levels are currently not handled.
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if current_keysym == &keysym {
|
|
||||||
let mut masks: [ModMask; 32 /* TODO: Re-evaluate the appropriate size */] = Default::default();
|
|
||||||
let masks_len = keymap.key_get_mods_for_level(
|
|
||||||
xkb_keycode,
|
|
||||||
layout_index,
|
|
||||||
level_index,
|
|
||||||
&mut masks,
|
|
||||||
);
|
|
||||||
let masks = &mut masks[0..masks_len];
|
|
||||||
let hid_keycode = xkb_to_hid_keycode(xkb_keycode);
|
|
||||||
|
|
||||||
info!("Candidate for {character:?} ({keysym:?}) considered: XKB = 0x{xkb_keycode:02x?}, HID = 0x{hid_keycode:02x?}, layout = {layout_index:?}, level = {level_index:?}, masks = 0x{masks:02x?}", xkb_keycode = xkb_keycode.raw());
|
|
||||||
|
|
||||||
let Some(hid_keycode) = hid_keycode else {
|
|
||||||
warn!("Could not translate XKB {xkb_keycode:?} to an HID keycode, skipping candidate.");
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (0x65..USB_HID_LEFT_CTRL).contains(&hid_keycode) {
|
|
||||||
warn!("Skipping candidate because is in a poorly supported range of HID keycodes.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(&simplest_mod_mask) = masks.iter().min() else {
|
|
||||||
error!("No mod mask found for this key.");
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let keycode_choice = KeycodeChoice {
|
|
||||||
mod_mask: simplest_mod_mask,
|
|
||||||
hid_keycode,
|
|
||||||
xkb_keycode,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(found_keycode) = &mut chosen_keycode {
|
|
||||||
*found_keycode = core::cmp::min(*found_keycode, keycode_choice);
|
|
||||||
} else {
|
|
||||||
chosen_keycode = Some(keycode_choice);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let Some(KeycodeChoice {
|
|
||||||
mod_mask,
|
|
||||||
hid_keycode,
|
|
||||||
xkb_keycode,
|
|
||||||
}) = chosen_keycode
|
|
||||||
else {
|
|
||||||
return Err(character);
|
|
||||||
};
|
|
||||||
|
|
||||||
warn!(
|
|
||||||
"Candidate for {character:?} ({keysym:?}) chosen: XKB = 0x{xkb_keycode:02x?}, HID = 0x{hid_keycode:02x?}, mod_mask = 0x{mod_mask:02x?}",
|
|
||||||
xkb_keycode = xkb_keycode.raw()
|
|
||||||
);
|
|
||||||
|
|
||||||
hid_keycodes.push(HidKeycodeWithMods {
|
|
||||||
keycode: hid_keycode,
|
|
||||||
mod_mask,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(hid_keycodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn hid_to_xkb_keycode_inner(rmk_keycode: u8) -> u8 {
|
|
||||||
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/hid/hid-input.c?id=refs/tags/v6.18#n27
|
|
||||||
const UNK: u8 = 240;
|
|
||||||
#[rustfmt::skip]
|
|
||||||
const HID_KEYBOARD: [u8; 256] = [
|
|
||||||
0, 0, 0, 0, 30, 48, 46, 32, 18, 33, 34, 35, 23, 36, 37, 38,
|
|
||||||
50, 49, 24, 25, 16, 19, 31, 20, 22, 47, 17, 45, 21, 44, 2, 3,
|
|
||||||
4, 5, 6, 7, 8, 9, 10, 11, 28, 1, 14, 15, 57, 12, 13, 26,
|
|
||||||
27, 43, 43, 39, 40, 41, 51, 52, 53, 58, 59, 60, 61, 62, 63, 64,
|
|
||||||
65, 66, 67, 68, 87, 88, 99, 70, 119, 110, 102, 104, 111, 107, 109, 106,
|
|
||||||
105, 108, 103, 69, 98, 55, 74, 78, 96, 79, 80, 81, 75, 76, 77, 71,
|
|
||||||
72, 73, 82, 83, 86, 127, 116, 117, 183, 184, 185, 186, 187, 188, 189, 190,
|
|
||||||
191, 192, 193, 194, 134, 138, 130, 132, 128, 129, 131, 137, 133, 135, 136, 113,
|
|
||||||
115, 114, UNK, UNK, UNK, 121, UNK, 89, 93, 124, 92, 94, 95, UNK, UNK, UNK,
|
|
||||||
122, 123, 90, 91, 85, UNK, UNK, UNK, UNK, UNK, UNK, UNK, 111, UNK, UNK, UNK,
|
|
||||||
UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK,
|
|
||||||
UNK, UNK, UNK, UNK, UNK, UNK, 179, 180, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK,
|
|
||||||
UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK,
|
|
||||||
UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, 111, UNK, UNK, UNK, UNK, UNK, UNK, UNK,
|
|
||||||
29, 42, 56, 125, 97, 54, 100, 126, 164, 166, 165, 163, 161, 115, 114, 113,
|
|
||||||
150, 158, 159, 128, 136, 177, 178, 176, 142, 152, 173, 140, UNK, UNK, UNK, UNK,
|
|
||||||
];
|
|
||||||
// https://cgit.freedesktop.org/xorg/driver/xf86-input-evdev/tree/src/evdev.c#n73
|
|
||||||
const MIN_KEYCODE: u8 = 8;
|
|
||||||
|
|
||||||
// TODO: The combination of these two operations should be precomputed
|
|
||||||
// in a const expr into a single look-up table.
|
|
||||||
HID_KEYBOARD[rmk_keycode as usize] + MIN_KEYCODE
|
|
||||||
// xkb::Keycode::new((HID_KEYBOARD[rmk_keycode as usize] + MIN_KEYCODE) as u32)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn hid_to_xkb_keycode(hid_keycode: u8) -> xkb::Keycode {
|
|
||||||
const HID_TO_XKB: [u8; 256] = {
|
|
||||||
let mut hid_to_xkb = [0_u8; _];
|
|
||||||
let mut hid: u8 = 0;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
hid_to_xkb[hid as usize] = hid_to_xkb_keycode_inner(hid);
|
|
||||||
let overflow;
|
|
||||||
(hid, overflow) = hid.overflowing_add(1);
|
|
||||||
if overflow {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hid_to_xkb
|
|
||||||
};
|
|
||||||
|
|
||||||
xkb::Keycode::new(HID_TO_XKB[hid_keycode as usize] as u32)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn xkb_to_hid_keycode(xkb_keycode: xkb::Keycode) -> Option<u8> {
|
|
||||||
const XKB_TO_HID: [u8; 256] = {
|
|
||||||
let mut xkb_to_hid = [0_u8; _];
|
|
||||||
let mut hid: u8 = 0;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
xkb_to_hid[hid_to_xkb_keycode_inner(hid) as usize] = hid;
|
|
||||||
let overflow;
|
|
||||||
(hid, overflow) = hid.overflowing_add(1);
|
|
||||||
if overflow {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
xkb_to_hid
|
|
||||||
};
|
|
||||||
|
|
||||||
if (xkb_keycode.raw() as usize) < XKB_TO_HID.len() {
|
|
||||||
Some(XKB_TO_HID[xkb_keycode.raw() as usize])
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait TryFromKeysym: Sized {
|
|
||||||
fn try_from_keysym(k: Keysym) -> Option<Self>;
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! declare_consts_for_special_keys {
|
|
||||||
($($_char:literal # $name:ident # $($_qt:ident)|* # $($_winit:ident $(($_pos:ident))?)|* # $($xkb:ident)|*;)*) => {
|
|
||||||
impl TryFromKeysym for Key {
|
|
||||||
fn try_from_keysym(k: Keysym) -> Option<Self> {
|
|
||||||
match k {
|
|
||||||
$(
|
|
||||||
$(Keysym::$xkb => Some(Key::$name),)*
|
|
||||||
)*
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
i_slint_common::for_each_special_keys!(declare_consts_for_special_keys);
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
use core::{
|
|
||||||
cell::RefCell,
|
|
||||||
sync::atomic::{AtomicBool, Ordering},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use alloc::{
|
|
||||||
boxed::Box, collections::vec_deque::VecDeque, rc::Rc, string::ToString, sync::Arc, vec::Vec,
|
|
||||||
};
|
|
||||||
use critical_section::Mutex;
|
|
||||||
use esp_hal::time::Instant;
|
|
||||||
use log::{debug, info};
|
|
||||||
use slint::{
|
|
||||||
EventLoopError, PhysicalSize, SharedString, WindowSize,
|
|
||||||
platform::{
|
|
||||||
EventLoopProxy, Key, WindowEvent,
|
|
||||||
software_renderer::{RenderingRotation, RepaintBufferType, Rgb565Pixel, SoftwareRenderer},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use xkbcommon::xkb::{self, Keysym};
|
|
||||||
|
|
||||||
use crate::proxy::{KEY_MESSAGE_CHANNEL, TryFromKeysym};
|
|
||||||
|
|
||||||
use super::window_adapter::SoftwareWindowAdapter;
|
|
||||||
|
|
||||||
pub struct FramebufferPtr(pub *mut [Rgb565Pixel]);
|
|
||||||
|
|
||||||
unsafe impl Send for FramebufferPtr {}
|
|
||||||
|
|
||||||
pub struct SlintBackend {
|
|
||||||
pub window_size: [u32; 2],
|
|
||||||
pub window: RefCell<Option<Rc<SoftwareWindowAdapter>>>,
|
|
||||||
pub framebuffer: FramebufferPtr,
|
|
||||||
pub quit_event_loop: Arc<AtomicBool>,
|
|
||||||
pub events: Arc<Mutex<RefCell<VecDeque<Box<dyn FnOnce() + Send>>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl slint::platform::Platform for SlintBackend {
|
|
||||||
fn create_window_adapter(
|
|
||||||
&self,
|
|
||||||
) -> Result<Rc<dyn slint::platform::WindowAdapter>, slint::PlatformError> {
|
|
||||||
let renderer = SoftwareRenderer::new_with_repaint_buffer_type(
|
|
||||||
RepaintBufferType::ReusedBuffer, /* TODO: Implement a swapchain */
|
|
||||||
);
|
|
||||||
renderer.set_rendering_rotation(RenderingRotation::Rotate270);
|
|
||||||
let window = SoftwareWindowAdapter::new(renderer);
|
|
||||||
// window.set_scale_factor(4.0);
|
|
||||||
window.set_size(WindowSize::Physical(PhysicalSize::new(
|
|
||||||
self.window_size[0],
|
|
||||||
self.window_size[1],
|
|
||||||
)));
|
|
||||||
self.window.replace(Some(window.clone()));
|
|
||||||
Ok(window)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn duration_since_start(&self) -> Duration {
|
|
||||||
Duration::from_millis(Instant::now().duration_since_epoch().as_millis())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_event_loop_proxy(&self) -> Option<Box<dyn EventLoopProxy>> {
|
|
||||||
Some(Box::new(AcidEventLoopProxy {
|
|
||||||
quit_event_loop: self.quit_event_loop.clone(),
|
|
||||||
events: self.events.clone(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_event_loop(&self) -> Result<(), slint::PlatformError> {
|
|
||||||
// Instead of `loop`ing here, we execute a single iteration and handle `loop`ing
|
|
||||||
// in `crate::run_renderer_task`, where we can make use of `await`.
|
|
||||||
|
|
||||||
/* loop */
|
|
||||||
{
|
|
||||||
let drained_events = critical_section::with(|cs| {
|
|
||||||
self.events
|
|
||||||
.borrow(cs)
|
|
||||||
.borrow_mut()
|
|
||||||
.drain(..)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
});
|
|
||||||
|
|
||||||
for event in drained_events {
|
|
||||||
(event)();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(window) = self.window.borrow().clone() {
|
|
||||||
// Handle key presses
|
|
||||||
while let Ok(mut key_message) = KEY_MESSAGE_CHANNEL.try_receive() {
|
|
||||||
debug!("key_message = {key_message:?}");
|
|
||||||
|
|
||||||
if let Some(string) = key_message.string.as_ref()
|
|
||||||
&& (Keysym::a..=Keysym::z).contains(&key_message.keysym)
|
|
||||||
&& let &[code] = string.as_bytes()
|
|
||||||
{
|
|
||||||
const UNICODE_CTRL_A: char = '\u{1}';
|
|
||||||
|
|
||||||
let letter_index_from_keysym =
|
|
||||||
key_message.keysym.raw().wrapping_sub(Keysym::a.raw());
|
|
||||||
let letter_index_from_string =
|
|
||||||
(code as u32).wrapping_sub(UNICODE_CTRL_A as u32);
|
|
||||||
|
|
||||||
if letter_index_from_keysym == letter_index_from_string {
|
|
||||||
key_message.keysym =
|
|
||||||
Keysym::new(Keysym::a.raw() + letter_index_from_keysym);
|
|
||||||
// TODO: Avoid allocation
|
|
||||||
key_message.string =
|
|
||||||
Some(((b'a' + letter_index_from_keysym as u8) as char).to_string());
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Translating CTRL-{letter} to {letter}",
|
|
||||||
letter = (b'A' + letter_index_from_keysym as u8) as char
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// let text = key_message.string.map(SharedString::from).or_else(|| {
|
|
||||||
// Key::try_from_keysym(key_message.keysym).map(SharedString::from)
|
|
||||||
// });
|
|
||||||
let text = Key::try_from_keysym(key_message.keysym)
|
|
||||||
.map(SharedString::from)
|
|
||||||
.or_else(|| key_message.string.map(SharedString::from));
|
|
||||||
|
|
||||||
debug!("text = {text:?}");
|
|
||||||
|
|
||||||
if let Some(text) = text {
|
|
||||||
match key_message.direction {
|
|
||||||
xkb::KeyDirection::Down => window
|
|
||||||
.try_dispatch_event(WindowEvent::KeyPressed { text: text.clone() })
|
|
||||||
.unwrap(),
|
|
||||||
xkb::KeyDirection::Up => window
|
|
||||||
.try_dispatch_event(WindowEvent::KeyReleased { text })
|
|
||||||
.unwrap(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.draw_if_needed(|renderer| {
|
|
||||||
// TODO: Proper synchronization.
|
|
||||||
let framebuffer = unsafe { &mut *self.framebuffer.0 };
|
|
||||||
renderer.render(framebuffer, self.window_size[1] as usize);
|
|
||||||
info!("UI rendered.");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AcidEventLoopProxy {
|
|
||||||
pub quit_event_loop: Arc<AtomicBool>,
|
|
||||||
pub events: Arc<Mutex<RefCell<VecDeque<Box<dyn FnOnce() + Send>>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventLoopProxy for AcidEventLoopProxy {
|
|
||||||
fn quit_event_loop(&self) -> Result<(), EventLoopError> {
|
|
||||||
self.quit_event_loop.store(true, Ordering::SeqCst);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn invoke_from_event_loop(
|
|
||||||
&self,
|
|
||||||
event: Box<dyn FnOnce() + Send>,
|
|
||||||
) -> Result<(), EventLoopError> {
|
|
||||||
critical_section::with(|cs| {
|
|
||||||
self.events.borrow(cs).borrow_mut().push_back(event);
|
|
||||||
});
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
use alloc::rc::Rc;
|
|
||||||
use slint::{SharedString, VecModel};
|
|
||||||
use spectre_api_sys::SpectreUserKey;
|
|
||||||
|
|
||||||
use crate::ui::storage::SpectreSite;
|
|
||||||
|
|
||||||
pub enum CallbackMessage {
|
|
||||||
/// The escape key was pressed.
|
|
||||||
Escape,
|
|
||||||
Login(CallbackMessageLogin),
|
|
||||||
Users(CallbackMessageUsers),
|
|
||||||
UserEdit(CallbackMessageUserEdit),
|
|
||||||
UserSites(CallbackMessageUserSites),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum LoginResult {
|
|
||||||
Failure,
|
|
||||||
Success {
|
|
||||||
user_key: SpectreUserKey,
|
|
||||||
sites: Rc<VecModel<SpectreSite>>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum CallbackMessageLogin {
|
|
||||||
PwAccepted {
|
|
||||||
user_index: i32,
|
|
||||||
username: SharedString,
|
|
||||||
password: SharedString,
|
|
||||||
},
|
|
||||||
LoginResult {
|
|
||||||
username: SharedString,
|
|
||||||
result: LoginResult,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum CallbackMessageUsers {
|
|
||||||
EditUser { username: SharedString, new: bool },
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum CallbackMessageUserEdit {
|
|
||||||
ComputeIdenticon { password: SharedString },
|
|
||||||
ComputeKeyId { key: SharedString },
|
|
||||||
ConfirmRequest,
|
|
||||||
ConfirmProcessed,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum CallbackMessageUserSites {
|
|
||||||
SiteNameEdited { query: SharedString },
|
|
||||||
SiteNameAccepted { site_list_index: i32 },
|
|
||||||
}
|
|
||||||
|
|
@ -1,900 +0,0 @@
|
||||||
// #![cfg_attr(not(feature = "simulator"), no_main)]
|
|
||||||
|
|
||||||
use core::{cell::RefCell, ffi::CStr, pin::Pin};
|
|
||||||
|
|
||||||
use alloc::{
|
|
||||||
borrow::Cow,
|
|
||||||
boxed::Box,
|
|
||||||
ffi::CString,
|
|
||||||
format,
|
|
||||||
rc::Rc,
|
|
||||||
string::{String, ToString},
|
|
||||||
vec,
|
|
||||||
vec::Vec,
|
|
||||||
};
|
|
||||||
use embassy_time::Instant;
|
|
||||||
use hex::FromHexError;
|
|
||||||
use i_slint_core::model::{ModelChangeListener, ModelChangeListenerContainer};
|
|
||||||
use log::{error, info, warn};
|
|
||||||
use password_hash::Key;
|
|
||||||
use slint::{
|
|
||||||
Model, ModelExt, ModelNotify, ModelRc, ModelTracker, SharedString, StandardListViewItem,
|
|
||||||
VecModel,
|
|
||||||
};
|
|
||||||
use spectre_api_sys::{
|
|
||||||
SpectreAlgorithm, SpectreCounter, SpectreKeyID, SpectreKeyPurpose, SpectreResultType,
|
|
||||||
SpectreUserKey,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "limit-fps")]
|
|
||||||
use crate::FRAME_DURATION_MIN;
|
|
||||||
use crate::{
|
|
||||||
PSRAM_ALLOCATOR, SIGNAL_LCD_SUBMIT, SIGNAL_UI_RENDER,
|
|
||||||
db::{
|
|
||||||
AcidDatabase, DbKey, DbPathSpectreUserSite, DbPathSpectreUserSites, DbPathSpectreUsers,
|
|
||||||
PartitionAcid, ReadTransactionExt,
|
|
||||||
},
|
|
||||||
ffi::{alloc::__spre_free, crypto::ACTIVE_ENCRYPTED_USER_KEY},
|
|
||||||
proxy::OUTPUT_STRING_CHANNEL,
|
|
||||||
ui::{
|
|
||||||
backend::SlintBackend,
|
|
||||||
messages::{
|
|
||||||
CallbackMessage, CallbackMessageLogin, CallbackMessageUserEdit,
|
|
||||||
CallbackMessageUserSites, CallbackMessageUsers, LoginResult,
|
|
||||||
},
|
|
||||||
storage::{SpectreSite, SpectreSiteConfig, SpectreUserConfig, SpectreUsersConfig},
|
|
||||||
},
|
|
||||||
util::DurationExt,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub mod backend;
|
|
||||||
pub mod messages;
|
|
||||||
pub mod storage;
|
|
||||||
pub mod window_adapter;
|
|
||||||
|
|
||||||
slint::include_modules!();
|
|
||||||
|
|
||||||
fn spectre_derive_user_key(
|
|
||||||
username: &CStr,
|
|
||||||
password: &CStr,
|
|
||||||
encrypted_key: Option<Key>,
|
|
||||||
) -> SpectreUserKey {
|
|
||||||
if let Some(encrypted_key) = encrypted_key {
|
|
||||||
critical_section::with(|cs| {
|
|
||||||
ACTIVE_ENCRYPTED_USER_KEY.borrow(cs).set(encrypted_key);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let user_key_start = Instant::now();
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let user_key = &*spectre_api_sys::spectre_user_key(
|
|
||||||
username.as_ptr(),
|
|
||||||
password.as_ptr(),
|
|
||||||
SpectreAlgorithm::Current,
|
|
||||||
);
|
|
||||||
let user_key_duration = Instant::now().duration_since(user_key_start);
|
|
||||||
warn!(
|
|
||||||
"User key derived in {} seconds:\n{user_key:02x?}",
|
|
||||||
user_key_duration.display_as_secs()
|
|
||||||
);
|
|
||||||
let user_key_stack = *user_key;
|
|
||||||
|
|
||||||
// TODO: Erase memory before freeing
|
|
||||||
__spre_free(user_key as *const _ as *mut _);
|
|
||||||
|
|
||||||
user_key_stack
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spectre_derive_site_password(user_key: &SpectreUserKey, site_name: &CStr) -> String {
|
|
||||||
unsafe {
|
|
||||||
let site_password_c = &*spectre_api_sys::spectre_site_result(
|
|
||||||
user_key as *const SpectreUserKey,
|
|
||||||
site_name.as_ptr(),
|
|
||||||
SpectreResultType::SpectreResultDefaultResult,
|
|
||||||
core::ptr::null(),
|
|
||||||
SpectreCounter::Initial,
|
|
||||||
SpectreKeyPurpose::Authentication,
|
|
||||||
core::ptr::null(),
|
|
||||||
);
|
|
||||||
let site_password = CStr::from_ptr(site_password_c)
|
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
.to_string();
|
|
||||||
__spre_free(site_password_c as *const _ as *mut _);
|
|
||||||
site_password
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[embassy_executor::task]
|
|
||||||
pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: PartitionAcid) {
|
|
||||||
let db = AcidDatabase::mount(flash_part_acid).await;
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// * Store a config as a versioned postcard-serialized struct
|
|
||||||
// * Store accounts and sites as ranges in the DB
|
|
||||||
|
|
||||||
i_slint_core::properties::ALLOCATOR
|
|
||||||
.set(&PSRAM_ALLOCATOR)
|
|
||||||
.ok()
|
|
||||||
.unwrap();
|
|
||||||
slint::platform::set_platform(Box::new(backend)).expect("backend already initialized");
|
|
||||||
|
|
||||||
let main = AppWindow::new().unwrap();
|
|
||||||
let state = State::new(db, main).await;
|
|
||||||
let window = state.borrow().window.clone_strong();
|
|
||||||
|
|
||||||
State::run_event_loop(window).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct State {
|
|
||||||
window: AppWindow,
|
|
||||||
db: Rc<AcidDatabase>,
|
|
||||||
users: SpectreUsersConfig,
|
|
||||||
/// Currently active view.
|
|
||||||
view: AppState,
|
|
||||||
// Retained state for each view.
|
|
||||||
state_login: StateLogin,
|
|
||||||
state_users: StateUsers,
|
|
||||||
state_user_edit: StateUserEdit,
|
|
||||||
state_user_sites: Option<StateUserSites>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl State {
|
|
||||||
async fn new(db: AcidDatabase, main: AppWindow) -> Rc<RefCell<Self>> {
|
|
||||||
let users = {
|
|
||||||
let users = Self::load_users(&db).await;
|
|
||||||
warn!("Users: {users:#?}");
|
|
||||||
users
|
|
||||||
};
|
|
||||||
let usernames = users.users.clone().map(|user| user.username);
|
|
||||||
|
|
||||||
let state = Rc::new(RefCell::new(State {
|
|
||||||
window: main.clone_strong(),
|
|
||||||
users,
|
|
||||||
db: Rc::new(db),
|
|
||||||
view: AppState::Login,
|
|
||||||
state_login: Default::default(),
|
|
||||||
state_users: Default::default(),
|
|
||||||
state_user_edit: Default::default(),
|
|
||||||
state_user_sites: Default::default(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
main.on_enter_view({
|
|
||||||
let state = state.clone();
|
|
||||||
move |view| {
|
|
||||||
state.borrow_mut().set_view(view, true, true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
main.on_escape({
|
|
||||||
let state = state.clone();
|
|
||||||
move || {
|
|
||||||
State::process_callback_message(&state, CallbackMessage::Escape);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
main.set_login_usernames(ModelRc::new(usernames));
|
|
||||||
|
|
||||||
main.on_login_pw_accepted({
|
|
||||||
let state = state.clone();
|
|
||||||
move |user_index, username, password| {
|
|
||||||
State::process_callback_message(
|
|
||||||
&state,
|
|
||||||
CallbackMessage::Login(CallbackMessageLogin::PwAccepted {
|
|
||||||
user_index,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
main.on_login_test_string_accepted(|string| {
|
|
||||||
slint::spawn_local(async move {
|
|
||||||
OUTPUT_STRING_CHANNEL.send(string.to_string()).await;
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
main.on_users_edit_user({
|
|
||||||
let state = state.clone();
|
|
||||||
move |username, new| {
|
|
||||||
State::process_callback_message(
|
|
||||||
&state,
|
|
||||||
CallbackMessage::Users(CallbackMessageUsers::EditUser { username, new }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
main.on_user_edit_compute_identicon({
|
|
||||||
let state = state.clone();
|
|
||||||
move |password| {
|
|
||||||
State::process_callback_message(
|
|
||||||
&state,
|
|
||||||
CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeIdenticon {
|
|
||||||
password,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
main.on_user_edit_compute_key_id({
|
|
||||||
let state = state.clone();
|
|
||||||
move |key| {
|
|
||||||
State::process_callback_message(
|
|
||||||
&state,
|
|
||||||
CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeKeyId { key }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
main.on_user_edit_confirm({
|
|
||||||
let state = state.clone();
|
|
||||||
move |_encrypted_key| {
|
|
||||||
State::process_callback_message(
|
|
||||||
&state,
|
|
||||||
CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmRequest),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
main.on_user_sites_site_name_edited({
|
|
||||||
let state = state.clone();
|
|
||||||
move |query| {
|
|
||||||
State::process_callback_message(
|
|
||||||
&state,
|
|
||||||
CallbackMessage::UserSites(CallbackMessageUserSites::SiteNameEdited { query }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
main.on_user_sites_site_name_accepted({
|
|
||||||
let state = state.clone();
|
|
||||||
move |site_list_index| {
|
|
||||||
State::process_callback_message(
|
|
||||||
&state,
|
|
||||||
CallbackMessage::UserSites(CallbackMessageUserSites::SiteNameAccepted {
|
|
||||||
site_list_index,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// let sites = Rc::new(VecModel::default());
|
|
||||||
// sites.push("First".into());
|
|
||||||
// sites.push("Second".into());
|
|
||||||
// main.set_sites(ModelRc::new(ModelRc::new(sites.clone()).map(
|
|
||||||
// |mut site: StandardListViewItem| {
|
|
||||||
// site.text += "10";
|
|
||||||
// site
|
|
||||||
// },
|
|
||||||
// )));
|
|
||||||
|
|
||||||
state
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn load_users(db: &AcidDatabase) -> SpectreUsersConfig {
|
|
||||||
let read = db.read_transaction().await;
|
|
||||||
let mut buffer = vec![0_u8; 128];
|
|
||||||
match read
|
|
||||||
.read_to_vec(&DbKey::new(DbPathSpectreUsers), &mut buffer)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(bytes) => postcard::from_bytes::<SpectreUsersConfig>(bytes).unwrap(),
|
|
||||||
Err(ekv::ReadError::KeyNotFound) => Default::default(),
|
|
||||||
Err(error) => panic!("Failed to read the users config: {error:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn save_users(db: &AcidDatabase, users: &SpectreUsersConfig) {
|
|
||||||
let mut write = db.write_transaction().await;
|
|
||||||
let buffer = postcard::to_allocvec(&users).unwrap();
|
|
||||||
write
|
|
||||||
.write(&DbKey::new(DbPathSpectreUsers), &buffer)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
write.commit().await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_site_list(
|
|
||||||
sites: &Rc<VecModel<SpectreSite>>,
|
|
||||||
query: &SharedString,
|
|
||||||
) -> Rc<VecModel<SiteListEntry>> {
|
|
||||||
let site_list = Rc::new(VecModel::default());
|
|
||||||
|
|
||||||
for site in sites.iter() {
|
|
||||||
site_list.push(SiteListEntry::Existing(site.site_name));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !query.is_empty() {
|
|
||||||
site_list.push(SiteListEntry::New(query.clone()))
|
|
||||||
}
|
|
||||||
|
|
||||||
site_list
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_site_list(&mut self) {
|
|
||||||
if let Some(StateUserSites {
|
|
||||||
sites,
|
|
||||||
query,
|
|
||||||
site_list,
|
|
||||||
..
|
|
||||||
}) = self.state_user_sites.as_mut()
|
|
||||||
{
|
|
||||||
*site_list = State::make_site_list(sites, query);
|
|
||||||
self.window
|
|
||||||
.set_user_sites_sites(ModelRc::from(Rc::new(site_list.clone().map(|site| {
|
|
||||||
let mut item = StandardListViewItem::default();
|
|
||||||
item.text = site.to_string();
|
|
||||||
item
|
|
||||||
}))));
|
|
||||||
} else {
|
|
||||||
self.window.invoke_user_sites_site_name_clear();
|
|
||||||
self.window.set_user_sites_sites(Default::default());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_callback_message(state_rc: &Rc<RefCell<State>>, message: CallbackMessage) {
|
|
||||||
let view = state_rc.borrow().view;
|
|
||||||
match view {
|
|
||||||
AppState::Login => StateLogin::process_callback_message(state_rc, message),
|
|
||||||
AppState::Users => StateUsers::process_callback_message(state_rc, message),
|
|
||||||
AppState::UserEdit => StateUserEdit::process_callback_message(state_rc, message),
|
|
||||||
AppState::UserSites => StateUserSites::process_callback_message(state_rc, message),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset_view(&mut self) {
|
|
||||||
match self.view {
|
|
||||||
AppState::Login => self.state_login = Default::default(),
|
|
||||||
AppState::Users => self.state_users = Default::default(),
|
|
||||||
AppState::UserEdit => self.state_user_edit = Default::default(),
|
|
||||||
AppState::UserSites => self.state_user_sites = Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_view(&mut self, view: AppState, reset_source: bool, reset_target: bool) {
|
|
||||||
if reset_source {
|
|
||||||
self.reset_view();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.view = view;
|
|
||||||
self.window.set_app_state(view);
|
|
||||||
|
|
||||||
if reset_target {
|
|
||||||
self.reset_view();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Instead of having a `loop` in the non-async `SlintBackend::run_event_loop`, we achieve
|
|
||||||
/// async by having only one iteration of the loop run, and `await`ing here.
|
|
||||||
/// The following block is analogous to `main.run()`.
|
|
||||||
async fn run_event_loop(window: AppWindow) -> ! {
|
|
||||||
window.show().unwrap();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
slint::run_event_loop().unwrap();
|
|
||||||
SIGNAL_LCD_SUBMIT.signal(());
|
|
||||||
#[cfg(feature = "limit-fps")]
|
|
||||||
embassy_time::Timer::after(FRAME_DURATION_MIN).await;
|
|
||||||
SIGNAL_UI_RENDER.wait().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[expect(unreachable_code)]
|
|
||||||
window.hide().unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trait AppViewTrait {
|
|
||||||
fn process_callback_message(_state_rc: &Rc<RefCell<State>>, _message: CallbackMessage) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct StateLogin {}
|
|
||||||
|
|
||||||
impl AppViewTrait for StateLogin {
|
|
||||||
fn process_callback_message(state_rc: &Rc<RefCell<State>>, message: CallbackMessage) {
|
|
||||||
let mut state = state_rc.borrow_mut();
|
|
||||||
match message {
|
|
||||||
CallbackMessage::Login(CallbackMessageLogin::PwAccepted {
|
|
||||||
user_index,
|
|
||||||
username: _,
|
|
||||||
password,
|
|
||||||
}) => {
|
|
||||||
let Some(user) = state.users.users.row_data(user_index as usize) else {
|
|
||||||
error!("Failed to find a user with index {user_index}.");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let username_c = CString::new(&*user.username)
|
|
||||||
.expect("Username cannot be converted to a C string.");
|
|
||||||
let password_c =
|
|
||||||
CString::new(&*password).expect("Password cannot be converted to a C string.");
|
|
||||||
let user_key =
|
|
||||||
spectre_derive_user_key(&username_c, &password_c, Some(user.encrypted_key));
|
|
||||||
|
|
||||||
// {
|
|
||||||
// let mut write = db.write_transaction().await;
|
|
||||||
// write
|
|
||||||
// .write(
|
|
||||||
// &DbKey::new(DbPathSpectreUserSite {
|
|
||||||
// user_sites: DbPathSpectreUserSites {
|
|
||||||
// username: "test".into(),
|
|
||||||
// },
|
|
||||||
// site: "example.org".into(),
|
|
||||||
// }),
|
|
||||||
// &postcard::to_allocvec(&SpectreSiteConfig::default()).unwrap(),
|
|
||||||
// )
|
|
||||||
// .await
|
|
||||||
// .unwrap();
|
|
||||||
// write
|
|
||||||
// .write(
|
|
||||||
// &DbKey::new(DbPathSpectreUserSite {
|
|
||||||
// user_sites: DbPathSpectreUserSites {
|
|
||||||
// username: "test".into(),
|
|
||||||
// },
|
|
||||||
// site: "sub.example.org".into(),
|
|
||||||
// }),
|
|
||||||
// &postcard::to_allocvec(&SpectreSiteConfig::default()).unwrap(),
|
|
||||||
// )
|
|
||||||
// .await
|
|
||||||
// .unwrap();
|
|
||||||
// write
|
|
||||||
// .write(
|
|
||||||
// &DbKey::new(DbPathSpectreUsers),
|
|
||||||
// &postcard::to_allocvec(&value).unwrap(),
|
|
||||||
// )
|
|
||||||
// .await
|
|
||||||
// .unwrap();
|
|
||||||
// write.commit().await.unwrap();
|
|
||||||
// }
|
|
||||||
|
|
||||||
if user.key_id != user_key.keyID.bytes {
|
|
||||||
State::process_callback_message(
|
|
||||||
state_rc,
|
|
||||||
CallbackMessage::Login(CallbackMessageLogin::LoginResult {
|
|
||||||
username: user.username,
|
|
||||||
result: LoginResult::Failure,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
slint::spawn_local({
|
|
||||||
let state_rc = state_rc.clone();
|
|
||||||
let username = user.username.clone();
|
|
||||||
let db = state.db.clone();
|
|
||||||
|
|
||||||
async move {
|
|
||||||
let read = db.read_transaction().await;
|
|
||||||
// TODO: https://github.com/embassy-rs/ekv/issues/20
|
|
||||||
let mut buffer = vec![0; 256];
|
|
||||||
// let slice = read
|
|
||||||
// .read_to_vec(&DbKey::new(DbPathSpectreUsers), &mut buffer)
|
|
||||||
// .await
|
|
||||||
// .unwrap();
|
|
||||||
// let read_value = postcard::from_bytes::<SpectreUsersConfig>(&slice).unwrap();
|
|
||||||
|
|
||||||
let key_sites = DbKey::new(DbPathSpectreUserSites {
|
|
||||||
username: Cow::Borrowed(&username),
|
|
||||||
});
|
|
||||||
let mut key_buffer = [0_u8; ekv::config::MAX_KEY_SIZE];
|
|
||||||
let mut cursor = read
|
|
||||||
.read_range(key_sites.range_of_children())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let sites = VecModel::default();
|
|
||||||
|
|
||||||
while let Some((key_len, value_len)) =
|
|
||||||
cursor.next(&mut key_buffer, &mut buffer).await.unwrap()
|
|
||||||
{
|
|
||||||
let key = DbKey::from_raw(key_buffer[..key_len].into());
|
|
||||||
let mut key_segments = key.segments();
|
|
||||||
let value = &buffer[..value_len];
|
|
||||||
let site_config =
|
|
||||||
postcard::from_bytes::<SpectreSiteConfig>(value).unwrap();
|
|
||||||
let site = SpectreSite {
|
|
||||||
config: site_config,
|
|
||||||
username: key_segments.nth(2).unwrap().as_ref().into(),
|
|
||||||
site_name: key_segments.nth(1).unwrap().as_ref().into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
info!("site = {:#?}", site);
|
|
||||||
sites.push(site);
|
|
||||||
}
|
|
||||||
|
|
||||||
State::process_callback_message(
|
|
||||||
&state_rc,
|
|
||||||
CallbackMessage::Login(CallbackMessageLogin::LoginResult {
|
|
||||||
username: user.username,
|
|
||||||
result: LoginResult::Success {
|
|
||||||
user_key,
|
|
||||||
sites: Rc::new(sites),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
CallbackMessage::Login(CallbackMessageLogin::LoginResult {
|
|
||||||
username,
|
|
||||||
result: LoginResult::Success { user_key, sites },
|
|
||||||
}) => {
|
|
||||||
info!("Correct password entered for user {:?}.", username);
|
|
||||||
state.state_user_sites = Some(StateUserSites {
|
|
||||||
username,
|
|
||||||
user_key,
|
|
||||||
query: SharedString::new(),
|
|
||||||
sites: sites.clone(),
|
|
||||||
site_list: Default::default(),
|
|
||||||
});
|
|
||||||
state.update_site_list();
|
|
||||||
state.set_view(AppState::UserSites, true, false);
|
|
||||||
}
|
|
||||||
CallbackMessage::Login(CallbackMessageLogin::LoginResult {
|
|
||||||
username,
|
|
||||||
result: LoginResult::Failure,
|
|
||||||
}) => {
|
|
||||||
warn!("Incorrect password entered for user {:?}.", username);
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct StateUsers {}
|
|
||||||
|
|
||||||
impl AppViewTrait for StateUsers {
|
|
||||||
fn process_callback_message(state_rc: &Rc<RefCell<State>>, message: CallbackMessage) {
|
|
||||||
let mut state = state_rc.borrow_mut();
|
|
||||||
match message {
|
|
||||||
CallbackMessage::Escape => {
|
|
||||||
state.set_view(AppState::Login, true, false);
|
|
||||||
}
|
|
||||||
CallbackMessage::Users(CallbackMessageUsers::EditUser { username, new }) => {
|
|
||||||
state.state_user_edit = StateUserEdit {
|
|
||||||
username: username.clone(),
|
|
||||||
new,
|
|
||||||
password: None,
|
|
||||||
encrypted_key: None,
|
|
||||||
};
|
|
||||||
state.window.set_user_edit_username(username);
|
|
||||||
state.set_view(AppState::UserEdit, true, false);
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct StateUserEdit {
|
|
||||||
username: SharedString,
|
|
||||||
new: bool,
|
|
||||||
password: Option<SharedString>,
|
|
||||||
encrypted_key: Option<(Key, SpectreUserKey)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppViewTrait for StateUserEdit {
|
|
||||||
fn process_callback_message(state_rc: &Rc<RefCell<State>>, message: CallbackMessage) {
|
|
||||||
let state = state_rc.clone();
|
|
||||||
let mut state = state.borrow_mut();
|
|
||||||
match message {
|
|
||||||
CallbackMessage::Escape => {
|
|
||||||
state.set_view(AppState::Users, true, false);
|
|
||||||
}
|
|
||||||
CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeIdenticon { password }) => {
|
|
||||||
let username_c = CString::new(&*state.state_user_edit.username)
|
|
||||||
.expect("Username cannot be converted to a C string.");
|
|
||||||
let password_c =
|
|
||||||
CString::new(&*password).expect("Password cannot be converted to a C string.");
|
|
||||||
// let user_key = spectre_derive_user_key(&username_c, &password_c);
|
|
||||||
let identicon: SharedString = unsafe {
|
|
||||||
let identicon = spectre_api_sys::spectre_identicon(
|
|
||||||
username_c.as_ptr(),
|
|
||||||
password_c.as_ptr(),
|
|
||||||
);
|
|
||||||
// TODO: identicon.color
|
|
||||||
format!(
|
|
||||||
"{}{}{}{}",
|
|
||||||
CStr::from_ptr(identicon.leftArm).to_str().unwrap(),
|
|
||||||
CStr::from_ptr(identicon.body).to_str().unwrap(),
|
|
||||||
CStr::from_ptr(identicon.rightArm).to_str().unwrap(),
|
|
||||||
CStr::from_ptr(identicon.accessory).to_str().unwrap()
|
|
||||||
)
|
|
||||||
.into()
|
|
||||||
};
|
|
||||||
|
|
||||||
warn!("Identicon: {identicon} ({identicon:?})");
|
|
||||||
state.window.set_user_edit_identicon(identicon.clone());
|
|
||||||
state.state_user_edit.password = Some(password);
|
|
||||||
}
|
|
||||||
CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeKeyId {
|
|
||||||
key: key_string,
|
|
||||||
}) => {
|
|
||||||
let Some(password) = state.state_user_edit.password.as_ref() else {
|
|
||||||
warn!("Attempted to compute a key ID when no password has been entered.");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut key: Key = [0; _];
|
|
||||||
if let Err(key_decode_error) = hex::decode_to_slice(&*key_string, &mut key) {
|
|
||||||
let message = match key_decode_error {
|
|
||||||
FromHexError::InvalidStringLength | FromHexError::OddLength => {
|
|
||||||
let required_size = key.len() * 2;
|
|
||||||
let provided_size = key_string.len();
|
|
||||||
let delta = provided_size as i32 - required_size as i32;
|
|
||||||
|
|
||||||
if delta < 0 {
|
|
||||||
slint::format!("Missing {} characters.", -delta)
|
|
||||||
} else if delta > 0 {
|
|
||||||
slint::format!("{} too many characters.", delta)
|
|
||||||
} else {
|
|
||||||
slint::format!("Invalid key length.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FromHexError::InvalidHexCharacter { c, index } => {
|
|
||||||
slint::format!("Invalid character {c:?} at position {index}.")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
state.window.set_user_edit_key_error(message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let username_c = CString::new(&*state.state_user_edit.username)
|
|
||||||
.expect("Username cannot be converted to a C string.");
|
|
||||||
let password_c =
|
|
||||||
CString::new(&**password).expect("Password cannot be converted to a C string.");
|
|
||||||
let user_key = spectre_derive_user_key(&username_c, &password_c, Some(key));
|
|
||||||
|
|
||||||
state.window.set_user_edit_key_error(SharedString::new());
|
|
||||||
state.window.set_user_edit_key_id(
|
|
||||||
CStr::from_bytes_with_nul(&user_key.keyID.hex)
|
|
||||||
.unwrap()
|
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
state.state_user_edit.encrypted_key = Some((key, user_key));
|
|
||||||
}
|
|
||||||
CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmRequest) => {
|
|
||||||
let Some((
|
|
||||||
encrypted_key,
|
|
||||||
SpectreUserKey {
|
|
||||||
keyID: SpectreKeyID { bytes: key_id, .. },
|
|
||||||
..
|
|
||||||
},
|
|
||||||
)) = state.state_user_edit.encrypted_key.take()
|
|
||||||
else {
|
|
||||||
warn!("Encrypted key is not set.");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// If a user with that username already exists, overwrite it.
|
|
||||||
let user = SpectreUserConfig {
|
|
||||||
username: state.state_user_edit.username.clone(),
|
|
||||||
encrypted_key,
|
|
||||||
key_id,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut existing_index = None;
|
|
||||||
|
|
||||||
for index in 0..state.users.users.row_count() {
|
|
||||||
if let Some(current_user) = state.users.users.row_data(index)
|
|
||||||
&& current_user.username == user.username
|
|
||||||
{
|
|
||||||
existing_index = Some(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(existing_index) = existing_index {
|
|
||||||
state.users.users.set_row_data(existing_index, user);
|
|
||||||
} else {
|
|
||||||
state.users.users.push(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
slint::spawn_local({
|
|
||||||
let state_rc = state_rc.clone();
|
|
||||||
let db = state.db.clone();
|
|
||||||
let users = state.users.clone();
|
|
||||||
|
|
||||||
async move {
|
|
||||||
State::save_users(&db, &users).await;
|
|
||||||
State::process_callback_message(
|
|
||||||
&state_rc,
|
|
||||||
CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmProcessed),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmProcessed) => {
|
|
||||||
state.state_user_edit = Default::default();
|
|
||||||
state.set_view(AppState::Users, true, true);
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct StateUserSites {
|
|
||||||
username: SharedString,
|
|
||||||
user_key: SpectreUserKey,
|
|
||||||
query: SharedString,
|
|
||||||
sites: Rc<VecModel<SpectreSite>>,
|
|
||||||
site_list: Rc<VecModel<SiteListEntry>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppViewTrait for StateUserSites {
|
|
||||||
fn process_callback_message(state_rc: &Rc<RefCell<State>>, message: CallbackMessage) {
|
|
||||||
let state = state_rc.clone();
|
|
||||||
let mut state = state.borrow_mut();
|
|
||||||
match message {
|
|
||||||
CallbackMessage::Escape => {
|
|
||||||
state.set_view(AppState::Login, true, false);
|
|
||||||
}
|
|
||||||
CallbackMessage::UserSites(CallbackMessageUserSites::SiteNameEdited { query }) => {
|
|
||||||
if let Some(user_sites) = state.state_user_sites.as_mut() {
|
|
||||||
user_sites.query = query;
|
|
||||||
state.update_site_list();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CallbackMessage::UserSites(CallbackMessageUserSites::SiteNameAccepted {
|
|
||||||
site_list_index,
|
|
||||||
}) => {
|
|
||||||
let Some(user_sites) = state.state_user_sites.as_mut() else {
|
|
||||||
error!("User sites uninitialized.");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(site_list_entry) = user_sites.site_list.row_data(site_list_index as usize)
|
|
||||||
else {
|
|
||||||
error!("Invalid site list entry index: {site_list_index}");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
warn!("Site name accepted: {site_list_entry:?}");
|
|
||||||
let site_name = match site_list_entry {
|
|
||||||
SiteListEntry::New(site_name) => site_name,
|
|
||||||
SiteListEntry::Existing(site_name) => site_name,
|
|
||||||
};
|
|
||||||
let site_name_c = CString::new(&*site_name).unwrap();
|
|
||||||
let site_password =
|
|
||||||
spectre_derive_site_password(&user_sites.user_key, &site_name_c);
|
|
||||||
|
|
||||||
warn!("Site password: {site_password:?}");
|
|
||||||
|
|
||||||
slint::spawn_local({
|
|
||||||
let username = user_sites.username.clone();
|
|
||||||
let db = state.db.clone();
|
|
||||||
async move {
|
|
||||||
// Send password to the host.
|
|
||||||
OUTPUT_STRING_CHANNEL.send(site_password).await;
|
|
||||||
|
|
||||||
// Update the stored site.
|
|
||||||
let mut write = db.write_transaction().await;
|
|
||||||
let key = DbKey::new(DbPathSpectreUserSite {
|
|
||||||
user_sites: DbPathSpectreUserSites {
|
|
||||||
username: Cow::Borrowed(&username),
|
|
||||||
},
|
|
||||||
site: Cow::Borrowed(&site_name),
|
|
||||||
});
|
|
||||||
let site = SpectreSiteConfig::default();
|
|
||||||
let site_bytes =
|
|
||||||
postcard::to_extend(&site, Vec::<u8, _>::new_in(&PSRAM_ALLOCATOR))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
write.write(&key, &site_bytes).await.unwrap();
|
|
||||||
write.commit().await.unwrap();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SitesModel<M>(Pin<Box<ModelChangeListenerContainer<SitesModelInner<M>>>>)
|
|
||||||
where
|
|
||||||
M: Model + 'static;
|
|
||||||
|
|
||||||
struct SitesModelInner<M>
|
|
||||||
where
|
|
||||||
M: Model + 'static,
|
|
||||||
{
|
|
||||||
wrapped_model: M,
|
|
||||||
notify: ModelNotify,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M> ModelChangeListener for SitesModelInner<M>
|
|
||||||
where
|
|
||||||
M: Model + 'static,
|
|
||||||
{
|
|
||||||
fn row_changed(self: Pin<&Self>, row: usize) {
|
|
||||||
self.notify
|
|
||||||
.row_changed(self.wrapped_model.row_count() - 1 - row);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn row_added(self: Pin<&Self>, index: usize, count: usize) {
|
|
||||||
let row_count = self.wrapped_model.row_count();
|
|
||||||
let old_row_count = row_count - count;
|
|
||||||
let index = old_row_count - index;
|
|
||||||
self.notify.row_added(index, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn row_removed(self: Pin<&Self>, index: usize, count: usize) {
|
|
||||||
let row_count = self.wrapped_model.row_count();
|
|
||||||
self.notify.row_removed(row_count - index, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset(self: Pin<&Self>) {
|
|
||||||
self.notify.reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M> SitesModel<M>
|
|
||||||
where
|
|
||||||
M: Model + 'static,
|
|
||||||
{
|
|
||||||
pub fn new(wrapped_model: M) -> Self {
|
|
||||||
let inner = SitesModelInner {
|
|
||||||
wrapped_model,
|
|
||||||
notify: Default::default(),
|
|
||||||
};
|
|
||||||
let container = Box::pin(ModelChangeListenerContainer::new(inner));
|
|
||||||
container
|
|
||||||
.wrapped_model
|
|
||||||
.model_tracker()
|
|
||||||
.attach_peer(container.as_ref().model_peer());
|
|
||||||
Self(container)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a reference to the inner model
|
|
||||||
pub fn source_model(&self) -> &M {
|
|
||||||
&self.0.as_ref().get().get_ref().wrapped_model
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M> Model for SitesModel<M>
|
|
||||||
where
|
|
||||||
M: Model + 'static,
|
|
||||||
{
|
|
||||||
type Data = M::Data;
|
|
||||||
|
|
||||||
fn row_count(&self) -> usize {
|
|
||||||
self.0.wrapped_model.row_count()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn row_data(&self, row: usize) -> Option<Self::Data> {
|
|
||||||
let count = self.0.wrapped_model.row_count();
|
|
||||||
self.0.wrapped_model.row_data(count.checked_sub(row + 1)?)
|
|
||||||
}
|
|
||||||
fn set_row_data(&self, row: usize, data: Self::Data) {
|
|
||||||
let count = self.0.as_ref().wrapped_model.row_count();
|
|
||||||
self.0.wrapped_model.set_row_data(count - row - 1, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn model_tracker(&self) -> &dyn ModelTracker {
|
|
||||||
&self.0.notify
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn core::any::Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum SiteListEntry {
|
|
||||||
New(SharedString),
|
|
||||||
Existing(SharedString),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SiteListEntry {
|
|
||||||
pub fn to_string(&self) -> SharedString {
|
|
||||||
match self {
|
|
||||||
SiteListEntry::New(site) => slint::format!("{site} <Add new site>"),
|
|
||||||
SiteListEntry::Existing(site) => site.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,160 +0,0 @@
|
||||||
use core::fmt::{Debug, Formatter};
|
|
||||||
|
|
||||||
use alloc::rc::Rc;
|
|
||||||
use chrono::NaiveDateTime;
|
|
||||||
use password_hash::Key;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use slint::{Model, SharedString, VecModel};
|
|
||||||
use spectre_api_sys::{SpectreAlgorithm, SpectreCounter, SpectreResultType};
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Default)]
|
|
||||||
pub struct SpectreUsersConfig {
|
|
||||||
#[serde(with = "vec_model")]
|
|
||||||
pub users: Rc<VecModel<SpectreUserConfig>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for SpectreUsersConfig {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
|
|
||||||
f.debug_struct("SpectreUsersConfig")
|
|
||||||
.field_with("users", |f| {
|
|
||||||
f.debug_list().entries(self.users.iter()).finish()
|
|
||||||
})
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Clone for SpectreUsersConfig {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self {
|
|
||||||
users: Rc::new(self.users.iter().collect()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct SpectreUserConfig {
|
|
||||||
pub username: SharedString,
|
|
||||||
#[serde(with = "serde_bytes")]
|
|
||||||
pub encrypted_key: Key,
|
|
||||||
#[serde(with = "serde_bytes")]
|
|
||||||
pub key_id: [u8; 32],
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct SpectreSiteConfig {
|
|
||||||
#[serde(with = "with_repr")]
|
|
||||||
pub algorithm: SpectreAlgorithm,
|
|
||||||
pub counter: SpectreCounter::Type,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
#[serde(with = "with_repr")]
|
|
||||||
pub result_type: SpectreResultType,
|
|
||||||
pub password: Option<SharedString>,
|
|
||||||
#[serde(with = "with_repr")]
|
|
||||||
pub login_type: SpectreResultType,
|
|
||||||
pub login_name: Option<SharedString>,
|
|
||||||
pub uses: u32,
|
|
||||||
pub last_used: NaiveDateTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SpectreSiteConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
algorithm: SpectreAlgorithm::Current,
|
|
||||||
counter: SpectreCounter::Default,
|
|
||||||
result_type: SpectreResultType::SpectreResultDefaultResult,
|
|
||||||
password: None,
|
|
||||||
login_type: SpectreResultType::SpectreResultDefaultLogin,
|
|
||||||
login_name: None,
|
|
||||||
uses: 0,
|
|
||||||
last_used: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct SpectreSite {
|
|
||||||
pub username: SharedString,
|
|
||||||
pub site_name: SharedString,
|
|
||||||
pub config: SpectreSiteConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
mod vec_model {
|
|
||||||
use alloc::{rc::Rc, vec::Vec};
|
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
|
||||||
use slint::{Model, VecModel};
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
pub fn serialize<T, S>(value: &Rc<VecModel<T>>, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
T: Serialize,
|
|
||||||
VecModel<T>: Model<Data = T>,
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
let vec: Vec<T> = value.iter().collect();
|
|
||||||
vec.serialize(serializer)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Rc<VecModel<T>>, D::Error>
|
|
||||||
where
|
|
||||||
T: Deserialize<'de>,
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let vec = Vec::<T>::deserialize(deserializer)?;
|
|
||||||
Ok(Rc::new(VecModel::from(vec)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod with_repr {
|
|
||||||
use serde::{Deserialize, Deserializer, Serializer};
|
|
||||||
use spectre_api_sys::{SpectreAlgorithm, SpectreResultType};
|
|
||||||
|
|
||||||
pub trait ReprConvert: Copy {
|
|
||||||
type Repr: Copy;
|
|
||||||
|
|
||||||
fn into_repr(self) -> Self::Repr;
|
|
||||||
fn from_repr(repr: Self::Repr) -> Self;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ReprConvert for SpectreAlgorithm {
|
|
||||||
type Repr = u32;
|
|
||||||
|
|
||||||
fn from_repr(repr: Self::Repr) -> Self {
|
|
||||||
unsafe { core::mem::transmute::<Self::Repr, Self>(repr) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_repr(self) -> Self::Repr {
|
|
||||||
self as Self::Repr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ReprConvert for SpectreResultType {
|
|
||||||
type Repr = u32;
|
|
||||||
|
|
||||||
fn from_repr(repr: Self::Repr) -> Self {
|
|
||||||
unsafe { core::mem::transmute::<Self::Repr, Self>(repr) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_repr(self) -> Self::Repr {
|
|
||||||
self as Self::Repr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
T: ReprConvert<Repr = u32>,
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
serializer.serialize_u32(value.into_repr())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
|
||||||
where
|
|
||||||
T: ReprConvert<Repr = u32>,
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
<u32 as Deserialize>::deserialize(deserializer).map(T::from_repr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
use core::fmt::Display;
|
|
||||||
|
|
||||||
use embassy_time::Duration;
|
|
||||||
|
|
||||||
pub struct FormattedDuration<'a>(&'a Duration);
|
|
||||||
|
|
||||||
impl Display for FormattedDuration<'_> {
|
|
||||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
||||||
let &FormattedDuration(duration) = self;
|
|
||||||
f.write_fmt(format_args!(
|
|
||||||
"{:01}.{:06}",
|
|
||||||
duration.as_secs(),
|
|
||||||
duration.as_micros() % 1_000_000
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait DurationExt {
|
|
||||||
fn display_as_secs(&self) -> FormattedDuration<'_>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DurationExt for Duration {
|
|
||||||
fn display_as_secs(&self) -> FormattedDuration<'_> {
|
|
||||||
FormattedDuration(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn get_file_name(path: &str) -> &str {
|
|
||||||
let path = path.as_bytes();
|
|
||||||
let mut start_index = path.len() as isize;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
start_index -= 1;
|
|
||||||
|
|
||||||
if start_index < 0
|
|
||||||
|| path[start_index as usize] == b'/'
|
|
||||||
|| path[start_index as usize] == b'\\'
|
|
||||||
{
|
|
||||||
start_index += 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match str::from_utf8(path.split_at(start_index as usize).1) {
|
|
||||||
Ok(substring) => substring,
|
|
||||||
Err(_) => panic!("Failed to extract the file name from a path."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export global Style {
|
|
||||||
in property <length> spacing: 8px;
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
|
||||||
|
Before Width: | Height: | Size: 365 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-key"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 352 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-log-in"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path><polyline points="10 17 15 12 10 7"></polyline><line x1="15" y1="12" x2="3" y2="12"></line></svg>
|
|
||||||
|
Before Width: | Height: | Size: 368 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-log-out"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>
|
|
||||||
|
Before Width: | Height: | Size: 367 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sliders"><line x1="4" y1="21" x2="4" y2="14"></line><line x1="4" y1="10" x2="4" y2="3"></line><line x1="12" y1="21" x2="12" y2="12"></line><line x1="12" y1="8" x2="12" y2="3"></line><line x1="20" y1="21" x2="20" y2="16"></line><line x1="20" y1="12" x2="20" y2="3"></line><line x1="1" y1="14" x2="7" y2="14"></line><line x1="9" y1="8" x2="15" y2="8"></line><line x1="17" y1="16" x2="23" y2="16"></line></svg>
|
|
||||||
|
Before Width: | Height: | Size: 611 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-trash-2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
|
|
||||||
|
Before Width: | Height: | Size: 448 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 400 B |
|
|
@ -1,48 +0,0 @@
|
||||||
import { ComboBox, LineEdit } from "std-widgets.slint";
|
|
||||||
import { Style } from "globals.slint";
|
|
||||||
import { IconButton } from "widgets/icon-button.slint";
|
|
||||||
|
|
||||||
export component LoginView inherits VerticalLayout {
|
|
||||||
padding: Style.spacing;
|
|
||||||
spacing: Style.spacing;
|
|
||||||
in property <[string]> usernames <=> combo_box_username.model;
|
|
||||||
callback users_clicked();
|
|
||||||
callback pw_accepted(int, string, string);
|
|
||||||
callback test_string_accepted <=> line_edit_test.accepted;
|
|
||||||
Rectangle { }
|
|
||||||
|
|
||||||
HorizontalLayout {
|
|
||||||
spacing: Style.spacing;
|
|
||||||
IconButton {
|
|
||||||
icon: @image-url("images/users.svg");
|
|
||||||
clicked => {
|
|
||||||
users_clicked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
combo_box_username := ComboBox { }
|
|
||||||
|
|
||||||
line_edit_user_pw := LineEdit {
|
|
||||||
input-type: InputType.password;
|
|
||||||
placeholder-text: "Password";
|
|
||||||
accepted(text) => {
|
|
||||||
root.pw_accepted(combo_box_username.current-index, combo_box_username.current-value, text);
|
|
||||||
line_edit_user_pw.text = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton {
|
|
||||||
icon: @image-url("images/log-in.svg");
|
|
||||||
clicked => {
|
|
||||||
root.pw_accepted(combo_box_username.current-index, combo_box_username.current-value, line_edit_user_pw.text);
|
|
||||||
line_edit_user_pw.text = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
line_edit_test := LineEdit {
|
|
||||||
placeholder-text: "Text to send to host.";
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle { }
|
|
||||||
}
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
VerticalBox,
|
|
||||||
LineEdit,
|
|
||||||
GridBox,
|
|
||||||
StandardListView,
|
|
||||||
ListView,
|
|
||||||
ComboBox,
|
|
||||||
} from "std-widgets.slint";
|
|
||||||
|
|
||||||
// Implementations of standard widgets can be found at https://github.com/slint-ui/slint/tree/master/internal/compiler/widgets
|
|
||||||
|
|
||||||
// See https://github.com/slint-ui/slint/issues/4956 for issues with fonts.
|
|
||||||
import "../fonts/IBM_Plex_Mono/IBMPlexMono-Regular.ttf";
|
|
||||||
import { UserSitesView } from "user-sites-view.slint";
|
|
||||||
import { Style } from "globals.slint";
|
|
||||||
import { UsersView } from "users-view.slint";
|
|
||||||
import { LoginView } from "login-view.slint";
|
|
||||||
import { UserEditView } from "user-edit-view.slint";
|
|
||||||
|
|
||||||
export enum AppState {
|
|
||||||
login,
|
|
||||||
users,
|
|
||||||
user-edit,
|
|
||||||
user-sites,
|
|
||||||
}
|
|
||||||
|
|
||||||
export component AppWindow inherits Window {
|
|
||||||
// Special characters to generate pre-render glyphs for.
|
|
||||||
in property <string> dummy: "ÄÖÜÁÉÍÓÚÝŔŚĹŹĆŃĚĽŽŠČŘĎŤŇŮÅäöüáéíóúýŕśĺźćńěľžščřďťňůåß„“”‘’—–@&$%+=¡¿¢£$¥€²³¼½¬¤¦§©®™°´ˇ¨●";
|
|
||||||
in property <string> dummy_identicon_symbols: "╔╚╰═█░▒▓☺☻╗╝╯═◈◎◐◑◒◓☀☁☂☃☄★☆☎☏⎈⌂☘☢☣☕⌚⌛⏰⚡⛄⛅☔♔♕♖♗♘♙♚♛♜♝♞♟♨♩♪♫⚐⚑⚔⚖⚙⚠⌘⏎✄✆✈✉✌";
|
|
||||||
default-font-family: "IBM Plex Mono";
|
|
||||||
default-font-size: 16pt;
|
|
||||||
height: 368px;
|
|
||||||
width: 960px;
|
|
||||||
forward-focus: focus-scope;
|
|
||||||
in property <AppState> app-state: AppState.login;
|
|
||||||
callback escape();
|
|
||||||
callback enter-view(view: AppState);
|
|
||||||
// Login View
|
|
||||||
in property <[string]> login_usernames <=> login_view.usernames;
|
|
||||||
callback login_pw_accepted <=> login_view.pw_accepted;
|
|
||||||
callback login_test_string_accepted <=> login_view.test_string_accepted;
|
|
||||||
// Users View
|
|
||||||
callback users_edit_user <=> users_view.edit_user;
|
|
||||||
// User Edit View
|
|
||||||
in property <string> user_edit_username <=> user_edit_view.username;
|
|
||||||
in property <string> user_edit_key_error <=> user_edit_view.key_error;
|
|
||||||
in-out property <string> user_edit_identicon <=> user_edit_view.identicon;
|
|
||||||
in-out property <string> user_edit_key_id <=> user_edit_view.key_id;
|
|
||||||
callback user_edit_compute_identicon <=> user_edit_view.compute_identicon;
|
|
||||||
callback user_edit_compute_key_id <=> user_edit_view.compute_key_id;
|
|
||||||
callback user_edit_confirm <=> user_edit_view.confirm;
|
|
||||||
// User Sites View
|
|
||||||
in property <[StandardListViewItem]> user_sites_sites <=> user_sites_view.model;
|
|
||||||
callback user_sites_site_name_edited <=> user_sites_view.site_name_edited;
|
|
||||||
callback user_sites_site_name_accepted <=> user_sites_view.site_name_accepted;
|
|
||||||
public function user_sites_site_name_clear() {
|
|
||||||
user_sites_view.site_name_clear();
|
|
||||||
}
|
|
||||||
focus-scope := FocusScope {
|
|
||||||
key-pressed(event) => {
|
|
||||||
if event.text == "\u{1b}" {
|
|
||||||
root.escape();
|
|
||||||
EventResult.accept
|
|
||||||
} else {
|
|
||||||
EventResult.reject
|
|
||||||
}
|
|
||||||
}
|
|
||||||
vertical-box := VerticalBox {
|
|
||||||
width: 960px;
|
|
||||||
height: 368px;
|
|
||||||
padding: 0px;
|
|
||||||
padding-top: 120px;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
Rectangle {
|
|
||||||
height: 240px;
|
|
||||||
background: #00141d;
|
|
||||||
// For debugging bounds.
|
|
||||||
// border-color: #ffcf00;
|
|
||||||
// border-width: 1px;
|
|
||||||
login_view := LoginView {
|
|
||||||
visible: app-state == AppState.login;
|
|
||||||
users_clicked => {
|
|
||||||
enter-view(AppState.users);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
users_view := UsersView {
|
|
||||||
visible: app-state == AppState.users;
|
|
||||||
}
|
|
||||||
|
|
||||||
user_edit_view := UserEditView {
|
|
||||||
visible: app-state == AppState.user-edit;
|
|
||||||
cancel => {
|
|
||||||
root.escape();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
user_sites_view := UserSitesView {
|
|
||||||
visible: app-state == AppState.user-sites;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
import { LineEdit, StandardListView, Button } from "std-widgets.slint";
|
|
||||||
import { Style } from "globals.slint";
|
|
||||||
import { IconButton } from "widgets/icon-button.slint";
|
|
||||||
|
|
||||||
export component UserEditView inherits HorizontalLayout {
|
|
||||||
padding: Style.spacing;
|
|
||||||
spacing: Style.spacing;
|
|
||||||
in property <string> username;
|
|
||||||
in property <string> key_error;
|
|
||||||
in-out property <string> identicon;
|
|
||||||
in-out property <string> key_id;
|
|
||||||
callback compute_identicon(password: string);
|
|
||||||
callback compute_key_id(encrypted_key: string);
|
|
||||||
callback confirm(encrypted_key: string);
|
|
||||||
callback cancel <=> button_cancel.clicked;
|
|
||||||
VerticalLayout {
|
|
||||||
spacing: Style.spacing;
|
|
||||||
Rectangle { }
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "Enter " + username + "'s encrypted key and press Enter:";
|
|
||||||
}
|
|
||||||
|
|
||||||
line_edit_password := LineEdit {
|
|
||||||
input-type: InputType.password;
|
|
||||||
placeholder-text: "Password";
|
|
||||||
edited(text) => {
|
|
||||||
root.identicon = "";
|
|
||||||
root.key_id = "";
|
|
||||||
}
|
|
||||||
accepted(text) => {
|
|
||||||
compute_identicon(text);
|
|
||||||
line_edit_password.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: identicon.is-empty ? "" : ("Check the identicon: " + identicon);
|
|
||||||
}
|
|
||||||
|
|
||||||
line_edit_encrypted_key := LineEdit {
|
|
||||||
input-type: InputType.text;
|
|
||||||
placeholder-text: "Encrypted key";
|
|
||||||
enabled: !identicon.is-empty;
|
|
||||||
edited(text) => {
|
|
||||||
root.key_id = "";
|
|
||||||
}
|
|
||||||
accepted(text) => {
|
|
||||||
compute_key_id(text);
|
|
||||||
button_confirm.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: !key_error.is-empty ? key_error : (!key_id.is-empty ? ("Key ID: " + key_id) : "");
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalLayout {
|
|
||||||
spacing: Style.spacing;
|
|
||||||
button_cancel := Button {
|
|
||||||
text: "Cancel";
|
|
||||||
}
|
|
||||||
|
|
||||||
button_confirm := Button {
|
|
||||||
text: "Confirm";
|
|
||||||
enabled: !identicon.is-empty && !key_id.is-empty;
|
|
||||||
clicked => {
|
|
||||||
confirm(line_edit_encrypted_key.text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
import { LineEdit, StandardListView, Button } from "std-widgets.slint";
|
|
||||||
import { Style } from "globals.slint";
|
|
||||||
import { IconButton } from "widgets/icon-button.slint";
|
|
||||||
|
|
||||||
export component UserSitesView inherits HorizontalLayout {
|
|
||||||
padding: Style.spacing;
|
|
||||||
spacing: Style.spacing;
|
|
||||||
in property <[StandardListViewItem]> model <=> list_view_sites.model;
|
|
||||||
in-out property <int> current-item <=> list_view_sites.current-item;
|
|
||||||
callback site_name_edited <=> line_edit_site_name.edited;
|
|
||||||
callback site_name_accepted(site_list_index: int);
|
|
||||||
public function site_name_clear() {
|
|
||||||
line_edit_site_name.text = "";
|
|
||||||
}
|
|
||||||
FocusScope {
|
|
||||||
key-pressed(event) => {
|
|
||||||
if event.text == "\n" {
|
|
||||||
site_name_accepted(list_view_sites.current-item);
|
|
||||||
EventResult.accept
|
|
||||||
} else {
|
|
||||||
EventResult.reject
|
|
||||||
}
|
|
||||||
}
|
|
||||||
VerticalLayout {
|
|
||||||
spacing: Style.spacing;
|
|
||||||
Text {
|
|
||||||
text: "Send password for:";
|
|
||||||
}
|
|
||||||
|
|
||||||
line_edit_site_name := LineEdit {
|
|
||||||
input-type: InputType.text;
|
|
||||||
placeholder-text: "example.org";
|
|
||||||
}
|
|
||||||
|
|
||||||
list_view_sites := StandardListView { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VerticalLayout {
|
|
||||||
spacing: Style.spacing;
|
|
||||||
// IconButton {
|
|
||||||
// icon: @image-url("images/log-out.svg");
|
|
||||||
// }
|
|
||||||
|
|
||||||
IconButton {
|
|
||||||
icon: @image-url("images/sliders.svg");
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton {
|
|
||||||
icon: @image-url("images/help-circle.svg");
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton {
|
|
||||||
icon: @image-url("images/key.svg");
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton {
|
|
||||||
icon: @image-url("images/trash-2.svg");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
import { LineEdit, StandardListView, Button } from "std-widgets.slint";
|
|
||||||
import { Style } from "globals.slint";
|
|
||||||
import { IconButton } from "widgets/icon-button.slint";
|
|
||||||
|
|
||||||
export component UsersView inherits HorizontalLayout {
|
|
||||||
padding: Style.spacing;
|
|
||||||
spacing: Style.spacing;
|
|
||||||
callback edit_user(username: string, new: bool);
|
|
||||||
VerticalLayout {
|
|
||||||
spacing: Style.spacing * 2;
|
|
||||||
VerticalLayout {
|
|
||||||
spacing: Style.spacing;
|
|
||||||
Text {
|
|
||||||
text: "Add new user:";
|
|
||||||
}
|
|
||||||
|
|
||||||
line_edit_site_pw := LineEdit {
|
|
||||||
input-type: InputType.text;
|
|
||||||
placeholder-text: "Full Name";
|
|
||||||
accepted(text) => {
|
|
||||||
edit_user(text, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VerticalLayout {
|
|
||||||
spacing: Style.spacing;
|
|
||||||
Text {
|
|
||||||
text: "Edit existing user:";
|
|
||||||
}
|
|
||||||
|
|
||||||
FocusScope {
|
|
||||||
forward-focus: list_view_sites;
|
|
||||||
key-pressed(event) => {
|
|
||||||
if event.text == "\u{0D}" /* enter */ || event.text == " " /* space */ {
|
|
||||||
edit_user(list_view_sites.current-item, false);
|
|
||||||
EventResult.accept
|
|
||||||
} else {
|
|
||||||
EventResult.reject
|
|
||||||
}
|
|
||||||
}
|
|
||||||
list_view_sites := StandardListView { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VerticalLayout {
|
|
||||||
spacing: Style.spacing;
|
|
||||||
IconButton {
|
|
||||||
icon: @image-url("images/key.svg");
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton {
|
|
||||||
icon: @image-url("images/trash-2.svg");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
import { Button } from "std-widgets.slint";
|
|
||||||
|
|
||||||
export component IconButton inherits Button {
|
|
||||||
colorize-icon: true;
|
|
||||||
icon-size: 24px;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
}
|
|
||||||
52
firmware/build.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
fn main() {
|
||||||
|
linker_be_nice();
|
||||||
|
// make sure linkall.x is the last linker script (otherwise might cause problems with flip-link)
|
||||||
|
println!("cargo:rustc-link-arg=-Tlinkall.x");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn linker_be_nice() {
|
||||||
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
if args.len() > 1 {
|
||||||
|
let kind = &args[1];
|
||||||
|
let what = &args[2];
|
||||||
|
|
||||||
|
match kind.as_str() {
|
||||||
|
"undefined-symbol" => match what.as_str() {
|
||||||
|
"_defmt_timestamp" => {
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("💡 `defmt` not found - make sure `defmt.x` is added as a linker script and you have included `use defmt_rtt as _;`");
|
||||||
|
eprintln!();
|
||||||
|
}
|
||||||
|
"_stack_start" => {
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("💡 Is the linker script `linkall.x` missing?");
|
||||||
|
eprintln!();
|
||||||
|
}
|
||||||
|
"esp_wifi_preempt_enable"
|
||||||
|
| "esp_wifi_preempt_yield_task"
|
||||||
|
| "esp_wifi_preempt_task_create" => {
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("💡 `esp-wifi` has no scheduler enabled. Make sure you have the `builtin-scheduler` feature enabled, or that you provide an external scheduler.");
|
||||||
|
eprintln!();
|
||||||
|
}
|
||||||
|
"embedded_test_linker_file_not_added_to_rustflags" => {
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("💡 `embedded-test` not found - make sure `embedded-test.x` is added as a linker script for tests");
|
||||||
|
eprintln!();
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
// we don't have anything helpful for "missing-lib" yet
|
||||||
|
_ => {
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"cargo:rustc-link-arg=-Wl,--error-handling-script={}",
|
||||||
|
std::env::current_exe().unwrap().display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit d24faf56214469b354b01c8ba36257e04737101e
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
if [ "$#" -lt 1 ]; then
|
|
||||||
echo "Usage: $0 <installation-directory-name>"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
|
||||||
REPO_DIR="$SCRIPT_DIR/libsodium"
|
|
||||||
INSTALL_DIR_NAME="$REPO_DIR/$1"
|
|
||||||
|
|
||||||
pushd "$REPO_DIR" >/dev/null
|
|
||||||
env CC=xtensa-esp32s3-elf-gcc \
|
|
||||||
CFLAGS="-ffreestanding -fno-builtin -mlongcalls" \
|
|
||||||
LDFLAGS="-nostdlib -static" \
|
|
||||||
./configure \
|
|
||||||
--host xtensa-esp32s3 \
|
|
||||||
--disable-shared \
|
|
||||||
--enable-static \
|
|
||||||
--prefix="$INSTALL_DIR_NAME"
|
|
||||||
make -j
|
|
||||||
make install
|
|
||||||
popd >/dev/null
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
[env] # These must be kept in sync with /.zed/settings.json
|
|
||||||
LIBSODIUM_INSTALL_DIR = "../libsodium/install-host"
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "password-hash"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "password_hash"
|
|
||||||
path = "src/lib.rs"
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "password_hash"
|
|
||||||
path = "src/bin.rs"
|
|
||||||
required-features = ["cmd"]
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = ["cmd"]
|
|
||||||
std = []
|
|
||||||
cmd = ["std", "dep:scrypt", "dep:itertools", "dep:spectre-api-sys", "dep:byteorder"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
argon2 = { version = "0.5.3", default-features = false, features = ["alloc"] }
|
|
||||||
itertools = { version = "0.14.0", optional = true }
|
|
||||||
scrypt = { version = "0.11.0", default-features = false, optional = true }
|
|
||||||
spectre-api-sys = { workspace = true, optional = true }
|
|
||||||
byteorder = { version = "1.5.0", optional = true }
|
|
||||||
# TODO: Why is this unable to provide the symbols for spectre-api-sys?
|
|
||||||
# libsodium-sys-stable = { version = "1.23.2", optional = true }
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
# Compiling
|
|
||||||
|
|
||||||
Compile on Linux or in WSL.
|
|
||||||
|
|
||||||
Compile libsodium for the host:
|
|
||||||
```bash
|
|
||||||
cd ../libsodium
|
|
||||||
./configure --disable-shared --enable-static --prefix="$PWD/install-host"
|
|
||||||
make -j
|
|
||||||
make install
|
|
||||||
```
|
|
||||||
|
|
||||||
Then compile and run password-hash:
|
|
||||||
```bash
|
|
||||||
cd ../password-hash
|
|
||||||
cargo run --release [--target x86_64-unknown-linux-gnu]
|
|
||||||
```
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
fn main() {
|
|
||||||
#[cfg(feature = "cmd")]
|
|
||||||
{
|
|
||||||
use std::{env, path::PathBuf};
|
|
||||||
|
|
||||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
|
||||||
println!(
|
|
||||||
"cargo:rustc-link-search=native={}",
|
|
||||||
manifest_dir.join("../spectre-api-c/build-host").display()
|
|
||||||
);
|
|
||||||
println!("cargo:rustc-link-lib=static=spectre");
|
|
||||||
println!(
|
|
||||||
"cargo:rerun-if-changed={}",
|
|
||||||
manifest_dir
|
|
||||||
.join("../spectre-api-c/build-host/libspectre.a")
|
|
||||||
.display()
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Ok(libsodium_install_dir) = env::var("LIBSODIUM_INSTALL_DIR") {
|
|
||||||
let libsodium_install_dir =
|
|
||||||
PathBuf::from(libsodium_install_dir).canonicalize().unwrap();
|
|
||||||
println!(
|
|
||||||
"cargo:rustc-link-search=native={}",
|
|
||||||
libsodium_install_dir.join("lib").display()
|
|
||||||
);
|
|
||||||
println!("cargo:rustc-link-lib=static=sodium");
|
|
||||||
println!(
|
|
||||||
"cargo:rerun-if-changed={}",
|
|
||||||
libsodium_install_dir.join("lib/libsodium.a").display()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
panic!("Environment variable `LIBSODIUM_INSTALL_DIR` missing!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
#![feature(allocator_api)]
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
alloc::Global,
|
|
||||||
ffi::{CStr, CString},
|
|
||||||
io::{Cursor, Write, stdin},
|
|
||||||
str::FromStr,
|
|
||||||
};
|
|
||||||
|
|
||||||
use byteorder::{BigEndian, WriteBytesExt};
|
|
||||||
use itertools::Itertools;
|
|
||||||
use password_hash::{Key, derive_encryption_key, encrypt_with};
|
|
||||||
use spectre_api_sys::SpectreAlgorithm;
|
|
||||||
|
|
||||||
macro_rules! read_line {
|
|
||||||
($lines:expr, $label:expr) => {{
|
|
||||||
print!("{}: ", $label);
|
|
||||||
std::io::stdout().flush().unwrap();
|
|
||||||
CString::from_str(
|
|
||||||
&$lines
|
|
||||||
.next()
|
|
||||||
.transpose()
|
|
||||||
.ok()
|
|
||||||
.flatten()
|
|
||||||
.unwrap_or_else(|| panic!("Expected {}.", $label)),
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let print_secrets = std::env::args().contains("--print-secrets");
|
|
||||||
|
|
||||||
let mut lines = stdin().lines();
|
|
||||||
let username = read_line!(lines, "username");
|
|
||||||
let secret = read_line!(lines, "secret");
|
|
||||||
|
|
||||||
let user_key: Key = unsafe {
|
|
||||||
&*spectre_api_sys::spectre_user_key(
|
|
||||||
username.as_ptr(),
|
|
||||||
secret.as_ptr(),
|
|
||||||
SpectreAlgorithm::Current,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.bytes;
|
|
||||||
let user_key_id = unsafe { spectre_api_sys::spectre_id_buf(user_key.as_ptr(), user_key.len()) };
|
|
||||||
let salt = derive_user_key_salt(&username);
|
|
||||||
let encryption_key = derive_encryption_key(&salt, secret.as_bytes(), Global);
|
|
||||||
|
|
||||||
if print_secrets {
|
|
||||||
println!("\nUser Key (SECRET):\n{:02x}", user_key.iter().format(""));
|
|
||||||
println!(
|
|
||||||
"\nEncryption Key (SECRET):\n{:02x}",
|
|
||||||
encryption_key.iter().format("")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut encrypted_user_key = user_key;
|
|
||||||
|
|
||||||
encrypt_with(&mut encrypted_user_key, &encryption_key);
|
|
||||||
println!(
|
|
||||||
"\nUser Key ID:\n{:02x}",
|
|
||||||
user_key_id.bytes.iter().format("")
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"\nEncrypted User Key:\n{:02x}",
|
|
||||||
encrypted_user_key.iter().format("")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copied from the body of the `spectre_user_key_v3` function.
|
|
||||||
fn derive_user_key_salt(username: &CStr) -> Vec<u8> {
|
|
||||||
use spectre_api_sys::*;
|
|
||||||
|
|
||||||
let mut salt = Cursor::new(Vec::new());
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let key_scope: &'static CStr =
|
|
||||||
CStr::from_ptr(spectre_purpose_scope(SpectreKeyPurpose::Authentication));
|
|
||||||
|
|
||||||
salt.write_all(key_scope.to_bytes()).unwrap();
|
|
||||||
salt.write_u32::<BigEndian>(username.count_bytes() as u32)
|
|
||||||
.unwrap();
|
|
||||||
salt.write_all(username.to_bytes()).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
salt.into_inner()
|
|
||||||
}
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
use core::slice;
|
|
||||||
|
|
||||||
use argon2::Block;
|
|
||||||
|
|
||||||
pub(crate) struct Argon2Blocks<A: alloc::alloc::Allocator> {
|
|
||||||
p: core::ptr::NonNull<Block>,
|
|
||||||
len: usize,
|
|
||||||
alloc: A,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<A: alloc::alloc::Allocator> Argon2Blocks<A> {
|
|
||||||
/// Each block is 1 KiB.
|
|
||||||
/// Total size is `len` * 1 KiB
|
|
||||||
pub fn new_in(len: usize, alloc: A) -> Option<Self> {
|
|
||||||
use alloc::alloc::Layout;
|
|
||||||
|
|
||||||
if len == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let layout = Layout::array::<Block>(len).ok()?;
|
|
||||||
// SAFETY: `alloc_zeroed` is used correctly with non-zero layout
|
|
||||||
let p = alloc.allocate_zeroed(layout).ok()?.cast();
|
|
||||||
|
|
||||||
Some(Self { p, len, alloc })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<A: alloc::alloc::Allocator> AsMut<[Block]> for Argon2Blocks<A> {
|
|
||||||
fn as_mut(&mut self) -> &mut [Block] {
|
|
||||||
unsafe { slice::from_raw_parts_mut(self.p.as_ptr(), self.len) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<A: alloc::alloc::Allocator> AsRef<[Block]> for Argon2Blocks<A> {
|
|
||||||
fn as_ref(&self) -> &[Block] {
|
|
||||||
unsafe { slice::from_raw_parts(self.p.as_ptr(), self.len) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<A: alloc::alloc::Allocator> Drop for Argon2Blocks<A> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
use alloc::alloc::Layout;
|
|
||||||
// SAFETY: layout was checked during construction
|
|
||||||
let layout = unsafe { Layout::array::<Block>(self.len).unwrap_unchecked() };
|
|
||||||
// SAFETY: we use `dealloc` correctly with the previously allocated pointer
|
|
||||||
unsafe {
|
|
||||||
self.alloc.deallocate(self.p.cast(), layout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
//! scrypt memory requirements scale linearly with parameters `N` and `r`.
|
|
||||||
//! This makes it unsuitable for embedded environments with the parameters
|
|
||||||
//! used in Spectre.
|
|
||||||
//! Our work-around is to derive the Spectre _user key_ using scrypt on the
|
|
||||||
//! host, encrypt it with XOR using a key derived using argon2, which
|
|
||||||
//! has parameters for specifying the memory and time requirements separately.
|
|
||||||
//! This encrypted key is then stored on the keyboard, to be decrypted again.
|
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
#![feature(allocator_api)]
|
|
||||||
|
|
||||||
extern crate alloc;
|
|
||||||
|
|
||||||
use alloc::alloc::Allocator;
|
|
||||||
use alloc::vec::Vec;
|
|
||||||
use argon2::{Algorithm, Argon2, ParamsBuilder, Version};
|
|
||||||
|
|
||||||
use crate::blocks::Argon2Blocks;
|
|
||||||
|
|
||||||
pub mod blocks;
|
|
||||||
|
|
||||||
pub type Key = [u8; 64];
|
|
||||||
|
|
||||||
/// KiB used by the argon2 algorithm.
|
|
||||||
/// Lower than the default, to fit in the constrained memory of embedded devices.
|
|
||||||
pub const ARGON2_M_COST: u32 = 1024;
|
|
||||||
/// Compensate the difficulty by increasing the iterations proportionally.
|
|
||||||
pub const ARGON2_T_COST: u32 =
|
|
||||||
argon2::Params::DEFAULT_T_COST * argon2::Params::DEFAULT_M_COST / ARGON2_M_COST;
|
|
||||||
pub const ARGON2_P_COST: u32 = argon2::Params::DEFAULT_P_COST;
|
|
||||||
pub const ARGON2_SALT_PREFIX: &[u8] = b"acid-firmware\0";
|
|
||||||
|
|
||||||
pub fn derive_encryption_key(
|
|
||||||
unprefixed_salt: &[u8],
|
|
||||||
secret: &[u8],
|
|
||||||
allocator: impl Allocator,
|
|
||||||
) -> Key {
|
|
||||||
let argon2 = Argon2::new(
|
|
||||||
Algorithm::Argon2id,
|
|
||||||
Version::default(),
|
|
||||||
ParamsBuilder::default()
|
|
||||||
.m_cost(ARGON2_M_COST)
|
|
||||||
.t_cost(ARGON2_T_COST)
|
|
||||||
.p_cost(ARGON2_P_COST)
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
let mut blocks = Argon2Blocks::new_in(ARGON2_M_COST as usize, &allocator).unwrap();
|
|
||||||
let mut key: Key = [0u8; _];
|
|
||||||
// Salt is prefixed to form a salt that is long enough for Argon2.
|
|
||||||
let mut salt =
|
|
||||||
Vec::with_capacity_in(unprefixed_salt.len() + ARGON2_SALT_PREFIX.len(), &allocator);
|
|
||||||
salt.extend_from_slice(ARGON2_SALT_PREFIX);
|
|
||||||
salt.extend_from_slice(unprefixed_salt);
|
|
||||||
argon2
|
|
||||||
.hash_password_into_with_memory(secret, &salt, &mut key, &mut blocks)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
key
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decrypt_with(data: &mut Key, key: &Key) {
|
|
||||||
for (dst_byte, user_byte) in core::iter::zip(data.iter_mut(), key.iter()) {
|
|
||||||
*dst_byte ^= *user_byte;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn encrypt_with(data: &mut Key, key: &Key) {
|
|
||||||
decrypt_with(data, key);
|
|
||||||
}
|
|
||||||
2
firmware/rust-toolchain.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
[toolchain]
|
||||||
|
channel = "esp"
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 733e6d6d73bc9462d8ec9528dd96eee23846a3e3
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
if [ "$#" -lt 2 ]; then
|
|
||||||
echo "Usage: $0 <build-directory-name> <sodium-install-dir>"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
|
||||||
REPO_DIR="$SCRIPT_DIR/spectre-api-c"
|
|
||||||
BUILD_DIR_NAME="$REPO_DIR/$1"
|
|
||||||
STATIC_LIB_PATH="$BUILD_DIR_NAME/libspectre.a"
|
|
||||||
|
|
||||||
pushd "$REPO_DIR" >/dev/null
|
|
||||||
meson setup "$BUILD_DIR_NAME" --cross-file "$SCRIPT_DIR/cross-esp32s3.txt" -Dlibsodium-install-dir="$2"
|
|
||||||
meson compile -C "$BUILD_DIR_NAME"
|
|
||||||
$SCRIPT_DIR/redefine-syms.sh "__spre_" "$STATIC_LIB_PATH" "$STATIC_LIB_PATH"
|
|
||||||
popd >/dev/null
|
|
||||||
1258
firmware/src/bin/main.rs
Normal file
5
firmware/src/lib.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#![no_std]
|
||||||
|
#![deny(clippy::mem_forget)]
|
||||||
|
#![feature(macro_metavar_expr)]
|
||||||
|
|
||||||
|
pub mod st7701s;
|
||||||
1212
firmware/src/st7701s.rs
Normal file
37
firmware2/.cargo/config.toml
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
[target.'cfg(all(any(target_arch = "riscv32", target_arch = "xtensa"), target_os = "none"))']
|
||||||
|
runner = "espflash flash --monitor"
|
||||||
|
# runner = "probe-rs run --chip esp32s3 --preverify"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
target = "xtensa-esp32s3-none-elf"
|
||||||
|
rustflags = [
|
||||||
|
# Required to obtain backtraces (e.g. when using the "esp-backtrace" crate.)
|
||||||
|
# NOTE: May negatively impact performance of produced code
|
||||||
|
"-C", "force-frame-pointers",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[env]
|
||||||
|
LIBXKBCOMMON_BUILD_DIR = "libxkbcommon/build"
|
||||||
|
ESP_LOG = "warn"
|
||||||
|
# This is overkill, but we can afford it.
|
||||||
|
SLINT_FONT_SIZES = "8,11,10,12,13,14,15,16,18,20,22,24,32"
|
||||||
|
|
||||||
|
# Xtensa only:
|
||||||
|
# Needed for nightly, until llvm upstream has support for Rust Xtensa.
|
||||||
|
[unstable]
|
||||||
|
build-std = ["alloc", "core"]
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
rmk = { path = "../../../rust/rmk/rmk" }
|
||||||
|
xkbcommon = { path = "../../../rust/xkbcommon-rs-ffi" }
|
||||||
|
|
||||||
|
# [patch.crates-io]
|
||||||
|
# esp-backtrace = { path = "../../../rust/esp-hal/esp-backtrace" }
|
||||||
|
# esp-hal = { path = "../../../rust/esp-hal/esp-hal" }
|
||||||
|
# esp-storage = { path = "../../../rust/esp-hal/esp-storage" }
|
||||||
|
# esp-alloc = { path = "../../../rust/esp-hal/esp-alloc" }
|
||||||
|
# esp-println = { path = "../../../rust/esp-hal/esp-println" }
|
||||||
|
# esp-radio = { path = "../../../rust/esp-hal/esp-radio" }
|
||||||
|
# esp-rtos = { path = "../../../rust/esp-hal/esp-rtos" }
|
||||||
|
# esp-bootloader-esp-idf = { path = "../../../rust/esp-hal/esp-bootloader-esp-idf" }
|
||||||
|
|
@ -9,18 +9,10 @@
|
||||||
"chip": "esp32s3",
|
"chip": "esp32s3",
|
||||||
"coreConfigs": [
|
"coreConfigs": [
|
||||||
{
|
{
|
||||||
"programBinary": "target/xtensa-esp32s3-none-elf/debug/acid-firmware",
|
"programBinary": "target/xtensa-esp32s3-none-elf/debug/acid-firmware"
|
||||||
"rttEnabled": true,
|
|
||||||
"rttChannelFormats": [
|
|
||||||
{
|
|
||||||
"channelNumber": 0,
|
|
||||||
"dataFormat": "String",
|
|
||||||
"mode": "BlockIfFull"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "probe-rs release restart",
|
"name": "probe-rs release restart",
|
||||||
"type": "probe-rs-debug",
|
"type": "probe-rs-debug",
|
||||||
|
|
@ -29,18 +21,10 @@
|
||||||
"chip": "esp32s3",
|
"chip": "esp32s3",
|
||||||
"coreConfigs": [
|
"coreConfigs": [
|
||||||
{
|
{
|
||||||
"programBinary": "target/xtensa-esp32s3-none-elf/release/acid-firmware",
|
"programBinary": "target/xtensa-esp32s3-none-elf/release/acid-firmware"
|
||||||
"rttEnabled": true,
|
|
||||||
"rttChannelFormats": [
|
|
||||||
{
|
|
||||||
"channelNumber": 0,
|
|
||||||
"dataFormat": "String",
|
|
||||||
"mode": "BlockIfFull"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"preLaunchTask": "rust: cargo build",
|
"preLaunchTask": "rust: cargo build",
|
||||||
"name": "probe-rs debug",
|
"name": "probe-rs debug",
|
||||||
|
|
@ -48,26 +32,15 @@
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"flashingConfig": {
|
"flashingConfig": {
|
||||||
"flashingEnabled": true,
|
"flashingEnabled": true,
|
||||||
"formatOptions": {
|
|
||||||
"idf_partition_table": "partition-table.csv"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"probe": "303a:1001",
|
"probe": "303a:1001",
|
||||||
"chip": "esp32s3",
|
"chip": "esp32s3",
|
||||||
"coreConfigs": [
|
"coreConfigs": [
|
||||||
{
|
{
|
||||||
"programBinary": "target/xtensa-esp32s3-none-elf/debug/acid-firmware",
|
"programBinary": "target/xtensa-esp32s3-none-elf/debug/acid-firmware"
|
||||||
"rttEnabled": true,
|
|
||||||
"rttChannelFormats": [
|
|
||||||
{
|
|
||||||
"channelNumber": 0,
|
|
||||||
"dataFormat": "String",
|
|
||||||
"mode": "BlockIfFull"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"preLaunchTask": "rust: cargo build --release",
|
"preLaunchTask": "rust: cargo build --release",
|
||||||
"name": "probe-rs release",
|
"name": "probe-rs release",
|
||||||
|
|
@ -75,25 +48,14 @@
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"flashingConfig": {
|
"flashingConfig": {
|
||||||
"flashingEnabled": true,
|
"flashingEnabled": true,
|
||||||
"formatOptions": {
|
|
||||||
"idf_partition_table": "partition-table.csv"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"probe": "303a:1001",
|
"probe": "303a:1001",
|
||||||
"chip": "esp32s3",
|
"chip": "esp32s3",
|
||||||
"coreConfigs": [
|
"coreConfigs": [
|
||||||
{
|
{
|
||||||
"programBinary": "target/xtensa-esp32s3-none-elf/release/acid-firmware",
|
"programBinary": "target/xtensa-esp32s3-none-elf/release/acid-firmware"
|
||||||
"rttEnabled": true,
|
}
|
||||||
"rttChannelFormats": [
|
]
|
||||||
{
|
|
||||||
"channelNumber": 0,
|
|
||||||
"dataFormat": "String",
|
|
||||||
"mode": "BlockIfFull"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
@ -4,15 +4,9 @@
|
||||||
{
|
{
|
||||||
"label": "rust: cargo build",
|
"label": "rust: cargo build",
|
||||||
"type": "cargo",
|
"type": "cargo",
|
||||||
"options": {
|
|
||||||
"cwd": "${workspaceFolder}/acid-firmware",
|
|
||||||
"env": {
|
|
||||||
"ESP_LOG": "info"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"command": "build",
|
"command": "build",
|
||||||
"args": [
|
"args": [
|
||||||
"--no-default-features", "--features=probe"
|
"--no-default-features", "--features=probe,info"
|
||||||
],
|
],
|
||||||
"problemMatcher": [
|
"problemMatcher": [
|
||||||
"$rustc"
|
"$rustc"
|
||||||
|
|
@ -25,15 +19,9 @@
|
||||||
{
|
{
|
||||||
"label": "rust: cargo build --release",
|
"label": "rust: cargo build --release",
|
||||||
"type": "cargo",
|
"type": "cargo",
|
||||||
"options": {
|
|
||||||
"cwd": "${workspaceFolder}/acid-firmware",
|
|
||||||
"env": {
|
|
||||||
"ESP_LOG": "info"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"command": "build",
|
"command": "build",
|
||||||
"args": [
|
"args": [
|
||||||
"--release", "--no-default-features", "--features=probe"
|
"--release", "--no-default-features", "--features=probe,info"
|
||||||
],
|
],
|
||||||
"problemMatcher": [
|
"problemMatcher": [
|
||||||
"$rustc"
|
"$rustc"
|
||||||
8555
firmware2/Cargo.lock
generated
Normal file
115
firmware2/Cargo.toml
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
[package]
|
||||||
|
name = "acid-firmware"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ['Jakub "Limeth" Hlusička']
|
||||||
|
description = "Firmware for the ACID keyboard"
|
||||||
|
homepage = "https://github.com/haobogu/rmk"
|
||||||
|
repository = "https://github.com/haobogu/rmk"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["usb-log", "limit-fps"]
|
||||||
|
# Make RMK not to use USB
|
||||||
|
no-usb = ["rmk/_no_usb"]
|
||||||
|
# Let RMK use BLE
|
||||||
|
ble = ["rmk/esp32s3_ble"]
|
||||||
|
# Use alternative logging via GPIO5 as RX and GPIO12 as TX.
|
||||||
|
# Disables default logging via USB.
|
||||||
|
# Does not support esp-println's `println!`.
|
||||||
|
alt-log = []
|
||||||
|
# Standard logging implementation over USB.
|
||||||
|
usb-log = ["esp-backtrace/panic-handler"]
|
||||||
|
# RTT (+ logging) for probe-rs
|
||||||
|
rtt-log = ["dep:rtt-target", "dep:panic-rtt-target"]
|
||||||
|
# Block the main core while it is driving the LCD.
|
||||||
|
# This prevents the main core from accessing PSRAM while the LCD is being driven,
|
||||||
|
# which causes the LCD to glitch. To prevent the main core from spending all its
|
||||||
|
# execution time on just driving the LCD, it will be limited.
|
||||||
|
limit-fps = []
|
||||||
|
# Development profiles
|
||||||
|
develop = ["limit-fps", "alt-log"]
|
||||||
|
develop-usb = ["limit-fps", "usb-log", "no-usb", "ble"]
|
||||||
|
probe = ["limit-fps", "rtt-log", "no-usb", "ble"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rmk = { version = "0.8.2", default-features = false, features = [
|
||||||
|
"log",
|
||||||
|
"storage",
|
||||||
|
"vial",
|
||||||
|
"controller",
|
||||||
|
] }
|
||||||
|
embassy-executor = { version = "0.9", features = ["log"] }
|
||||||
|
embassy-time = { version = "0.5.0", features = ["log"] }
|
||||||
|
embassy-embedded-hal = "0.5.0"
|
||||||
|
embassy-sync = { version = "0.7.2", features = ["log"] }
|
||||||
|
esp-backtrace = { version = "0.18", default-features = false, features = [
|
||||||
|
"esp32s3",
|
||||||
|
"println",
|
||||||
|
] }
|
||||||
|
esp-hal = { version = "1.0", features = ["esp32s3", "unstable", "psram", "log-04"] }
|
||||||
|
esp-storage = { version = "0.8.0", features = ["esp32s3"] }
|
||||||
|
esp-alloc = { version = "0.9.0", features = ["nightly"] }
|
||||||
|
esp-println = { version = "0.16.0", features = ["esp32s3", "log-04"] }
|
||||||
|
esp-radio = { version = "0.17", features = ["esp32s3", "unstable", "ble"] }
|
||||||
|
esp-rtos = { version = "0.2", features = ["esp32s3", "esp-radio", "embassy"] }
|
||||||
|
esp-bootloader-esp-idf = { version = "0.4", features = ["esp32s3", "log-04"] }
|
||||||
|
bt-hci = { version = "0.6" }
|
||||||
|
rand_core = { version = "0.6", default-features = false }
|
||||||
|
static_cell = "2"
|
||||||
|
lazy_static = { version = "1.5.0", features = ["spin_no_std"], default-features = false }
|
||||||
|
log = "0.4.29"
|
||||||
|
bitflags = "2.10.0"
|
||||||
|
paste = "1.0.15"
|
||||||
|
itertools = { version = "0.14.0", default-features = false }
|
||||||
|
bytemuck = "1.24.0"
|
||||||
|
slint = { version = "1.14.1", default-features = false, features = ["compat-1-2", "libm", "log", "unsafe-single-threaded", "renderer-software"]}
|
||||||
|
i-slint-common = "1.14.1"
|
||||||
|
critical-section = "1.2.0"
|
||||||
|
cfg-if = "1.0.4"
|
||||||
|
xkbcommon = { git = "https://github.com/Limeth/xkbcommon-rs", branch = "esp32s3", default-features = false, features = ["c-lib-wrap"] }
|
||||||
|
rtt-target = { version = "0.6.2", features = ["log"], optional = true }
|
||||||
|
panic-rtt-target = { version = "0.2.0", optional = true }
|
||||||
|
enumset = "1.1.10"
|
||||||
|
printf-compat = { version = "0.2.1", default-features = false }
|
||||||
|
|
||||||
|
# Crates for serial UART CLI
|
||||||
|
embedded-cli = { version = "0.2.1", default-features = false, features = ["help", "macros"] }
|
||||||
|
embedded-io = "0.6.1"
|
||||||
|
mutually_exclusive_features = "0.1.0"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
xz2 = "0.1.7"
|
||||||
|
json = "0.12"
|
||||||
|
const-gen = "1.6"
|
||||||
|
embuild = "0.33"
|
||||||
|
cc = "1.2.9"
|
||||||
|
slint-build = "1.14.1"
|
||||||
|
gix = { version = "0.76.0", default-features = false, features = ["max-performance", "status"] }
|
||||||
|
indoc = "2.0.7"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "acid-firmware"
|
||||||
|
test = false
|
||||||
|
bench = false
|
||||||
|
|
||||||
|
[profile.release-with-debug]
|
||||||
|
inherits = "release"
|
||||||
|
debug = true
|
||||||
|
|
||||||
|
|
||||||
|
[profile.dev.package.esp-storage]
|
||||||
|
opt-level = 3
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
# Rust debug is too slow.
|
||||||
|
# For debug builds always builds with some optimization
|
||||||
|
opt-level = "s"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
codegen-units = 1 # LLVM can perform better optimizations using a single thread
|
||||||
|
debug = 2
|
||||||
|
debug-assertions = false
|
||||||
|
incremental = false
|
||||||
|
lto = 'thin'
|
||||||
|
opt-level = 3
|
||||||
|
overflow-checks = false
|
||||||
|
|
@ -43,25 +43,14 @@ This replaces the debugging symbols with paths that will be available when debug
|
||||||
|
|
||||||
Then compile the firmware with:
|
Then compile the firmware with:
|
||||||
```ps1
|
```ps1
|
||||||
$env:XKBCOMMON_BUILD_DIR="libxkbcommon/build-debug"; cargo build
|
$env:LIBXKBCOMMON_BUILD_DIR="libxkbcommon/build-debug"; cargo build
|
||||||
```
|
```
|
||||||
|
|
||||||
## Debugging via alternative UART pins
|
|
||||||
|
|
||||||
Connect your serial debugger's TX to GPIO5 and its RX to GPIO12.
|
|
||||||
On Linux, listen to the serial stream using:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
tio -m INLCRNL /dev/ttyUSB1
|
|
||||||
```
|
|
||||||
|
|
||||||
On Windows, use PuTTY with a baudrate of 115200.
|
|
||||||
|
|
||||||
### Creating keymaps
|
### Creating keymaps
|
||||||
|
|
||||||
To generate an English (US) keymap, the following command may be used:
|
To generate an English (US) keymap, the following command may be used:
|
||||||
|
|
||||||
`xkbcli compile-keymap --include [path-to-xkb-directory] --layout us >my_keymap.xkb`
|
`xkbcli compile-keymap --include [path-to-xkb-directory] --layout us >my_compose.txt`
|
||||||
|
|
||||||
Substitute `us` for any other 2-letter country code.
|
Substitute `us` for any other 2-letter country code.
|
||||||
|
|
||||||
|
|
@ -80,18 +69,7 @@ Use libxkbcommon's `xkbcli` to compile a standalone compose file:
|
||||||
|
|
||||||
Compose files to replace `[path-to-Compose-file]` with may be found in:
|
Compose files to replace `[path-to-Compose-file]` with may be found in:
|
||||||
* the `/usr/share/X11/locale` directory on X11-based Linux distributions;
|
* the `/usr/share/X11/locale` directory on X11-based Linux distributions;
|
||||||
* the [`libx11` git repository](https://gitlab.freedesktop.org/xorg/lib/libx11/-/tree/master/nls). There's a button to download it as a ZIP archive.
|
* the [`libx11` git repository](https://gitlab.freedesktop.org/xorg/lib/libx11/-/tree/master). There's an button to download it as a ZIP archive.
|
||||||
|
|
||||||
### Setup in GNOME
|
|
||||||
|
|
||||||
#### Making all xkb layouts available
|
|
||||||
|
|
||||||
By default, GNOME displays only the most common keyboard layouts (`xkb_symbols`). Other keyboard layouts can be made visible via:
|
|
||||||
```sh
|
|
||||||
gsettings set org.gnome.desktop.input-sources show-all-sources true
|
|
||||||
```
|
|
||||||
|
|
||||||
In order to use completely custom layouts, [this article](https://web.archive.org/web/20260212010717/https://staticf0x.github.io/2021/custom-keyboard-layout-in-x11-and-wayland.html) may be helpful.
|
|
||||||
|
|
||||||
# esp32s3 BLE example
|
# esp32s3 BLE example
|
||||||
|
|
||||||
138
firmware2/build.rs
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
use std::env;
|
||||||
|
use std::fs::{File, OpenOptions};
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use const_gen::*;
|
||||||
|
use indoc::formatdoc;
|
||||||
|
use json::JsonValue;
|
||||||
|
use slint_build::{CompilerConfiguration, EmbedResourcesKind};
|
||||||
|
use xz2::read::XzEncoder;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||||
|
if let Ok(repo) = gix::discover(&manifest_dir) {
|
||||||
|
let commit_hash = repo.head_commit().unwrap().short_id().unwrap();
|
||||||
|
println!("cargo:rustc-env=GIT_COMMIT_HASH={}", commit_hash);
|
||||||
|
println!(
|
||||||
|
"cargo:rustc-env=GIT_COMMIT={}",
|
||||||
|
repo.find_tag(repo.head_id().unwrap())
|
||||||
|
.ok()
|
||||||
|
.map(|tag| format!("{} ({})", tag.decode().unwrap().name, commit_hash))
|
||||||
|
.unwrap_or_else(|| commit_hash.to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate vial config at the root of project
|
||||||
|
println!("cargo:rerun-if-changed=vial.json");
|
||||||
|
generate_vial_config();
|
||||||
|
|
||||||
|
println!("cargo:rustc-link-arg-bins=-Tlinkall.x");
|
||||||
|
|
||||||
|
// Set the extra linker script from defmt
|
||||||
|
// println!("cargo:rustc-link-arg=-Tdefmt.x");
|
||||||
|
|
||||||
|
// TODO: Make it a submodule and use relative paths.
|
||||||
|
|
||||||
|
// Link libxkbcommon.
|
||||||
|
let libxkbcommon_build_dir = env::var("LIBXKBCOMMON_BUILD_DIR")
|
||||||
|
.expect("The build directory of libxkbcommon must be specified using the `LIBXKBCOMMON_BUILD_DIR` environment variable.");
|
||||||
|
let libxkbcommon_library_path = PathBuf::from(&libxkbcommon_build_dir).join("libxkbcommon.a");
|
||||||
|
|
||||||
|
if !libxkbcommon_library_path.is_file() {
|
||||||
|
panic!(
|
||||||
|
"{}",
|
||||||
|
formatdoc! {"
|
||||||
|
The compiled libxkbcommon library was not found at path {libxkbcommon_library_path:?}.
|
||||||
|
Most likely, the library was not compiled. See the `README.md` for compilation instructions.
|
||||||
|
If the `LIBXKBCOMMON_BUILD_DIR` environment variable is not set to the build directory you want to use, change it.
|
||||||
|
Currently, `LIBXKBCOMMON_BUILD_DIR` is set to: {libxkbcommon_build_dir}
|
||||||
|
"}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("cargo:rustc-link-search=native={libxkbcommon_build_dir}");
|
||||||
|
println!("cargo:rustc-link-lib=static=xkbcommon");
|
||||||
|
println!("cargo:rerun-if-changed={libxkbcommon_build_dir}/libxkbcommon.a");
|
||||||
|
|
||||||
|
// Slint config and compilation
|
||||||
|
{
|
||||||
|
// Don't think this does anything:
|
||||||
|
// println!("cargo:rerun-if-env-changed=SLINT_FONT_SIZES");
|
||||||
|
|
||||||
|
let slint_config = CompilerConfiguration::new()
|
||||||
|
// .with_scale_factor(2.0)
|
||||||
|
.with_style("cosmic-dark".to_string())
|
||||||
|
.embed_resources(EmbedResourcesKind::EmbedForSoftwareRenderer);
|
||||||
|
slint_build::compile_with_config("ui/main.slint", slint_config)
|
||||||
|
.expect("Slint build failed");
|
||||||
|
slint_build::print_rustc_flags().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_vial_config() {
|
||||||
|
// Generated vial config file
|
||||||
|
let path = Path::new(&env::var_os("OUT_DIR").unwrap()).join("config_generated.rs");
|
||||||
|
let mut out_file = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.write(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(path)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let p = Path::new("vial.json");
|
||||||
|
let mut content = String::new();
|
||||||
|
match File::open(p) {
|
||||||
|
Ok(mut file) => {
|
||||||
|
file.read_to_string(&mut content)
|
||||||
|
.expect("Cannot read vial.json");
|
||||||
|
}
|
||||||
|
Err(e) => println!("Cannot find vial.json {p:?}: {e}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let vial_cfg = json::parse(&content).unwrap();
|
||||||
|
let vial_cfg_string = json::stringify(vial_cfg.clone());
|
||||||
|
let mut keyboard_def_compressed: Vec<u8> = Vec::new();
|
||||||
|
XzEncoder::new(vial_cfg_string.as_bytes(), 6)
|
||||||
|
.read_to_end(&mut keyboard_def_compressed)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let keyboard_id: Vec<u8> = vec![0xB9, 0xBC, 0x09, 0xB2, 0x9D, 0x37, 0x4C, 0xEA];
|
||||||
|
let const_declarations = [
|
||||||
|
const_declaration!(pub VIAL_KEYBOARD_DEF = keyboard_def_compressed),
|
||||||
|
const_declaration!(pub VIAL_KEYBOARD_ID = keyboard_id),
|
||||||
|
]
|
||||||
|
.map(|s| "#[allow(clippy::redundant_static_lifetimes)]\n".to_owned() + s.as_str())
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
writeln!(out_file, "{}", const_declarations).unwrap();
|
||||||
|
|
||||||
|
writeln!(out_file, "#[repr(u8)] pub enum CustomKeycodes {{").unwrap();
|
||||||
|
|
||||||
|
// const CUSTOM_KEYCODE_FIRST: u16 = 0x840;
|
||||||
|
|
||||||
|
#[allow(clippy::collapsible_if)]
|
||||||
|
if let JsonValue::Object(vial_cfg) = vial_cfg {
|
||||||
|
if let Some(JsonValue::Array(custom_keycodes)) = vial_cfg.get("customKeycodes") {
|
||||||
|
for (index, custom_keycode) in custom_keycodes.iter().enumerate() {
|
||||||
|
if let JsonValue::Object(custom_keycode) = custom_keycode {
|
||||||
|
let name = custom_keycode
|
||||||
|
.get("name")
|
||||||
|
.expect("A custom keycode in vial.json is missing a name.")
|
||||||
|
.as_str()
|
||||||
|
.expect("A custom keycode's name must be a string.");
|
||||||
|
writeln!(
|
||||||
|
out_file,
|
||||||
|
" {} = {},",
|
||||||
|
name,
|
||||||
|
// CUSTOM_KEYCODE_FIRST + index as u16
|
||||||
|
index as u8
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(out_file, "}}").unwrap();
|
||||||
|
}
|
||||||
6
firmware/libxkbcommon-compile.sh → firmware2/libxkbcommon-compile.sh
Executable file → Normal file
|
|
@ -10,8 +10,10 @@ BUILD_DIR_NAME="$LIBXKBCOMMON_DIR/$1"
|
||||||
STATIC_LIB_PATH="$BUILD_DIR_NAME/libxkbcommon.a"
|
STATIC_LIB_PATH="$BUILD_DIR_NAME/libxkbcommon.a"
|
||||||
SETUP_ARGS=${@:2}
|
SETUP_ARGS=${@:2}
|
||||||
|
|
||||||
|
git submodule update --init --recursive
|
||||||
pushd "$LIBXKBCOMMON_DIR" >/dev/null
|
pushd "$LIBXKBCOMMON_DIR" >/dev/null
|
||||||
meson setup "$BUILD_DIR_NAME" \
|
meson setup "$BUILD_DIR_NAME" \
|
||||||
|
--wipe \
|
||||||
--cross-file "$SCRIPT_DIR/cross-esp32s3.txt" \
|
--cross-file "$SCRIPT_DIR/cross-esp32s3.txt" \
|
||||||
-Denable-x11=false \
|
-Denable-x11=false \
|
||||||
-Denable-wayland=false \
|
-Denable-wayland=false \
|
||||||
|
|
@ -21,8 +23,8 @@ pushd "$LIBXKBCOMMON_DIR" >/dev/null
|
||||||
-Dxkb-config-root=/usr/share/X11/xkb \
|
-Dxkb-config-root=/usr/share/X11/xkb \
|
||||||
-Dx-locale-root=/usr/share/X11/locale \
|
-Dx-locale-root=/usr/share/X11/locale \
|
||||||
$SETUP_ARGS
|
$SETUP_ARGS
|
||||||
meson compile -C "$BUILD_DIR_NAME" xkbcommon
|
meson compile -C "$BUILD_DIR_NAME"
|
||||||
$SCRIPT_DIR/redefine-syms.sh "__xkbc_" "$STATIC_LIB_PATH" "$STATIC_LIB_PATH"
|
$SCRIPT_DIR/libxkbcommon-redefine-syms.sh "$STATIC_LIB_PATH" "$STATIC_LIB_PATH"
|
||||||
popd >/dev/null
|
popd >/dev/null
|
||||||
|
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
10
firmware/redefine-syms.sh → firmware2/libxkbcommon-redefine-syms.sh
Executable file → Normal file
|
|
@ -1,10 +1,10 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
PREFIX=$1
|
INPUT_LIB=$1
|
||||||
INPUT_LIB=$2
|
OUTPUT_LIB=$2
|
||||||
OUTPUT_LIB=$3
|
PREFIX="__xkbc_"
|
||||||
|
|
||||||
if [ "$#" -ne 3 ]; then
|
if [ "$#" -ne 2 ]; then
|
||||||
echo "Usage: $0 <prefix> <input_lib.a> <output_lib.a>"
|
echo "Usage: $0 <input_lib.a> <output_lib.a>"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
|
|
||||||
use core::fmt::Write;
|
use core::fmt::Write;
|
||||||
|
|
||||||
use embedded_cli::Command;
|
|
||||||
use embedded_cli::cli::CliBuilder;
|
use embedded_cli::cli::CliBuilder;
|
||||||
use esp_hal::{
|
use embedded_cli::Command;
|
||||||
Async,
|
use esp_hal::{Async, uart::{TxError, UartRx}};
|
||||||
uart::{TxError, UartRx},
|
use log::{info, error};
|
||||||
};
|
|
||||||
use log::{error, info};
|
|
||||||
|
|
||||||
use crate::logging::uart::with_uart_tx;
|
use crate::logging::with_uart_tx;
|
||||||
|
|
||||||
struct Writer;
|
struct Writer;
|
||||||
|
|
||||||
|
|
@ -18,21 +16,26 @@ impl embedded_io::ErrorType for Writer {
|
||||||
|
|
||||||
impl embedded_io::Write for Writer {
|
impl embedded_io::Write for Writer {
|
||||||
fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
|
fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
|
||||||
with_uart_tx(|_, uart| uart.write(buf))
|
with_uart_tx(|_, uart| {
|
||||||
|
uart.write(buf)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flush(&mut self) -> Result<(), Self::Error> {
|
fn flush(&mut self) -> Result<(), Self::Error> {
|
||||||
with_uart_tx(|_, uart| uart.flush())
|
with_uart_tx(|_, uart| {
|
||||||
|
uart.flush()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Command)]
|
#[derive(Command)]
|
||||||
enum Base /*<'a>*/ {
|
enum Base/*<'a>*/ {
|
||||||
// /// Say hello to World or someone else
|
// /// Say hello to World or someone else
|
||||||
// Hello {
|
// Hello {
|
||||||
// /// To whom to say hello (World by default)
|
// /// To whom to say hello (World by default)
|
||||||
// name: Option<&'a str>,
|
// name: Option<&'a str>,
|
||||||
// },
|
// },
|
||||||
|
|
||||||
/// Display the version of the firmware.
|
/// Display the version of the firmware.
|
||||||
Version,
|
Version,
|
||||||
|
|
||||||
|
|
@ -70,26 +73,16 @@ pub async fn run_console(mut uart_rx: UartRx<'_, Async>) {
|
||||||
// write!(cli.writer(), "Hello, {}", name.unwrap_or("World"))?;
|
// write!(cli.writer(), "Hello, {}", name.unwrap_or("World"))?;
|
||||||
// }
|
// }
|
||||||
Base::Version => {
|
Base::Version => {
|
||||||
cli.writer()
|
cli.writer().write_fmt(format_args!("{} - {} - {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"), env!("GIT_COMMIT"))).unwrap();
|
||||||
.write_fmt(format_args!(
|
|
||||||
"{} - {} - {}",
|
|
||||||
env!("CARGO_PKG_NAME"),
|
|
||||||
env!("CARGO_PKG_VERSION"),
|
|
||||||
env!("GIT_COMMIT")
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
Base::Reset => {
|
Base::Reset => {
|
||||||
cli.writer()
|
cli.writer().write_str("Performing software reset.").unwrap();
|
||||||
.write_str("Performing software reset.")
|
|
||||||
.unwrap();
|
|
||||||
esp_hal::system::software_reset();
|
esp_hal::system::software_reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}),
|
}),
|
||||||
)
|
).unwrap();
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
#![allow(unused_variables)]
|
#![allow(unused_variables)]
|
||||||
|
|
||||||
use core::alloc::GlobalAlloc;
|
|
||||||
use core::ffi::{c_size_t, c_void};
|
use core::ffi::{c_size_t, c_void};
|
||||||
|
use core::alloc::GlobalAlloc;
|
||||||
|
|
||||||
use enumset::EnumSet;
|
use enumset::EnumSet;
|
||||||
|
use esp_alloc::EspHeap;
|
||||||
use crate::ffi::string::__xkbc_memcpy;
|
|
||||||
|
|
||||||
// Here we select the allocator to use for libxkbcommon.
|
// Here we select the allocator to use for libxkbcommon.
|
||||||
pub use crate::PSRAM_ALLOCATOR as XKBC_ALLOCATOR;
|
static XKBC_ALLOCATOR: &EspHeap = &crate::PSRAM_ALLOCATOR;
|
||||||
|
|
||||||
// Implementation based on esp-alloc's `compat` feature.
|
// Implementation based on esp-alloc's `compat` feature.
|
||||||
|
|
||||||
|
|
@ -17,14 +16,9 @@ pub unsafe extern "C" fn __xkbc_malloc(size: c_size_t) -> *mut c_void {
|
||||||
unsafe { malloc_with_caps(size, EnumSet::empty()) as *mut _ }
|
unsafe { malloc_with_caps(size, EnumSet::empty()) as *mut _ }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub unsafe extern "C" fn __spre_malloc(size: c_size_t) -> *mut c_void {
|
|
||||||
unsafe { __xkbc_malloc(size) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub unsafe extern "C" fn __xkbc_calloc(number: c_size_t, size: c_size_t) -> *mut c_void {
|
pub unsafe extern "C" fn __xkbc_calloc(number: c_size_t, size: c_size_t) -> *mut c_void {
|
||||||
let total_size = number * size;
|
let total_size = number as usize * size;
|
||||||
unsafe {
|
unsafe {
|
||||||
let ptr = __xkbc_malloc(total_size) as *mut u8;
|
let ptr = __xkbc_malloc(total_size) as *mut u8;
|
||||||
|
|
||||||
|
|
@ -38,21 +32,11 @@ pub unsafe extern "C" fn __xkbc_calloc(number: c_size_t, size: c_size_t) -> *mut
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub unsafe extern "C" fn __spre_calloc(number: c_size_t, size: c_size_t) -> *mut c_void {
|
|
||||||
unsafe { __xkbc_calloc(number, size) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub unsafe extern "C" fn __xkbc_realloc(ptr: *mut c_void, new_size: c_size_t) -> *mut c_void {
|
pub unsafe extern "C" fn __xkbc_realloc(ptr: *mut c_void, new_size: c_size_t) -> *mut c_void {
|
||||||
unsafe { realloc_with_caps(ptr as *mut _, new_size, EnumSet::empty()) as *mut _ }
|
unsafe { realloc_with_caps(ptr as *mut _, new_size, EnumSet::empty()) as *mut _ }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub unsafe extern "C" fn __spre_realloc(ptr: *mut c_void, new_size: c_size_t) -> *mut c_void {
|
|
||||||
unsafe { __xkbc_realloc(ptr, new_size) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub unsafe extern "C" fn __xkbc_free(ptr: *mut c_void) {
|
pub unsafe extern "C" fn __xkbc_free(ptr: *mut c_void) {
|
||||||
if ptr.is_null() {
|
if ptr.is_null() {
|
||||||
|
|
@ -70,13 +54,6 @@ pub unsafe extern "C" fn __xkbc_free(ptr: *mut c_void) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub unsafe extern "C" fn __spre_free(ptr: *mut c_void) {
|
|
||||||
unsafe {
|
|
||||||
__xkbc_free(ptr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe fn malloc_with_caps(size: usize, caps: EnumSet<crate::MemoryCapability>) -> *mut u8 {
|
unsafe fn malloc_with_caps(size: usize, caps: EnumSet<crate::MemoryCapability>) -> *mut u8 {
|
||||||
let total_size = size + 4;
|
let total_size = size + 4;
|
||||||
|
|
||||||
|
|
@ -100,6 +77,10 @@ unsafe fn realloc_with_caps(
|
||||||
new_size: usize,
|
new_size: usize,
|
||||||
caps: enumset::EnumSet<crate::MemoryCapability>,
|
caps: enumset::EnumSet<crate::MemoryCapability>,
|
||||||
) -> *mut u8 {
|
) -> *mut u8 {
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn memcpy(d: *mut u8, s: *const u8, l: usize);
|
||||||
|
}
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
let p = malloc_with_caps(new_size, caps);
|
let p = malloc_with_caps(new_size, caps);
|
||||||
if !p.is_null() && !ptr.is_null() {
|
if !p.is_null() && !ptr.is_null() {
|
||||||
|
|
@ -107,7 +88,7 @@ unsafe fn realloc_with_caps(
|
||||||
(ptr as *const u32).sub(1).read_volatile() as usize,
|
(ptr as *const u32).sub(1).read_volatile() as usize,
|
||||||
new_size,
|
new_size,
|
||||||
);
|
);
|
||||||
__xkbc_memcpy(p as *mut _, ptr as *const _, len);
|
memcpy(p, ptr, len);
|
||||||
__xkbc_free(ptr as *mut _);
|
__xkbc_free(ptr as *mut _);
|
||||||
}
|
}
|
||||||
p
|
p
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use core::ffi::{c_char, c_int};
|
use core::ffi::{c_char, c_int};
|
||||||
|
|
||||||
#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
|
#[allow(non_camel_case_types)]
|
||||||
pub enum DIR {}
|
pub enum DIR {}
|
||||||
|
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
|
|
@ -26,11 +26,7 @@ pub mod file;
|
||||||
pub unsafe extern "C" fn __xkbc_fopen(filename: *const c_char, mode: *const c_char) -> *mut FILE {
|
pub unsafe extern "C" fn __xkbc_fopen(filename: *const c_char, mode: *const c_char) -> *mut FILE {
|
||||||
warn!(
|
warn!(
|
||||||
"The xkbcommon library is attempting to open a file at path: {:?}",
|
"The xkbcommon library is attempting to open a file at path: {:?}",
|
||||||
if filename.is_null() {
|
unsafe { CStr::from_ptr(filename) }
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(unsafe { CStr::from_ptr(filename) })
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
null_mut()
|
null_mut()
|
||||||
}
|
}
|
||||||
|
|
@ -76,15 +72,6 @@ pub unsafe extern "C" fn __xkbc_fprintf(
|
||||||
unsafe { __xkbc_vfprintf(stream, format, args.as_va_list()) }
|
unsafe { __xkbc_vfprintf(stream, format, args.as_va_list()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub unsafe extern "C" fn __spre_fprintf(
|
|
||||||
stream: *mut FILE,
|
|
||||||
format: *const c_char,
|
|
||||||
args: ...
|
|
||||||
) -> c_int {
|
|
||||||
unsafe { __xkbc_fprintf(stream, format) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub unsafe extern "C" fn __xkbc_vfprintf(
|
pub unsafe extern "C" fn __xkbc_vfprintf(
|
||||||
stream: *mut FILE,
|
stream: *mut FILE,
|
||||||
|
|
@ -150,43 +137,3 @@ pub unsafe extern "C" fn __xkbc_vsnprintf(
|
||||||
string_length as c_int
|
string_length as c_int
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub unsafe extern "C" fn __spre_sprintf(
|
|
||||||
string: *mut c_char,
|
|
||||||
format: *const c_char,
|
|
||||||
mut args: ...
|
|
||||||
) -> c_int {
|
|
||||||
unsafe { vsprintf(string, format, args.as_va_list()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub unsafe fn vsprintf(string: *mut c_char, format: *const c_char, ap: VaList) -> c_int {
|
|
||||||
let mut rust_buffer = String::new();
|
|
||||||
unsafe {
|
|
||||||
printf_compat::format(format, ap, fmt_write(&mut rust_buffer));
|
|
||||||
// __xkbc_strncpy would be preferrable, if it was available
|
|
||||||
__xkbc_memcpy(
|
|
||||||
string as *mut _,
|
|
||||||
rust_buffer.as_ptr() as *mut _,
|
|
||||||
rust_buffer.len(),
|
|
||||||
);
|
|
||||||
*string.add(rust_buffer.len()) = 0; // Add terminating null byte.
|
|
||||||
rust_buffer.len() as c_int
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub unsafe extern "C" fn __spre_vsnprintf(
|
|
||||||
string: *mut c_char,
|
|
||||||
// Length in bytes **including the terminating null byte**.
|
|
||||||
size: c_size_t,
|
|
||||||
format: *const c_char,
|
|
||||||
ap: VaList,
|
|
||||||
) -> c_int {
|
|
||||||
unsafe { __xkbc_vsnprintf(string, size, format, ap) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub unsafe extern "C" fn __spre_sscanf(s: *const c_char, format: *const c_char, ...) -> c_int {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
@ -8,11 +8,9 @@ use core::{
|
||||||
use inout::file::{FILE, STDERR, STDIN, STDOUT};
|
use inout::file::{FILE, STDERR, STDIN, STDOUT};
|
||||||
|
|
||||||
pub mod alloc;
|
pub mod alloc;
|
||||||
pub mod crypto;
|
|
||||||
pub mod gcc_runtime;
|
pub mod gcc_runtime;
|
||||||
pub mod inout;
|
pub mod inout;
|
||||||
pub mod string;
|
pub mod string;
|
||||||
pub mod time;
|
|
||||||
|
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
pub type c_intmax_t = c_longlong;
|
pub type c_intmax_t = c_longlong;
|
||||||
|
|
@ -40,11 +38,6 @@ pub unsafe extern "C" fn __xkbc___errno() -> *mut c_int {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub unsafe extern "C" fn __spre___errno() -> *mut c_int {
|
|
||||||
unsafe { __xkbc___errno() }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub unsafe extern "C" fn __xkbc_qsort(
|
pub unsafe extern "C" fn __xkbc_qsort(
|
||||||
base: *mut c_void,
|
base: *mut c_void,
|
||||||
|
|
@ -83,11 +76,6 @@ pub unsafe extern "C" fn __xkbc___getreent() -> *mut _reent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub unsafe extern "C" fn __spre___getreent() -> *mut _reent {
|
|
||||||
unsafe { __xkbc___getreent() }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub unsafe extern "C" fn __xkbc_secure_getenv(name: *const c_char) -> *mut c_char {
|
pub unsafe extern "C" fn __xkbc_secure_getenv(name: *const c_char) -> *mut c_char {
|
||||||
unsafe { __xkbc_getenv(name) }
|
unsafe { __xkbc_getenv(name) }
|
||||||
|
|
@ -148,12 +136,3 @@ pub unsafe extern "C" fn __xkbc___assert_func(
|
||||||
pub unsafe extern "C" fn __xkbc_atoi(s: *const c_char) -> c_int {
|
pub unsafe extern "C" fn __xkbc_atoi(s: *const c_char) -> c_int {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: What is this even for?
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub static __spre__ctype_: [c_char; 0] = [];
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub unsafe extern "C" fn __spre_abort() -> ! {
|
|
||||||
panic!("`abort()` called.")
|
|
||||||
}
|
|
||||||
|
|
@ -13,7 +13,7 @@ pub unsafe extern "C" fn __xkbc_memset(
|
||||||
c: c_int,
|
c: c_int,
|
||||||
n: c_size_t,
|
n: c_size_t,
|
||||||
) -> *mut c_void {
|
) -> *mut c_void {
|
||||||
if dest_original.is_null() {
|
if dest_original == null_mut() {
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
panic!("Attempted to memset a nullptr.");
|
panic!("Attempted to memset a nullptr.");
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -33,15 +33,6 @@ pub unsafe extern "C" fn __xkbc_memset(
|
||||||
dest_original
|
dest_original
|
||||||
}
|
}
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub unsafe extern "C" fn __spre_memset(
|
|
||||||
dest_original: *mut c_void,
|
|
||||||
c: c_int,
|
|
||||||
n: c_size_t,
|
|
||||||
) -> *mut c_void {
|
|
||||||
unsafe { __xkbc_memset(dest_original, c, n) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub unsafe extern "C" fn __xkbc_memmove(
|
pub unsafe extern "C" fn __xkbc_memmove(
|
||||||
dst: *mut c_void,
|
dst: *mut c_void,
|
||||||
|
|
@ -64,15 +55,6 @@ pub unsafe extern "C" fn __xkbc_memcpy(
|
||||||
dst
|
dst
|
||||||
}
|
}
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub unsafe extern "C" fn __spre_memcpy(
|
|
||||||
dst: *mut c_void,
|
|
||||||
src: *const c_void,
|
|
||||||
count: c_size_t,
|
|
||||||
) -> *mut c_void {
|
|
||||||
unsafe { __xkbc_memcpy(dst, src, count) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub unsafe extern "C" fn __xkbc_memchr(s: *const c_void, c: c_int, n: c_size_t) -> *mut c_void {
|
pub unsafe extern "C" fn __xkbc_memchr(s: *const c_void, c: c_int, n: c_size_t) -> *mut c_void {
|
||||||
let mut s = s as *const c_uchar;
|
let mut s = s as *const c_uchar;
|
||||||
|
|
@ -108,11 +90,6 @@ pub unsafe extern "C" fn __xkbc_memcmp(s1: *const c_char, s2: *const c_char, n:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub unsafe extern "C" fn __spre_memcmp(s1: *const c_char, s2: *const c_char, n: c_size_t) -> c_int {
|
|
||||||
unsafe { __xkbc_memcmp(s1, s2, n) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub unsafe extern "C" fn __xkbc_strpbrk(s: *const c_char, accept: *const c_char) -> *mut c_char {
|
pub unsafe extern "C" fn __xkbc_strpbrk(s: *const c_char, accept: *const c_char) -> *mut c_char {
|
||||||
todo!()
|
todo!()
|
||||||
|
|
@ -123,11 +100,6 @@ pub unsafe extern "C" fn __xkbc_strerror(errnum: c_int) -> *mut c_char {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub unsafe extern "C" fn __spre_strerror(errnum: c_int) -> *mut c_char {
|
|
||||||
unsafe { __xkbc_strerror(errnum) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub unsafe extern "C" fn __xkbc_strdup(string: *const c_char) -> *mut c_char {
|
pub unsafe extern "C" fn __xkbc_strdup(string: *const c_char) -> *mut c_char {
|
||||||
strndup_inner(string, unsafe { __xkbc_strlen(string) })
|
strndup_inner(string, unsafe { __xkbc_strlen(string) })
|
||||||
|
|
@ -164,11 +136,6 @@ pub unsafe extern "C" fn __xkbc_strlen(mut s: *const c_char) -> c_size_t {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub unsafe extern "C" fn __spre_strlen(s: *const c_char) -> c_size_t {
|
|
||||||
unsafe { __xkbc_strlen(s) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub unsafe extern "C" fn __xkbc_strnlen(s: *const c_char, maxlen: c_size_t) -> c_size_t {
|
pub unsafe extern "C" fn __xkbc_strnlen(s: *const c_char, maxlen: c_size_t) -> c_size_t {
|
||||||
let found: *const c_char =
|
let found: *const c_char =
|
||||||
|
|
@ -201,15 +168,6 @@ pub unsafe extern "C" fn __xkbc_strncmp(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub unsafe extern "C" fn __spre_strncmp(
|
|
||||||
s1: *const c_char,
|
|
||||||
s2: *const c_char,
|
|
||||||
n: c_size_t,
|
|
||||||
) -> c_int {
|
|
||||||
unsafe { __xkbc_strncmp(s1, s2, n) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub unsafe extern "C" fn __xkbc_strchr(cs: *const c_char, c: c_int) -> *mut c_char {
|
pub unsafe extern "C" fn __xkbc_strchr(cs: *const c_char, c: c_int) -> *mut c_char {
|
||||||
todo!()
|
todo!()
|
||||||
|
|
@ -240,12 +198,3 @@ pub unsafe extern "C" fn __xkbc_strtol(
|
||||||
) -> c_long {
|
) -> c_long {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub unsafe extern "C" fn __spre_strtol(
|
|
||||||
nptr: *const c_char,
|
|
||||||
endptr: *mut *mut c_char,
|
|
||||||
base: c_int,
|
|
||||||
) -> c_long {
|
|
||||||
unsafe { __xkbc_strtol(nptr, endptr, base) }
|
|
||||||
}
|
|
||||||
295
firmware2/src/keymap.rs
Normal file
|
|
@ -0,0 +1,295 @@
|
||||||
|
use core::alloc::Layout;
|
||||||
|
use core::convert::TryFrom;
|
||||||
|
use core::fmt::Debug;
|
||||||
|
use core::slice;
|
||||||
|
|
||||||
|
use alloc::boxed::Box;
|
||||||
|
use alloc::collections::btree_map::BTreeMap;
|
||||||
|
use alloc::string::String;
|
||||||
|
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||||
|
use embassy_sync::channel::Channel;
|
||||||
|
use embassy_time::Instant;
|
||||||
|
use esp_alloc::MemoryCapability;
|
||||||
|
use log::{debug, info, warn};
|
||||||
|
use rmk::descriptor::KeyboardReport;
|
||||||
|
use rmk::futures::FutureExt;
|
||||||
|
use rmk::hid::Report;
|
||||||
|
use rmk::types::action::{Action, KeyAction};
|
||||||
|
use rmk::{a, k, layer};
|
||||||
|
use slint::platform::Key;
|
||||||
|
use xkbcommon::xkb::{self, FeedResult, KeyDirection, Keysym, Status};
|
||||||
|
|
||||||
|
use crate::matrix::{MATRIX_COLS, MATRIX_ROWS};
|
||||||
|
use crate::vial::CustomKeycodes;
|
||||||
|
use crate::{KEYBOARD_REPORT_PROXY, PSRAM_ALLOCATOR};
|
||||||
|
|
||||||
|
pub const NUM_LAYER: usize = 1;
|
||||||
|
|
||||||
|
pub static KEY_MESSAGE_CHANNEL: Channel<CriticalSectionRawMutex, KeyMessage, 16> = Channel::new();
|
||||||
|
|
||||||
|
pub struct KeyMessage {
|
||||||
|
pub keysym: Keysym,
|
||||||
|
pub string: Option<String>,
|
||||||
|
pub direction: KeyDirection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for KeyMessage {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
f.debug_struct("KeyMessage")
|
||||||
|
.field("keysym", &self.keysym)
|
||||||
|
.field("string", &self.string)
|
||||||
|
.field_with("direction", |f| match self.direction {
|
||||||
|
KeyDirection::Down => f.write_str("Down"),
|
||||||
|
KeyDirection::Up => f.write_str("Up"),
|
||||||
|
})
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub const fn get_default_keymap() -> [[[KeyAction; MATRIX_COLS]; MATRIX_ROWS]; NUM_LAYER] {
|
||||||
|
[
|
||||||
|
layer!([
|
||||||
|
[k!(Escape), k!(Kc1), k!(Kc2), k!(Kc3), k!(Kc4), k!(Kc5), k!(Kc6), k!(Kc7), k!(Kc8), k!(Kc9), k!(Kc0), k!(Backspace)],
|
||||||
|
[k!(Tab), k!(Q), k!(W), k!(E), k!(R), k!(T), k!(Z), k!(U), k!(I), k!(O), k!(P), k!(Delete)],
|
||||||
|
[k!(LCtrl), k!(A), k!(S), k!(D), k!(F), k!(G), k!(H), k!(J), k!(K), k!(L), k!(Comma), k!(Enter)],
|
||||||
|
[k!(LShift), k!(Y), k!(X), k!(C), k!(V), k!(B), k!(N), k!(M), a!(No), a!(No), k!(Up), KeyAction::Single(Action::User(CustomKeycodes::FOCUS_LCD as u8))],
|
||||||
|
[a!(No), a!(No), k!(LGui), k!(LAlt), KeyAction::Single(Action::TriLayerLower), k!(Space), k!(Space), KeyAction::Single(Action::TriLayerLower), k!(RAlt), k!(Left), k!(Down), k!(Right)]
|
||||||
|
// [a!(No), a!(No), k!(LGui), k!(LAlt), k!(TriLayerLower), k!(Space), k!(Space), k!(TriLayerLower), k!(RAlt), k!(Left), k!(Down), k!(Right)]
|
||||||
|
])
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_hid_report_interceptor() -> impl Future<Output = ()> {
|
||||||
|
const KEYMAP_STRING: &str = include_str!("../keymaps/cz.xkb");
|
||||||
|
const COMPOSE_MAP_STRING: &str = include_str!("../compose/cs_CZ Compose.txt");
|
||||||
|
const COMPOSE_MAP_LOCALE: &str = "cs_CZ.UTF-8";
|
||||||
|
|
||||||
|
let keymap_string_buffer = unsafe {
|
||||||
|
let allocation = PSRAM_ALLOCATOR.alloc_caps(
|
||||||
|
MemoryCapability::External.into(),
|
||||||
|
Layout::from_size_align(KEYMAP_STRING.len(), 32).unwrap(),
|
||||||
|
);
|
||||||
|
let slice = str::from_utf8_unchecked_mut(slice::from_raw_parts_mut(
|
||||||
|
allocation,
|
||||||
|
KEYMAP_STRING.len(),
|
||||||
|
));
|
||||||
|
|
||||||
|
slice
|
||||||
|
.as_bytes_mut()
|
||||||
|
.copy_from_slice(KEYMAP_STRING.as_bytes());
|
||||||
|
|
||||||
|
Box::from_raw_in(slice as *mut str, &PSRAM_ALLOCATOR)
|
||||||
|
};
|
||||||
|
let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
|
||||||
|
info!("Loading XKB keymap...");
|
||||||
|
let instant_start = Instant::now();
|
||||||
|
let keymap = xkb::Keymap::new_from_string(
|
||||||
|
&context,
|
||||||
|
keymap_string_buffer,
|
||||||
|
xkb::KEYMAP_FORMAT_TEXT_V1,
|
||||||
|
xkb::KEYMAP_COMPILE_NO_FLAGS,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let duration = Instant::now().duration_since(instant_start);
|
||||||
|
info!(
|
||||||
|
"XKB keymap loaded successfully! Took {seconds}.{millis:03} seconds.",
|
||||||
|
seconds = duration.as_secs(),
|
||||||
|
millis = duration.as_millis() % 1_000
|
||||||
|
);
|
||||||
|
info!("Loading XKB compose map...");
|
||||||
|
let instant_start = Instant::now();
|
||||||
|
let compose_table = xkb::compose::Table::new_from_buffer(
|
||||||
|
&context,
|
||||||
|
COMPOSE_MAP_STRING,
|
||||||
|
COMPOSE_MAP_LOCALE,
|
||||||
|
xkb::compose::FORMAT_TEXT_V1,
|
||||||
|
xkb::compose::COMPILE_NO_FLAGS,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let duration = Instant::now().duration_since(instant_start);
|
||||||
|
info!(
|
||||||
|
"XKB compose map loaded successfully! Took {seconds}.{millis:03} seconds.",
|
||||||
|
seconds = duration.as_secs(),
|
||||||
|
millis = duration.as_millis() % 1_000
|
||||||
|
);
|
||||||
|
let mut state = xkb::State::new(&keymap);
|
||||||
|
let mut previous_state = KeyboardReport::default();
|
||||||
|
let mut compose_state = xkb::compose::State::new(&compose_table, xkb::compose::STATE_NO_FLAGS);
|
||||||
|
// TODO: Use a stack-allocated map instead
|
||||||
|
// This is a map from the basic keysyms (not a composed ones) to the string that should be produced.
|
||||||
|
let mut pressed_keys_to_strings = BTreeMap::<Keysym, Option<String>>::new();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
loop {
|
||||||
|
let report = KEYBOARD_REPORT_PROXY.receive().await;
|
||||||
|
|
||||||
|
if let Report::KeyboardReport(report) = &report {
|
||||||
|
const KEYCODES_LEN_MODIFIERS: usize = 8;
|
||||||
|
const KEYCODES_LEN_REGULAR: usize = 6;
|
||||||
|
const KEYCODES_LEN: usize = KEYCODES_LEN_MODIFIERS + KEYCODES_LEN_REGULAR;
|
||||||
|
|
||||||
|
let mut pressed_keys = rmk::heapless::Vec::<u8, KEYCODES_LEN>::new();
|
||||||
|
let mut released_keys = rmk::heapless::Vec::<u8, KEYCODES_LEN>::new();
|
||||||
|
|
||||||
|
let pressed_mods_bits = !previous_state.modifier & report.modifier;
|
||||||
|
let released_mods_bits = previous_state.modifier & !report.modifier;
|
||||||
|
|
||||||
|
for index in 0..KEYCODES_LEN_MODIFIERS {
|
||||||
|
const USB_HID_LEFT_CTRL: u8 = 0xE0;
|
||||||
|
let mod_bit = 1_u8 << index;
|
||||||
|
let mod_keycode = USB_HID_LEFT_CTRL + index as u8;
|
||||||
|
|
||||||
|
if pressed_mods_bits & mod_bit != 0 {
|
||||||
|
pressed_keys.push(mod_keycode).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if released_mods_bits & mod_bit != 0 {
|
||||||
|
released_keys.push(mod_keycode).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This currently depends on pressed keys not changing position in the array.
|
||||||
|
// Should be made independent of that.
|
||||||
|
for (&keycode_old, &keycode_new) in
|
||||||
|
core::iter::zip(&previous_state.keycodes, &report.keycodes)
|
||||||
|
{
|
||||||
|
if keycode_old == keycode_new {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if keycode_old != 0 {
|
||||||
|
released_keys.push(keycode_old).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if keycode_new != 0 {
|
||||||
|
pressed_keys.push(keycode_new).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
previous_state = report.clone();
|
||||||
|
|
||||||
|
for keycode in released_keys {
|
||||||
|
debug!("Release: 0x{:02x} ({})", keycode, keycode);
|
||||||
|
let keycode_xkb = into_xkb_keycode(keycode);
|
||||||
|
state.update_key(keycode_xkb, KeyDirection::Up);
|
||||||
|
let keysym = state.key_get_one_sym(keycode_xkb);
|
||||||
|
let string = pressed_keys_to_strings.remove(&keysym).unwrap_or_else(|| {
|
||||||
|
warn!("Could not determine the string of a released key: keysym={keysym:?} pressed_keys_to_strings={pressed_keys_to_strings:?}");
|
||||||
|
None
|
||||||
|
});
|
||||||
|
KEY_MESSAGE_CHANNEL
|
||||||
|
.send(KeyMessage {
|
||||||
|
keysym,
|
||||||
|
string,
|
||||||
|
direction: KeyDirection::Up,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
for keycode in pressed_keys {
|
||||||
|
debug!("Pressed: 0x{:02x} ({})", keycode, keycode);
|
||||||
|
let keycode_xkb = into_xkb_keycode(keycode);
|
||||||
|
let sym = state.key_get_one_sym(keycode_xkb);
|
||||||
|
|
||||||
|
let result: Option<(Keysym, Keysym, Option<String>)> = match compose_state
|
||||||
|
.feed(sym)
|
||||||
|
{
|
||||||
|
FeedResult::Ignored => {
|
||||||
|
let string = state.key_get_utf8(keycode_xkb);
|
||||||
|
Some((sym, sym, Some(string)))
|
||||||
|
}
|
||||||
|
FeedResult::Accepted => {
|
||||||
|
let status = compose_state.status();
|
||||||
|
debug!("Compose status: {status:?}");
|
||||||
|
match status {
|
||||||
|
Status::Nothing => {
|
||||||
|
let string = state.key_get_utf8(keycode_xkb);
|
||||||
|
Some((sym, sym, Some(string)))
|
||||||
|
}
|
||||||
|
Status::Composing => None,
|
||||||
|
Status::Composed => {
|
||||||
|
let composed_sym = compose_state.keysym().unwrap_or_default();
|
||||||
|
let string = compose_state.utf8().unwrap_or_default();
|
||||||
|
Some((sym, composed_sym, Some(string)))
|
||||||
|
}
|
||||||
|
Status::Cancelled => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((basic_keysym, composed_keysym, string)) = result {
|
||||||
|
// Change `Some("")` into `None`.
|
||||||
|
let string = string.filter(|string| !string.is_empty());
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Basic keysym: {basic_keysym:?}, composed keysym: {composed_keysym:?}, string: {:?}",
|
||||||
|
string.as_ref()
|
||||||
|
);
|
||||||
|
|
||||||
|
pressed_keys_to_strings.insert(basic_keysym, string.clone());
|
||||||
|
KEY_MESSAGE_CHANNEL
|
||||||
|
.send(KeyMessage {
|
||||||
|
keysym: composed_keysym,
|
||||||
|
string,
|
||||||
|
direction: KeyDirection::Down,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
state.update_key(keycode_xkb, KeyDirection::Down);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_xkb_keycode(rmk_keycode: u8) -> xkb::Keycode {
|
||||||
|
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/hid/hid-input.c?id=refs/tags/v6.18#n27
|
||||||
|
const UNK: u8 = 240;
|
||||||
|
#[rustfmt::skip]
|
||||||
|
const HID_KEYBOARD: [u8; 256] = [
|
||||||
|
0, 0, 0, 0, 30, 48, 46, 32, 18, 33, 34, 35, 23, 36, 37, 38,
|
||||||
|
50, 49, 24, 25, 16, 19, 31, 20, 22, 47, 17, 45, 21, 44, 2, 3,
|
||||||
|
4, 5, 6, 7, 8, 9, 10, 11, 28, 1, 14, 15, 57, 12, 13, 26,
|
||||||
|
27, 43, 43, 39, 40, 41, 51, 52, 53, 58, 59, 60, 61, 62, 63, 64,
|
||||||
|
65, 66, 67, 68, 87, 88, 99, 70, 119, 110, 102, 104, 111, 107, 109, 106,
|
||||||
|
105, 108, 103, 69, 98, 55, 74, 78, 96, 79, 80, 81, 75, 76, 77, 71,
|
||||||
|
72, 73, 82, 83, 86, 127, 116, 117, 183, 184, 185, 186, 187, 188, 189, 190,
|
||||||
|
191, 192, 193, 194, 134, 138, 130, 132, 128, 129, 131, 137, 133, 135, 136, 113,
|
||||||
|
115, 114, UNK, UNK, UNK, 121, UNK, 89, 93, 124, 92, 94, 95, UNK, UNK, UNK,
|
||||||
|
122, 123, 90, 91, 85, UNK, UNK, UNK, UNK, UNK, UNK, UNK, 111, UNK, UNK, UNK,
|
||||||
|
UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK,
|
||||||
|
UNK, UNK, UNK, UNK, UNK, UNK, 179, 180, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK,
|
||||||
|
UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK,
|
||||||
|
UNK, UNK, UNK, UNK, UNK, UNK, UNK, UNK, 111, UNK, UNK, UNK, UNK, UNK, UNK, UNK,
|
||||||
|
29, 42, 56, 125, 97, 54, 100, 126, 164, 166, 165, 163, 161, 115, 114, 113,
|
||||||
|
150, 158, 159, 128, 136, 177, 178, 176, 142, 152, 173, 140, UNK, UNK, UNK, UNK,
|
||||||
|
];
|
||||||
|
// https://cgit.freedesktop.org/xorg/driver/xf86-input-evdev/tree/src/evdev.c#n73
|
||||||
|
const MIN_KEYCODE: u8 = 8;
|
||||||
|
|
||||||
|
// TODO: The combination of these two operations should be precomputed
|
||||||
|
// in a const expr into a single look-up table.
|
||||||
|
xkb::Keycode::new((HID_KEYBOARD[rmk_keycode as usize] + MIN_KEYCODE) as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TryFromKeysym: Sized {
|
||||||
|
fn try_from_keysym(k: Keysym) -> Option<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! declare_consts_for_special_keys {
|
||||||
|
($($_char:literal # $name:ident # $($_qt:ident)|* # $($_winit:ident $(($_pos:ident))?)|* # $($xkb:ident)|*;)*) => {
|
||||||
|
impl TryFromKeysym for Key {
|
||||||
|
fn try_from_keysym(k: Keysym) -> Option<Self> {
|
||||||
|
match k {
|
||||||
|
$(
|
||||||
|
$(Keysym::$xkb => Some(Key::$name),)*
|
||||||
|
)*
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
i_slint_common::for_each_special_keys!(declare_consts_for_special_keys);
|
||||||
151
firmware2/src/logging.rs
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
use core::cell::RefCell;
|
||||||
|
use core::fmt::Write;
|
||||||
|
|
||||||
|
use critical_section::{CriticalSection, Mutex};
|
||||||
|
use esp_hal::Blocking;
|
||||||
|
use esp_hal::uart::UartTx;
|
||||||
|
use log::{LevelFilter, Log};
|
||||||
|
|
||||||
|
pub const LOG_LEVEL_FILTER: LevelFilter = {
|
||||||
|
if let Some(string) = option_env!("ESP_LOG") {
|
||||||
|
if string.eq_ignore_ascii_case("ERROR") {
|
||||||
|
LevelFilter::Error
|
||||||
|
} else if string.eq_ignore_ascii_case("WARN") {
|
||||||
|
LevelFilter::Warn
|
||||||
|
} else if string.eq_ignore_ascii_case("INFO") {
|
||||||
|
LevelFilter::Info
|
||||||
|
} else if string.eq_ignore_ascii_case("DEBUG") {
|
||||||
|
LevelFilter::Debug
|
||||||
|
} else if string.eq_ignore_ascii_case("TRACE") {
|
||||||
|
LevelFilter::Trace
|
||||||
|
} else {
|
||||||
|
panic!("Unknown `ESP_LOG` value. Only `ERROR`, `WARN`, `INFO`, `DEBUG`, `TRACE`, or `OFF` may be used.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LevelFilter::Off
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static ALT_LOGGER_UART: Mutex<RefCell<Option<UartTx<'static, Blocking>>>> =
|
||||||
|
Mutex::new(RefCell::new(None));
|
||||||
|
|
||||||
|
pub fn with_uart_tx<R>(
|
||||||
|
f: impl FnOnce(CriticalSection<'_>, &'_ mut UartTx<'static, Blocking>) -> R,
|
||||||
|
) -> R {
|
||||||
|
critical_section::with(|cs| {
|
||||||
|
let mut uart = ALT_LOGGER_UART.borrow(cs).borrow_mut();
|
||||||
|
let uart = uart.as_mut().unwrap();
|
||||||
|
|
||||||
|
(f)(cs, uart)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AlternativeLogger;
|
||||||
|
|
||||||
|
impl Log for AlternativeLogger {
|
||||||
|
#[allow(unused)]
|
||||||
|
fn enabled(&self, _: &log::Metadata) -> bool {
|
||||||
|
// Filtered by `log` already
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
fn log(&self, record: &log::Record) {
|
||||||
|
with_uart_tx(|cs, uart| {
|
||||||
|
print_log_record(uart, record);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&self) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const RESET: &str = "\u{001B}[0m";
|
||||||
|
const RED: &str = "\u{001B}[31m";
|
||||||
|
const GREEN: &str = "\u{001B}[32m";
|
||||||
|
const YELLOW: &str = "\u{001B}[33m";
|
||||||
|
const BLUE: &str = "\u{001B}[34m";
|
||||||
|
const CYAN: &str = "\u{001B}[35m";
|
||||||
|
|
||||||
|
#[cfg(feature = "rtt-log")]
|
||||||
|
#[allow(unused)]
|
||||||
|
use ::rtt_target::{rprint as print, rprintln as println};
|
||||||
|
|
||||||
|
#[cfg(feature = "alt-log")]
|
||||||
|
#[allow(unused)]
|
||||||
|
macro_rules! println {
|
||||||
|
() => {{
|
||||||
|
do_print(Default::default());
|
||||||
|
}};
|
||||||
|
|
||||||
|
($($arg:tt)*) => {{
|
||||||
|
do_print(::core::format_args!($($arg)*));
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
fn do_print(args: core::fmt::Arguments<'_>) {
|
||||||
|
with_uart_tx(|_, uart| {
|
||||||
|
uart.write_fmt(args).unwrap();
|
||||||
|
uart.write_str("\n").unwrap();
|
||||||
|
uart.flush().unwrap();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_log_record(uart: &mut UartTx<'_, Blocking>, record: &log::Record) {
|
||||||
|
let color = match record.level() {
|
||||||
|
log::Level::Error => RED,
|
||||||
|
log::Level::Warn => YELLOW,
|
||||||
|
log::Level::Info => GREEN,
|
||||||
|
log::Level::Debug => BLUE,
|
||||||
|
log::Level::Trace => CYAN,
|
||||||
|
};
|
||||||
|
let reset = RESET;
|
||||||
|
let args = format_args!(
|
||||||
|
"{}{:>5} - {}{}\n",
|
||||||
|
color,
|
||||||
|
record.level(),
|
||||||
|
record.args(),
|
||||||
|
reset
|
||||||
|
);
|
||||||
|
uart.write_fmt(args).unwrap();
|
||||||
|
uart.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "rtt-log")]
|
||||||
|
use panic_rtt_target as _;
|
||||||
|
|
||||||
|
#[cfg(feature = "alt-log")]
|
||||||
|
#[panic_handler]
|
||||||
|
fn panic_handler(info: &core::panic::PanicInfo) -> ! {
|
||||||
|
use esp_backtrace::Backtrace;
|
||||||
|
|
||||||
|
println!("{RED}");
|
||||||
|
println!("=============== CUSTOM PANIC HANDLER ==============");
|
||||||
|
println!("{info}{RESET}");
|
||||||
|
println!("");
|
||||||
|
println!("Backtrace:");
|
||||||
|
println!("");
|
||||||
|
|
||||||
|
let backtrace = Backtrace::capture();
|
||||||
|
|
||||||
|
for frame in backtrace.frames() {
|
||||||
|
println!("0x{:x}", frame.program_counter());
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "alt-log")]
|
||||||
|
pub fn setup_alternative_logging(
|
||||||
|
alt_uart: UartTx<'static, Blocking>,
|
||||||
|
level_filter: log::LevelFilter,
|
||||||
|
) {
|
||||||
|
critical_section::with(|cs| {
|
||||||
|
*ALT_LOGGER_UART.borrow(cs).borrow_mut() = Some(alt_uart);
|
||||||
|
});
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
log::set_logger_racy(&AlternativeLogger).unwrap();
|
||||||
|
log::set_max_level_racy(level_filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,30 +18,22 @@ extern crate alloc;
|
||||||
|
|
||||||
use core::alloc::Layout;
|
use core::alloc::Layout;
|
||||||
use core::cell::RefCell;
|
use core::cell::RefCell;
|
||||||
use core::fmt::Write;
|
use core::slice;
|
||||||
use core::sync::atomic::{AtomicBool, Ordering};
|
use core::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
use alloc::boxed::Box;
|
use alloc::boxed::Box;
|
||||||
use alloc::collections::vec_deque::VecDeque;
|
|
||||||
use alloc::format;
|
|
||||||
use alloc::string::String;
|
use alloc::string::String;
|
||||||
use alloc::sync::Arc;
|
|
||||||
use alloc::vec;
|
use alloc::vec;
|
||||||
use alloc::vec::Vec;
|
use cfg_if::cfg_if;
|
||||||
use embassy_embedded_hal::adapter::BlockingAsync;
|
|
||||||
use embassy_embedded_hal::flash::partition::Partition;
|
|
||||||
use embassy_executor::Spawner;
|
use embassy_executor::Spawner;
|
||||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||||
use embassy_sync::channel::Channel;
|
use embassy_sync::channel::Channel;
|
||||||
use embassy_sync::mutex::Mutex;
|
|
||||||
use embassy_sync::signal::Signal;
|
use embassy_sync::signal::Signal;
|
||||||
use embassy_time::{Duration, Timer};
|
use embassy_time::{Duration, Instant};
|
||||||
use esp_alloc::{HeapRegion, MemoryCapability};
|
use esp_alloc::{HeapRegion, MemoryCapability};
|
||||||
use esp_bootloader_esp_idf::partitions::PartitionTable;
|
|
||||||
use esp_hal::Blocking;
|
use esp_hal::Blocking;
|
||||||
use esp_hal::clock::CpuClock;
|
use esp_hal::clock::CpuClock;
|
||||||
use esp_hal::dma::{BurstConfig, DmaDescriptor, DmaTxBuf, ExternalBurstConfig};
|
use esp_hal::dma::{BurstConfig, DmaDescriptor, DmaTxBuf, ExternalBurstConfig};
|
||||||
use esp_hal::efuse::Efuse;
|
|
||||||
use esp_hal::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull};
|
use esp_hal::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull};
|
||||||
use esp_hal::i2c::master::{I2c, I2cAddress};
|
use esp_hal::i2c::master::{I2c, I2cAddress};
|
||||||
use esp_hal::interrupt::software::SoftwareInterruptControl;
|
use esp_hal::interrupt::software::SoftwareInterruptControl;
|
||||||
|
|
@ -49,56 +41,47 @@ use esp_hal::lcd_cam::LcdCam;
|
||||||
use esp_hal::lcd_cam::lcd::dpi::Dpi;
|
use esp_hal::lcd_cam::lcd::dpi::Dpi;
|
||||||
use esp_hal::mcpwm::{McPwm, PeripheralClockConfig};
|
use esp_hal::mcpwm::{McPwm, PeripheralClockConfig};
|
||||||
use esp_hal::psram::{FlashFreq, PsramConfig, PsramSize, SpiRamFreq, SpiTimingConfigCoreClock};
|
use esp_hal::psram::{FlashFreq, PsramConfig, PsramSize, SpiRamFreq, SpiTimingConfigCoreClock};
|
||||||
use esp_hal::ram;
|
|
||||||
use esp_hal::rng::TrngSource;
|
|
||||||
use esp_hal::sha::ShaBackend;
|
|
||||||
use esp_hal::system::Stack;
|
use esp_hal::system::Stack;
|
||||||
use esp_hal::timer::timg::TimerGroup;
|
use esp_hal::timer::timg::TimerGroup;
|
||||||
use esp_rtos::embassy::Executor;
|
use esp_rtos::embassy::Executor;
|
||||||
use esp_storage::FlashStorage;
|
use esp_storage::FlashStorage;
|
||||||
use indoc::writedoc;
|
use log::{LevelFilter, error, info, warn};
|
||||||
use itertools::Itertools;
|
|
||||||
use log::{error, info, warn};
|
|
||||||
use rmk::channel::{CONTROLLER_CHANNEL, ControllerSub};
|
use rmk::channel::{CONTROLLER_CHANNEL, ControllerSub};
|
||||||
use rmk::config::{
|
use rmk::config::{BehaviorConfig, PositionalConfig, RmkConfig, StorageConfig, VialConfig};
|
||||||
DeviceConfig, RmkConfig, StorageConfig,
|
|
||||||
VialConfig,
|
|
||||||
};
|
|
||||||
use rmk::controller::{Controller, EventController};
|
use rmk::controller::{Controller, EventController};
|
||||||
use rmk::debounce::default_debouncer::DefaultDebouncer;
|
use rmk::debounce::default_debouncer::DefaultDebouncer;
|
||||||
|
use rmk::descriptor::KeyboardReport;
|
||||||
use rmk::event::ControllerEvent;
|
use rmk::event::ControllerEvent;
|
||||||
use rmk::hid::Report;
|
use rmk::hid::Report;
|
||||||
use rmk::input_device::Runnable;
|
use rmk::input_device::Runnable;
|
||||||
|
use rmk::join_all;
|
||||||
use rmk::keyboard::Keyboard;
|
use rmk::keyboard::Keyboard;
|
||||||
use rmk::storage::async_flash_wrapper;
|
use rmk::storage::async_flash_wrapper;
|
||||||
use rmk::types::action::{Action, KeyAction};
|
use rmk::types::action::{Action, KeyAction};
|
||||||
use rmk::join_all;
|
use rmk::types::keycode::{HidKeyCode, KeyCode};
|
||||||
use rmk::{initialize_keymap_and_storage, run_devices, run_rmk};
|
use rmk::{initialize_keymap_and_storage, run_devices, run_rmk};
|
||||||
|
use slint::ComponentHandle;
|
||||||
use slint::platform::software_renderer::Rgb565Pixel;
|
use slint::platform::software_renderer::Rgb565Pixel;
|
||||||
use static_cell::StaticCell;
|
use static_cell::StaticCell;
|
||||||
|
use ui::AppWindow;
|
||||||
|
use xkbcommon::xkb::{self, FeedResult, KeyDirection, Keysym, Status};
|
||||||
use {esp_alloc as _, esp_backtrace as _};
|
use {esp_alloc as _, esp_backtrace as _};
|
||||||
|
|
||||||
|
use crate::keymap::{KEY_MESSAGE_CHANNEL, create_hid_report_interceptor};
|
||||||
|
use crate::logging::LOG_LEVEL_FILTER;
|
||||||
use crate::matrix::IoeMatrix;
|
use crate::matrix::IoeMatrix;
|
||||||
use crate::peripherals::st7701s::St7701s;
|
use crate::peripherals::st7701s::St7701s;
|
||||||
use crate::proxy::create_hid_report_interceptor;
|
|
||||||
use crate::ui::backend::{FramebufferPtr, SlintBackend};
|
use crate::ui::backend::{FramebufferPtr, SlintBackend};
|
||||||
use crate::vial::{
|
use crate::vial::{CustomKeycodes, VIAL_KEYBOARD_DEF, VIAL_KEYBOARD_ID};
|
||||||
CustomKeycodes, VIAL_KEYBOARD_DEF, VIAL_KEYBOARD_ID, VIAL_KEYBOARD_NAME, VIAL_PRODUCT_ID,
|
|
||||||
VIAL_VENDOR_ID,
|
|
||||||
};
|
|
||||||
|
|
||||||
mutually_exclusive_features::none_or_one_of!["usb-log", "alt-log", "rtt-log"];
|
mutually_exclusive_features::none_or_one_of!["usb-log", "alt-log", "rtt-log"];
|
||||||
|
|
||||||
mod config;
|
|
||||||
mod crypto;
|
|
||||||
mod db;
|
|
||||||
mod ffi;
|
mod ffi;
|
||||||
|
mod keymap;
|
||||||
mod logging;
|
mod logging;
|
||||||
mod matrix;
|
mod matrix;
|
||||||
mod peripherals;
|
mod peripherals;
|
||||||
mod proxy;
|
|
||||||
mod ui;
|
mod ui;
|
||||||
mod util;
|
|
||||||
mod vial;
|
mod vial;
|
||||||
|
|
||||||
#[cfg(feature = "alt-log")]
|
#[cfg(feature = "alt-log")]
|
||||||
|
|
@ -108,15 +91,6 @@ mod console;
|
||||||
// For more information see: <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/app_image_format.html#application-description>
|
// For more information see: <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/app_image_format.html#application-description>
|
||||||
esp_bootloader_esp_idf::esp_app_desc!();
|
esp_bootloader_esp_idf::esp_app_desc!();
|
||||||
|
|
||||||
// Memory allocation regions.
|
|
||||||
// These can be debugged using `xtensa-esp32s3-elf-size -A <path-to-binary>`.
|
|
||||||
// A panic such as `memory allocation of 3740121773 bytes failed` is caused by a heap overflow. The size is `DEEDBAAD` in hex.
|
|
||||||
|
|
||||||
/// Total heap size
|
|
||||||
const HEAP_SIZE: usize = 112 * 1024;
|
|
||||||
/// Size of the app core's stack
|
|
||||||
const STACK_SIZE_CORE_APP: usize = 80 * 1024;
|
|
||||||
|
|
||||||
// const FRAME_DURATION_MIN: Duration = Duration::from_millis(40); // 25 FPS
|
// const FRAME_DURATION_MIN: Duration = Duration::from_millis(40); // 25 FPS
|
||||||
const FRAME_DURATION_MIN: Duration = Duration::from_millis(100); // 10 FPS
|
const FRAME_DURATION_MIN: Duration = Duration::from_millis(100); // 10 FPS
|
||||||
|
|
||||||
|
|
@ -133,6 +107,15 @@ static SIGNAL_UI_RENDER: Signal<CriticalSectionRawMutex, ()> = Signal::new();
|
||||||
|
|
||||||
#[esp_rtos::main]
|
#[esp_rtos::main]
|
||||||
async fn main(_spawner: Spawner) {
|
async fn main(_spawner: Spawner) {
|
||||||
|
#[cfg(feature = "usb-log")]
|
||||||
|
{
|
||||||
|
esp_println::logger::init_logger(LOG_LEVEL_FILTER);
|
||||||
|
info!("Logger initialized!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "rtt-log")]
|
||||||
|
rtt_target::rtt_init_log!(LOG_LEVEL_FILTER);
|
||||||
|
|
||||||
let config = esp_hal::Config::default()
|
let config = esp_hal::Config::default()
|
||||||
.with_cpu_clock(CpuClock::max())
|
.with_cpu_clock(CpuClock::max())
|
||||||
.with_psram(PsramConfig {
|
.with_psram(PsramConfig {
|
||||||
|
|
@ -142,24 +125,30 @@ async fn main(_spawner: Spawner) {
|
||||||
ram_frequency: SpiRamFreq::Freq80m,
|
ram_frequency: SpiRamFreq::Freq80m,
|
||||||
});
|
});
|
||||||
let peripherals: esp_hal::peripherals::Peripherals = esp_hal::init(config);
|
let peripherals: esp_hal::peripherals::Peripherals = esp_hal::init(config);
|
||||||
|
info!("System initialized!");
|
||||||
|
|
||||||
#[cfg(feature = "usb-log")]
|
|
||||||
let console_task = logging::usb::setup_logging();
|
|
||||||
#[cfg(feature = "alt-log")]
|
#[cfg(feature = "alt-log")]
|
||||||
let console_task =
|
let alt_uart_rx_task = {
|
||||||
logging::uart::setup_logging(peripherals.UART2, peripherals.GPIO12, peripherals.GPIO5);
|
use esp_hal::uart::Uart;
|
||||||
#[cfg(feature = "rtt-log")]
|
|
||||||
let console_task = logging::rtt::setup_logging();
|
|
||||||
|
|
||||||
// Use the internal DRAM as the heap.
|
let (uart_rx, uart_tx) = Uart::new(peripherals.UART2, Default::default())
|
||||||
// Memory reclaimed from the esp-idf bootloader.
|
.unwrap()
|
||||||
const HEAP_SIZE_RECLAIMED: usize = const {
|
.with_tx(peripherals.GPIO12)
|
||||||
let range = esp_metadata_generated::memory_range!("DRAM2_UNINIT");
|
.with_rx(peripherals.GPIO5)
|
||||||
range.end - range.start
|
.split();
|
||||||
|
logging::setup_alternative_logging(uart_tx, LOG_LEVEL_FILTER);
|
||||||
|
info!("Logger initialized!");
|
||||||
|
console::run_console(uart_rx.into_async())
|
||||||
};
|
};
|
||||||
|
|
||||||
esp_alloc::heap_allocator!(#[ram(reclaimed)] size: HEAP_SIZE_RECLAIMED);
|
#[cfg(not(feature = "alt-log"))]
|
||||||
esp_alloc::heap_allocator!(size: HEAP_SIZE - HEAP_SIZE_RECLAIMED);
|
let alt_uart_rx_task = async {};
|
||||||
|
|
||||||
|
// Use the internal DRAM as the heap.
|
||||||
|
// TODO: Can we combine the regular link section with dram2?
|
||||||
|
// esp_alloc::heap_allocator!(size: 80 * 1024);
|
||||||
|
// esp_alloc::heap_allocator!(#[unsafe(link_section = ".dram2_uninit")] size: 72 * 1024);
|
||||||
|
esp_alloc::heap_allocator!(#[unsafe(link_section = ".dram2_uninit")] size: 64 * 1024);
|
||||||
info!("Heap initialized! {:#?}", esp_alloc::HEAP.stats());
|
info!("Heap initialized! {:#?}", esp_alloc::HEAP.stats());
|
||||||
|
|
||||||
// Initialize the PSRAM allocator.
|
// Initialize the PSRAM allocator.
|
||||||
|
|
@ -172,12 +161,10 @@ async fn main(_spawner: Spawner) {
|
||||||
MemoryCapability::External.into(),
|
MemoryCapability::External.into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
info!(
|
|
||||||
"PSRAM allocator initialized with capacity of {} MiB!",
|
|
||||||
psram_size / 1024 / 1024
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
info!("PSRAM allocator initialized!");
|
||||||
|
|
||||||
// let mut io = Io::new(peripherals.IO_MUX);
|
// let mut io = Io::new(peripherals.IO_MUX);
|
||||||
// io.set_interrupt_handler(interrupt_handler);
|
// io.set_interrupt_handler(interrupt_handler);
|
||||||
|
|
||||||
|
|
@ -200,9 +187,6 @@ async fn main(_spawner: Spawner) {
|
||||||
let mut _pwm = McPwm::new(peripherals.MCPWM0, PeripheralClockConfig::with_prescaler(1));
|
let mut _pwm = McPwm::new(peripherals.MCPWM0, PeripheralClockConfig::with_prescaler(1));
|
||||||
let mut _pwm_pin = Output::new(peripherals.GPIO21, Level::High, OutputConfig::default());
|
let mut _pwm_pin = Output::new(peripherals.GPIO21, Level::High, OutputConfig::default());
|
||||||
|
|
||||||
let mut sha_backend = ShaBackend::new(peripherals.SHA);
|
|
||||||
let _sha_driver_handle = sha_backend.start();
|
|
||||||
|
|
||||||
let timg0 = TimerGroup::new(peripherals.TIMG0);
|
let timg0 = TimerGroup::new(peripherals.TIMG0);
|
||||||
let software_interrupt = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
|
let software_interrupt = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
|
||||||
esp_rtos::start(
|
esp_rtos::start(
|
||||||
|
|
@ -211,16 +195,16 @@ async fn main(_spawner: Spawner) {
|
||||||
|
|
||||||
info!("ESP-RTOS started!");
|
info!("ESP-RTOS started!");
|
||||||
|
|
||||||
// Enable the TRNG source, so `Trng` can be constructed.
|
|
||||||
let _trng_source = TrngSource::new(peripherals.RNG, peripherals.ADC1);
|
|
||||||
|
|
||||||
#[cfg(feature = "ble")]
|
#[cfg(feature = "ble")]
|
||||||
let mut host_resources = rmk::HostResources::new();
|
let mut host_resources = rmk::HostResources::new();
|
||||||
#[cfg(feature = "ble")]
|
#[cfg(feature = "ble")]
|
||||||
let stack = {
|
let stack = {
|
||||||
|
// Enable the TRNG source, so `Trng` can be constructed.
|
||||||
use bt_hci::controller::ExternalController;
|
use bt_hci::controller::ExternalController;
|
||||||
|
use esp_hal::rng::TrngSource;
|
||||||
use esp_radio::{Controller as RadioController, ble::controller::BleConnector};
|
use esp_radio::{Controller as RadioController, ble::controller::BleConnector};
|
||||||
|
|
||||||
|
let _trng_source = TrngSource::new(peripherals.RNG, peripherals.ADC1);
|
||||||
let mut rng = esp_hal::rng::Trng::try_new().unwrap();
|
let mut rng = esp_hal::rng::Trng::try_new().unwrap();
|
||||||
static RADIO: StaticCell<RadioController<'static>> = StaticCell::new();
|
static RADIO: StaticCell<RadioController<'static>> = StaticCell::new();
|
||||||
let radio = RADIO.init(esp_radio::init().unwrap());
|
let radio = RADIO.init(esp_radio::init().unwrap());
|
||||||
|
|
@ -240,15 +224,15 @@ async fn main(_spawner: Spawner) {
|
||||||
// Initialize USB
|
// Initialize USB
|
||||||
#[cfg(not(feature = "no-usb"))]
|
#[cfg(not(feature = "no-usb"))]
|
||||||
let usb_driver = {
|
let usb_driver = {
|
||||||
|
use core::ptr::addr_of_mut;
|
||||||
use esp_hal::otg_fs::Usb;
|
use esp_hal::otg_fs::Usb;
|
||||||
use esp_hal::otg_fs::asynch::{Config, Driver};
|
use esp_hal::otg_fs::asynch::{Config, Driver};
|
||||||
|
|
||||||
static EP_MEMORY: StaticCell<[u8; 1024]> = StaticCell::new();
|
static mut EP_MEMORY: [u8; 1024] = [0; 1024];
|
||||||
let ep_memory = EP_MEMORY.init_with(|| [0_u8; _]);
|
|
||||||
let usb = Usb::new(peripherals.USB0, peripherals.GPIO20, peripherals.GPIO19);
|
let usb = Usb::new(peripherals.USB0, peripherals.GPIO20, peripherals.GPIO19);
|
||||||
// Create the driver, from the HAL.
|
// Create the driver, from the HAL.
|
||||||
let config = Config::default();
|
let config = Config::default();
|
||||||
let driver = Driver::new(usb, ep_memory, config);
|
let driver = Driver::new(usb, unsafe { &mut *addr_of_mut!(EP_MEMORY) }, config);
|
||||||
|
|
||||||
info!("USB driver for RMK built!");
|
info!("USB driver for RMK built!");
|
||||||
|
|
||||||
|
|
@ -256,20 +240,7 @@ async fn main(_spawner: Spawner) {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize the flash
|
// Initialize the flash
|
||||||
static PARTITION_TABLE_BUFFER: StaticCell<Vec<u8, &'static esp_alloc::EspHeap>> =
|
let flash = FlashStorage::new(peripherals.FLASH)
|
||||||
StaticCell::new();
|
|
||||||
let partition_table_buffer = PARTITION_TABLE_BUFFER.init_with(|| {
|
|
||||||
let mut buffer = Vec::<u8, _>::new_in(&PSRAM_ALLOCATOR);
|
|
||||||
buffer.resize(1024, 0_u8);
|
|
||||||
buffer
|
|
||||||
});
|
|
||||||
|
|
||||||
static FLASH: StaticCell<(
|
|
||||||
Mutex<CriticalSectionRawMutex, BlockingAsync<FlashStorage>>,
|
|
||||||
PartitionTable<'static>,
|
|
||||||
)> = StaticCell::new();
|
|
||||||
let (flash, partition_table) = FLASH.init_with(|| {
|
|
||||||
let mut flash = FlashStorage::new(peripherals.FLASH)
|
|
||||||
// Flash memory may not be written to while another core is executing from it.
|
// Flash memory may not be written to while another core is executing from it.
|
||||||
// By default, `FlashStorage` is configured to abort the operation and log an error message.
|
// By default, `FlashStorage` is configured to abort the operation and log an error message.
|
||||||
// However, it can also be configured to auto-park the other core, such that writing to
|
// However, it can also be configured to auto-park the other core, such that writing to
|
||||||
|
|
@ -278,68 +249,7 @@ async fn main(_spawner: Spawner) {
|
||||||
// to avoid having to park the other core, which could result in better performance.
|
// to avoid having to park the other core, which could result in better performance.
|
||||||
// Invalid configuration would then present itself as freezing/UB.
|
// Invalid configuration would then present itself as freezing/UB.
|
||||||
.multicore_auto_park();
|
.multicore_auto_park();
|
||||||
let partition_table = {
|
let flash = async_flash_wrapper(flash);
|
||||||
esp_bootloader_esp_idf::partitions::read_partition_table(
|
|
||||||
&mut flash,
|
|
||||||
partition_table_buffer,
|
|
||||||
)
|
|
||||||
.expect("Failed to read the partition table.")
|
|
||||||
};
|
|
||||||
|
|
||||||
(
|
|
||||||
Mutex::<CriticalSectionRawMutex, _>::new(async_flash_wrapper(flash)),
|
|
||||||
partition_table,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut buffer = String::new();
|
|
||||||
|
|
||||||
writeln!(buffer, "Partition table:").unwrap();
|
|
||||||
|
|
||||||
for (index, partition) in partition_table.iter().enumerate() {
|
|
||||||
writedoc!(
|
|
||||||
buffer,
|
|
||||||
"
|
|
||||||
Partition #{index} {label:?}:
|
|
||||||
offset: 0x{offset:x}
|
|
||||||
length: 0x{len:x}
|
|
||||||
type: 0x{type:?}
|
|
||||||
read only: {read_only}
|
|
||||||
encrypted: {encrypted}
|
|
||||||
magic: {magic}
|
|
||||||
",
|
|
||||||
label = partition.label_as_str(),
|
|
||||||
offset = partition.offset(),
|
|
||||||
len = partition.len(),
|
|
||||||
type = partition.partition_type(),
|
|
||||||
read_only = partition.is_read_only(),
|
|
||||||
encrypted = partition.is_encrypted(),
|
|
||||||
magic = partition.magic(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("{}", buffer);
|
|
||||||
}
|
|
||||||
let flash_part_info_rmk = partition_table
|
|
||||||
.iter()
|
|
||||||
.find(|partition| partition.label_as_str() == "rmk")
|
|
||||||
.expect("No \"rmk\" partition found. Make sure to use the custom partition-table.csv when flashing.");
|
|
||||||
let flash_part_info_acid = partition_table
|
|
||||||
.iter()
|
|
||||||
.find(|partition| partition.label_as_str() == "acid")
|
|
||||||
.expect("No \"acid\" partition found. Make sure to use the custom partition-table.csv when flashing.");
|
|
||||||
let flash_part_rmk = Partition::new(
|
|
||||||
flash,
|
|
||||||
flash_part_info_rmk.offset(),
|
|
||||||
flash_part_info_rmk.len(),
|
|
||||||
);
|
|
||||||
let flash_part_acid = Partition::new(
|
|
||||||
flash,
|
|
||||||
flash_part_info_acid.offset(),
|
|
||||||
flash_part_info_acid.len(),
|
|
||||||
);
|
|
||||||
|
|
||||||
info!("Flash memory configured!");
|
info!("Flash memory configured!");
|
||||||
|
|
||||||
|
|
@ -385,55 +295,27 @@ async fn main(_spawner: Spawner) {
|
||||||
// RMK config
|
// RMK config
|
||||||
let vial_config = VialConfig::new(VIAL_KEYBOARD_ID, VIAL_KEYBOARD_DEF, &[(0, 0), (1, 1)]);
|
let vial_config = VialConfig::new(VIAL_KEYBOARD_ID, VIAL_KEYBOARD_DEF, &[(0, 0), (1, 1)]);
|
||||||
let storage_config = StorageConfig {
|
let storage_config = StorageConfig {
|
||||||
start_addr: 0,
|
start_addr: 0x3f0000,
|
||||||
num_sectors: {
|
num_sectors: 16,
|
||||||
assert!(
|
|
||||||
flash_part_info_rmk.len() % FlashStorage::SECTOR_SIZE == 0,
|
|
||||||
"The size of the RMK partition must be a multiple of {} bytes. Current size: {}",
|
|
||||||
FlashStorage::SECTOR_SIZE,
|
|
||||||
flash_part_info_rmk.len()
|
|
||||||
);
|
|
||||||
(flash_part_info_rmk.len() / FlashStorage::SECTOR_SIZE) as u8
|
|
||||||
},
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
// Retrieve the hardware-unique MAC address.
|
|
||||||
let mac_address = Efuse::read_base_mac_address();
|
|
||||||
static SERIAL_NUMBER: StaticCell<Box<str>> = StaticCell::new();
|
|
||||||
let serial_number = SERIAL_NUMBER.init_with(|| {
|
|
||||||
/// A magic prefix string that is required for the device to be recognized by the Vial GUI.
|
|
||||||
const VIAL_SERIAL_PREFIX: &str = "vial:f64c2b3c";
|
|
||||||
format!(
|
|
||||||
"{VIAL_SERIAL_PREFIX}:acid:{:02x}",
|
|
||||||
mac_address.iter().format("")
|
|
||||||
)
|
|
||||||
.into_boxed_str()
|
|
||||||
});
|
|
||||||
let device_config = DeviceConfig {
|
|
||||||
vid: VIAL_VENDOR_ID,
|
|
||||||
pid: VIAL_PRODUCT_ID,
|
|
||||||
manufacturer: "",
|
|
||||||
product_name: VIAL_KEYBOARD_NAME,
|
|
||||||
serial_number,
|
|
||||||
};
|
|
||||||
info!("RMK Device Config: {device_config:#04x?}");
|
|
||||||
let rmk_config = RmkConfig {
|
let rmk_config = RmkConfig {
|
||||||
device_config,
|
|
||||||
vial_config,
|
vial_config,
|
||||||
storage_config,
|
storage_config,
|
||||||
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialze keyboard stuffs
|
// Initialze keyboard stuffs
|
||||||
// Initialize the storage and keymap
|
// Initialize the storage and keymap
|
||||||
let mut default_keymap = config::get_default_keymap();
|
let mut default_keymap = keymap::get_default_keymap();
|
||||||
let mut behavior_config = config::get_behavior_config();
|
let mut behavior_config = BehaviorConfig::default();
|
||||||
let mut positional_config = config::get_positional_config();
|
let mut per_key_config = PositionalConfig::default();
|
||||||
let (keymap, mut storage) = initialize_keymap_and_storage(
|
let (keymap, mut storage) = initialize_keymap_and_storage(
|
||||||
&mut default_keymap,
|
&mut default_keymap,
|
||||||
flash_part_rmk,
|
flash,
|
||||||
&storage_config,
|
&storage_config,
|
||||||
&mut behavior_config,
|
&mut behavior_config,
|
||||||
&mut positional_config,
|
&mut per_key_config,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|
@ -473,7 +355,7 @@ async fn main(_spawner: Spawner) {
|
||||||
let window_size = [framebuffer.height, framebuffer.width];
|
let window_size = [framebuffer.height, framebuffer.width];
|
||||||
let framebuffer_ptr = FramebufferPtr(framebuffer.as_target_pixels() as _);
|
let framebuffer_ptr = FramebufferPtr(framebuffer.as_target_pixels() as _);
|
||||||
|
|
||||||
static SECOND_CORE_STACK: StaticCell<Stack<STACK_SIZE_CORE_APP>> = StaticCell::new();
|
static SECOND_CORE_STACK: StaticCell<Stack<{ 8192 * 2 }>> = StaticCell::new();
|
||||||
let second_core_stack = SECOND_CORE_STACK.init(Stack::new());
|
let second_core_stack = SECOND_CORE_STACK.init(Stack::new());
|
||||||
esp_rtos::start_second_core(
|
esp_rtos::start_second_core(
|
||||||
peripherals.CPU_CTRL,
|
peripherals.CPU_CTRL,
|
||||||
|
|
@ -495,10 +377,8 @@ async fn main(_spawner: Spawner) {
|
||||||
window_size,
|
window_size,
|
||||||
window: RefCell::new(None),
|
window: RefCell::new(None),
|
||||||
framebuffer: framebuffer_ptr,
|
framebuffer: framebuffer_ptr,
|
||||||
quit_event_loop: Default::default(),
|
|
||||||
events: Arc::new(critical_section::Mutex::new(RefCell::new(VecDeque::new()))),
|
|
||||||
};
|
};
|
||||||
spawner.must_spawn(ui::run_renderer_task(slint_backend, flash_part_acid));
|
spawner.must_spawn(ui::run_renderer_task(slint_backend));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -511,7 +391,6 @@ async fn main(_spawner: Spawner) {
|
||||||
|
|
||||||
// TODO: Probably want to select! instead and re-try.
|
// TODO: Probably want to select! instead and re-try.
|
||||||
join_all![
|
join_all![
|
||||||
run_alloc_stats_reporter(),
|
|
||||||
// We currently send the framebuffer data using the main core, which does not seem to slow
|
// We currently send the framebuffer data using the main core, which does not seem to slow
|
||||||
// down the rest of the tasks too much.
|
// down the rest of the tasks too much.
|
||||||
run_lcd(st7701s, framebuffer),
|
run_lcd(st7701s, framebuffer),
|
||||||
|
|
@ -530,39 +409,11 @@ async fn main(_spawner: Spawner) {
|
||||||
),
|
),
|
||||||
create_hid_report_interceptor(),
|
create_hid_report_interceptor(),
|
||||||
user_controller.event_loop(),
|
user_controller.event_loop(),
|
||||||
console_task
|
alt_uart_rx_task
|
||||||
]
|
]
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_alloc_stats_reporter() {
|
|
||||||
let mut psram_used_prev = 0;
|
|
||||||
let mut heap_used_prev = 0;
|
|
||||||
loop {
|
|
||||||
let psram_stats = PSRAM_ALLOCATOR.stats();
|
|
||||||
let heap_stats = esp_alloc::HEAP.stats();
|
|
||||||
if psram_stats.current_usage != psram_used_prev {
|
|
||||||
let difference = psram_stats.current_usage as isize - psram_used_prev as isize;
|
|
||||||
psram_used_prev = psram_stats.current_usage;
|
|
||||||
warn!(
|
|
||||||
"PSRAM usage changed: {}{}\n{psram_stats}",
|
|
||||||
if difference < 0 { '-' } else { '+' },
|
|
||||||
difference.abs()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if heap_stats.current_usage != heap_used_prev {
|
|
||||||
let difference = heap_stats.current_usage as isize - heap_used_prev as isize;
|
|
||||||
heap_used_prev = heap_stats.current_usage;
|
|
||||||
warn!(
|
|
||||||
"HEAP usage changed: {}{}\n{heap_stats}",
|
|
||||||
if difference < 0 { '-' } else { '+' },
|
|
||||||
difference.abs()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Timer::after_secs(1).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UserController {
|
struct UserController {
|
||||||
sub: ControllerSub,
|
sub: ControllerSub,
|
||||||
}
|
}
|
||||||
|
|
@ -579,11 +430,12 @@ impl Controller for UserController {
|
||||||
type Event = ControllerEvent;
|
type Event = ControllerEvent;
|
||||||
|
|
||||||
async fn process_event(&mut self, event: Self::Event) {
|
async fn process_event(&mut self, event: Self::Event) {
|
||||||
if let ControllerEvent::Key(keyboard_event, KeyAction::Single(Action::User(user_key_index))) =
|
if let ControllerEvent::Key(
|
||||||
event
|
keyboard_event,
|
||||||
&& user_key_index == CustomKeycodes::FOCUS_LCD as u8
|
KeyAction::Single(Action::User(user_key_index)),
|
||||||
&& keyboard_event.pressed
|
) = event
|
||||||
{
|
{
|
||||||
|
if user_key_index == CustomKeycodes::FOCUS_LCD as u8 && keyboard_event.pressed {
|
||||||
let enabled = !LCD_ENABLED.fetch_xor(true, Ordering::SeqCst);
|
let enabled = !LCD_ENABLED.fetch_xor(true, Ordering::SeqCst);
|
||||||
|
|
||||||
match enabled {
|
match enabled {
|
||||||
|
|
@ -594,7 +446,9 @@ impl Controller for UserController {
|
||||||
}
|
}
|
||||||
true => {
|
true => {
|
||||||
info!("Enabling LCD.");
|
info!("Enabling LCD.");
|
||||||
*rmk::channel::KEYBOARD_REPORT_SENDER.write().await = &KEYBOARD_REPORT_PROXY;
|
*rmk::channel::KEYBOARD_REPORT_SENDER.write().await =
|
||||||
|
&KEYBOARD_REPORT_PROXY;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -667,7 +521,7 @@ async fn run_lcd(mut st7701s: St7701s<'static, Blocking>, framebuffer: &'static
|
||||||
|
|
||||||
// TODO: Use bounce buffers:
|
// TODO: Use bounce buffers:
|
||||||
// https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32s3/api-reference/peripherals/lcd.html#bounce-buffer-with-single-psram-frame-buffer
|
// https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32s3/api-reference/peripherals/lcd.html#bounce-buffer-with-single-psram-frame-buffer
|
||||||
// This can be implemented as a `DmaTxBuffer`.
|
// They need to be implemented in esp-hal.
|
||||||
let transfer = match st7701s.dpi.send(false, framebuffer.dma_buf.take().unwrap()) {
|
let transfer = match st7701s.dpi.send(false, framebuffer.dma_buf.take().unwrap()) {
|
||||||
Err((error, result_dpi, result_dma_buf)) => {
|
Err((error, result_dpi, result_dma_buf)) => {
|
||||||
error!(
|
error!(
|
||||||
|
|
@ -13,8 +13,6 @@ use rmk::{
|
||||||
matrix::{KeyState, MatrixTrait},
|
matrix::{KeyState, MatrixTrait},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::config::{MATRIX_AREA, MATRIX_COLS, MATRIX_ROWS};
|
|
||||||
|
|
||||||
pub struct RaiiGuard<F: FnOnce()> {
|
pub struct RaiiGuard<F: FnOnce()> {
|
||||||
on_drop: Option<F>,
|
on_drop: Option<F>,
|
||||||
}
|
}
|
||||||
|
|
@ -35,6 +33,10 @@ impl<F: FnOnce()> Drop for RaiiGuard<F> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const MATRIX_ROWS: usize = 5;
|
||||||
|
pub const MATRIX_COLS: usize = 12;
|
||||||
|
pub const MATRIX_AREA: usize = MATRIX_ROWS * MATRIX_COLS;
|
||||||
|
|
||||||
/// IO Expander Matrix
|
/// IO Expander Matrix
|
||||||
pub struct IoeMatrix<D>
|
pub struct IoeMatrix<D>
|
||||||
where
|
where
|
||||||