Compare commits
44 commits
libxkbcomm
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea58ef0c8e | ||
|
|
707c994a76 | ||
|
|
5004e8dfdf | ||
|
|
8e304540ea | ||
|
|
ab506de76a | ||
|
|
a09da0c8a7 | ||
|
|
3b364c64c2 | ||
|
|
1e2d43a628 | ||
|
|
4a5ada0bb0 | ||
|
|
d4c8d69cf3 | ||
|
|
40b9b5d278 | ||
|
|
3ac1656d33 | ||
|
|
b6d9a71b59 | ||
|
|
c2e3f1bec3 | ||
|
|
f8ef06ee0c | ||
|
|
5592708271 | ||
|
|
2b8dfa7b44 | ||
|
|
3947215a23 | ||
|
|
5f34f078db | ||
|
|
d4aad0e8cd | ||
|
|
3b24825677 | ||
|
|
8426852d7c | ||
|
|
9c2a614aff | ||
|
|
b33f4852b2 | ||
|
|
0cb6209d4b | ||
|
|
a3a95b179b | ||
|
|
9aa5430851 | ||
|
|
bbbaea803b | ||
|
|
6cd7b32bee | ||
|
|
2a5779ffcf | ||
|
|
299a1195f1 | ||
|
|
7fca722f24 | ||
|
|
16ed51b19e | ||
|
|
810f21827b | ||
|
|
ee17cc9f57 | ||
|
|
d1dd4abc06 | ||
|
|
47e6c890ca | ||
|
|
a5a5ee9330 | ||
|
|
dbdfa8ae44 | ||
|
|
3c695be996 | ||
|
|
b5535d6f52 | ||
|
|
35c017535e | ||
|
|
c98acc4da4 | ||
|
|
24daa0ad29 |
8
.gitmodules
vendored
|
|
@ -1,3 +1,9 @@
|
|||
[submodule "firmware2/libxkbcommon"]
|
||||
path = firmware2/libxkbcommon
|
||||
path = firmware/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
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
[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
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Convert to LF line endings on checkout.
|
||||
*.sh text eol=lf
|
||||
42
firmware/.github/workflows/rust_ci.yml
vendored
|
|
@ -1,42 +0,0 @@
|
|||
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,19 +1,2 @@
|
|||
# will have compiled files and executables
|
||||
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/
|
||||
/.cargo
|
||||
!/acid-firmware/partition-table.csv
|
||||
|
|
|
|||
|
|
@ -9,9 +9,17 @@
|
|||
"chip": "esp32s3",
|
||||
"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",
|
||||
|
|
@ -21,9 +29,17 @@
|
|||
"chip": "esp32s3",
|
||||
"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",
|
||||
|
|
@ -32,14 +48,25 @@
|
|||
"request": "launch",
|
||||
"flashingConfig": {
|
||||
"flashingEnabled": true,
|
||||
"formatOptions": {
|
||||
"idf_partition_table": "partition-table.csv"
|
||||
}
|
||||
},
|
||||
"probe": "303a:1001",
|
||||
"chip": "esp32s3",
|
||||
"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",
|
||||
|
|
@ -48,14 +75,25 @@
|
|||
"request": "launch",
|
||||
"flashingConfig": {
|
||||
"flashingEnabled": true,
|
||||
"formatOptions": {
|
||||
"idf_partition_table": "partition-table.csv"
|
||||
}
|
||||
},
|
||||
"probe": "303a:1001",
|
||||
"chip": "esp32s3",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
25
firmware/.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,15 @@
|
|||
{
|
||||
"label": "rust: cargo build",
|
||||
"type": "cargo",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/acid-firmware",
|
||||
"env": {
|
||||
"ESP_LOG": "info"
|
||||
}
|
||||
},
|
||||
"command": "build",
|
||||
"args": [
|
||||
"--no-default-features", "--features=probe,info"
|
||||
"--no-default-features", "--features=probe"
|
||||
],
|
||||
"problemMatcher": [
|
||||
"$rustc"
|
||||
|
|
@ -19,9 +25,15 @@
|
|||
{
|
||||
"label": "rust: cargo build --release",
|
||||
"type": "cargo",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/acid-firmware",
|
||||
"env": {
|
||||
"ESP_LOG": "info"
|
||||
}
|
||||
},
|
||||
"command": "build",
|
||||
"args": [
|
||||
"--release", "--no-default-features", "--features=probe,info"
|
||||
"--release", "--no-default-features", "--features=probe"
|
||||
],
|
||||
"problemMatcher": [
|
||||
"$rustc"
|
||||
14
firmware/.zed/debug.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// 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": [],
|
||||
},
|
||||
]
|
||||
39
firmware/.zed/settings.json
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"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"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
7806
firmware/Cargo.lock
generated
|
|
@ -1,56 +1,26 @@
|
|||
[package]
|
||||
edition = "2021"
|
||||
name = "acid-firmware"
|
||||
rust-version = "1.86"
|
||||
version = "0.1.0"
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["acid-firmware", "password-hash"]
|
||||
default-members = ["acid-firmware"]
|
||||
|
||||
[[bin]]
|
||||
name = "acid-firmware"
|
||||
path = "./src/bin/main.rs"
|
||||
[workspace.dependencies]
|
||||
spectre-api-sys = { git = "https://github.com/Limeth/spectre-api-sys", rev = "9e844eb056c3dfee8286ac21ec40fa689a8b8aa2" }
|
||||
|
||||
[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]
|
||||
# Rust debug is too slow.
|
||||
# For debug builds always builds with some optimization
|
||||
opt-level = "s"
|
||||
lto = 'thin'
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1 # LLVM can perform better optimizations using a single thread
|
||||
debug = 2
|
||||
codegen-units = 1 # LLVM can perform better optimizations using a single thread
|
||||
debug = 2
|
||||
debug-assertions = false
|
||||
incremental = false
|
||||
lto = 'fat'
|
||||
opt-level = 's'
|
||||
overflow-checks = false
|
||||
incremental = false
|
||||
lto = 'thin'
|
||||
opt-level = 3
|
||||
overflow-checks = false
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
# 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?
|
||||
40
firmware/acid-firmware/.cargo/config.toml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[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"]
|
||||
128
firmware/acid-firmware/Cargo.toml
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
[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
|
||||
|
|
@ -43,14 +43,25 @@ This replaces the debugging symbols with paths that will be available when debug
|
|||
|
||||
Then compile the firmware with:
|
||||
```ps1
|
||||
$env:LIBXKBCOMMON_BUILD_DIR="libxkbcommon/build-debug"; cargo build
|
||||
$env:XKBCOMMON_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
|
||||
|
||||
To generate an English (US) keymap, the following command may be used:
|
||||
|
||||
`xkbcli compile-keymap --include [path-to-xkb-directory] --layout us >my_compose.txt`
|
||||
`xkbcli compile-keymap --include [path-to-xkb-directory] --layout us >my_keymap.xkb`
|
||||
|
||||
Substitute `us` for any other 2-letter country code.
|
||||
|
||||
|
|
@ -69,7 +80,18 @@ Use libxkbcommon's `xkbcli` to compile a standalone compose file:
|
|||
|
||||
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 [`libx11` git repository](https://gitlab.freedesktop.org/xorg/lib/libx11/-/tree/master). There's an button to download it as a ZIP archive.
|
||||
* 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.
|
||||
|
||||
### 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
|
||||
|
||||
246
firmware/acid-firmware/build.rs
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
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();
|
||||
}
|
||||
1914
firmware/acid-firmware/keymaps/cz_coder.xkb
Normal file
6
firmware/acid-firmware/partition-table.csv
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# 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,
|
||||
|
130
firmware/acid-firmware/src/config.rs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
|
||||
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,12 +1,14 @@
|
|||
|
||||
use core::fmt::Write;
|
||||
|
||||
use embedded_cli::cli::CliBuilder;
|
||||
use embedded_cli::Command;
|
||||
use esp_hal::{Async, uart::{TxError, UartRx}};
|
||||
use log::{info, error};
|
||||
use embedded_cli::cli::CliBuilder;
|
||||
use esp_hal::{
|
||||
Async,
|
||||
uart::{TxError, UartRx},
|
||||
};
|
||||
use log::{error, info};
|
||||
|
||||
use crate::logging::with_uart_tx;
|
||||
use crate::logging::uart::with_uart_tx;
|
||||
|
||||
struct Writer;
|
||||
|
||||
|
|
@ -16,26 +18,21 @@ impl embedded_io::ErrorType for Writer {
|
|||
|
||||
impl embedded_io::Write for Writer {
|
||||
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> {
|
||||
with_uart_tx(|_, uart| {
|
||||
uart.flush()
|
||||
})
|
||||
with_uart_tx(|_, uart| uart.flush())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Command)]
|
||||
enum Base/*<'a>*/ {
|
||||
enum Base /*<'a>*/ {
|
||||
// /// Say hello to World or someone else
|
||||
// Hello {
|
||||
// /// To whom to say hello (World by default)
|
||||
// name: Option<&'a str>,
|
||||
// },
|
||||
|
||||
/// Display the version of the firmware.
|
||||
Version,
|
||||
|
||||
|
|
@ -73,16 +70,26 @@ pub async fn run_console(mut uart_rx: UartRx<'_, Async>) {
|
|||
// write!(cli.writer(), "Hello, {}", name.unwrap_or("World"))?;
|
||||
// }
|
||||
Base::Version => {
|
||||
cli.writer().write_fmt(format_args!("{} - {} - {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"), env!("GIT_COMMIT"))).unwrap();
|
||||
cli.writer()
|
||||
.write_fmt(format_args!(
|
||||
"{} - {} - {}",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
env!("GIT_COMMIT")
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
Base::Reset => {
|
||||
cli.writer().write_str("Performing software reset.").unwrap();
|
||||
cli.writer()
|
||||
.write_str("Performing software reset.")
|
||||
.unwrap();
|
||||
esp_hal::system::software_reset();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}),
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
1
firmware/acid-firmware/src/crypto.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
319
firmware/acid-firmware/src/db/mod.rs
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
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,13 +1,14 @@
|
|||
#![allow(unused_variables)]
|
||||
|
||||
use core::ffi::{c_size_t, c_void};
|
||||
use core::alloc::GlobalAlloc;
|
||||
use core::ffi::{c_size_t, c_void};
|
||||
|
||||
use enumset::EnumSet;
|
||||
use esp_alloc::EspHeap;
|
||||
|
||||
use crate::ffi::string::__xkbc_memcpy;
|
||||
|
||||
// Here we select the allocator to use for libxkbcommon.
|
||||
static XKBC_ALLOCATOR: &EspHeap = &crate::PSRAM_ALLOCATOR;
|
||||
pub use crate::PSRAM_ALLOCATOR as XKBC_ALLOCATOR;
|
||||
|
||||
// Implementation based on esp-alloc's `compat` feature.
|
||||
|
||||
|
|
@ -16,9 +17,14 @@ pub unsafe extern "C" fn __xkbc_malloc(size: c_size_t) -> *mut c_void {
|
|||
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)]
|
||||
pub unsafe extern "C" fn __xkbc_calloc(number: c_size_t, size: c_size_t) -> *mut c_void {
|
||||
let total_size = number as usize * size;
|
||||
let total_size = number * size;
|
||||
unsafe {
|
||||
let ptr = __xkbc_malloc(total_size) as *mut u8;
|
||||
|
||||
|
|
@ -32,11 +38,21 @@ 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)]
|
||||
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(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)]
|
||||
pub unsafe extern "C" fn __xkbc_free(ptr: *mut c_void) {
|
||||
if ptr.is_null() {
|
||||
|
|
@ -54,6 +70,13 @@ 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 {
|
||||
let total_size = size + 4;
|
||||
|
||||
|
|
@ -77,10 +100,6 @@ unsafe fn realloc_with_caps(
|
|||
new_size: usize,
|
||||
caps: enumset::EnumSet<crate::MemoryCapability>,
|
||||
) -> *mut u8 {
|
||||
unsafe extern "C" {
|
||||
fn memcpy(d: *mut u8, s: *const u8, l: usize);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let p = malloc_with_caps(new_size, caps);
|
||||
if !p.is_null() && !ptr.is_null() {
|
||||
|
|
@ -88,7 +107,7 @@ unsafe fn realloc_with_caps(
|
|||
(ptr as *const u32).sub(1).read_volatile() as usize,
|
||||
new_size,
|
||||
);
|
||||
memcpy(p, ptr, len);
|
||||
__xkbc_memcpy(p as *mut _, ptr as *const _, len);
|
||||
__xkbc_free(ptr as *mut _);
|
||||
}
|
||||
p
|
||||
211
firmware/acid-firmware/src/ffi/crypto.rs
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
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!()
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use core::ffi::{c_char, c_int};
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
|
||||
pub enum DIR {}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
|
|
@ -26,7 +26,11 @@ pub mod file;
|
|||
pub unsafe extern "C" fn __xkbc_fopen(filename: *const c_char, mode: *const c_char) -> *mut FILE {
|
||||
warn!(
|
||||
"The xkbcommon library is attempting to open a file at path: {:?}",
|
||||
unsafe { CStr::from_ptr(filename) }
|
||||
if filename.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(unsafe { CStr::from_ptr(filename) })
|
||||
}
|
||||
);
|
||||
null_mut()
|
||||
}
|
||||
|
|
@ -72,6 +76,15 @@ pub unsafe extern "C" fn __xkbc_fprintf(
|
|||
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)]
|
||||
pub unsafe extern "C" fn __xkbc_vfprintf(
|
||||
stream: *mut FILE,
|
||||
|
|
@ -137,3 +150,43 @@ pub unsafe extern "C" fn __xkbc_vsnprintf(
|
|||
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,9 +8,11 @@ use core::{
|
|||
use inout::file::{FILE, STDERR, STDIN, STDOUT};
|
||||
|
||||
pub mod alloc;
|
||||
pub mod crypto;
|
||||
pub mod gcc_runtime;
|
||||
pub mod inout;
|
||||
pub mod string;
|
||||
pub mod time;
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
pub type c_intmax_t = c_longlong;
|
||||
|
|
@ -38,6 +40,11 @@ pub unsafe extern "C" fn __xkbc___errno() -> *mut c_int {
|
|||
todo!()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn __spre___errno() -> *mut c_int {
|
||||
unsafe { __xkbc___errno() }
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn __xkbc_qsort(
|
||||
base: *mut c_void,
|
||||
|
|
@ -76,6 +83,11 @@ 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)]
|
||||
pub unsafe extern "C" fn __xkbc_secure_getenv(name: *const c_char) -> *mut c_char {
|
||||
unsafe { __xkbc_getenv(name) }
|
||||
|
|
@ -136,3 +148,12 @@ pub unsafe extern "C" fn __xkbc___assert_func(
|
|||
pub unsafe extern "C" fn __xkbc_atoi(s: *const c_char) -> c_int {
|
||||
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,
|
||||
n: c_size_t,
|
||||
) -> *mut c_void {
|
||||
if dest_original == null_mut() {
|
||||
if dest_original.is_null() {
|
||||
if n > 0 {
|
||||
panic!("Attempted to memset a nullptr.");
|
||||
} else {
|
||||
|
|
@ -33,6 +33,15 @@ pub unsafe extern "C" fn __xkbc_memset(
|
|||
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)]
|
||||
pub unsafe extern "C" fn __xkbc_memmove(
|
||||
dst: *mut c_void,
|
||||
|
|
@ -55,6 +64,15 @@ pub unsafe extern "C" fn __xkbc_memcpy(
|
|||
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)]
|
||||
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;
|
||||
|
|
@ -90,6 +108,11 @@ 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)]
|
||||
pub unsafe extern "C" fn __xkbc_strpbrk(s: *const c_char, accept: *const c_char) -> *mut c_char {
|
||||
todo!()
|
||||
|
|
@ -100,6 +123,11 @@ pub unsafe extern "C" fn __xkbc_strerror(errnum: c_int) -> *mut c_char {
|
|||
todo!()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn __spre_strerror(errnum: c_int) -> *mut c_char {
|
||||
unsafe { __xkbc_strerror(errnum) }
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn __xkbc_strdup(string: *const c_char) -> *mut c_char {
|
||||
strndup_inner(string, unsafe { __xkbc_strlen(string) })
|
||||
|
|
@ -136,6 +164,11 @@ pub unsafe extern "C" fn __xkbc_strlen(mut s: *const c_char) -> c_size_t {
|
|||
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)]
|
||||
pub unsafe extern "C" fn __xkbc_strnlen(s: *const c_char, maxlen: c_size_t) -> c_size_t {
|
||||
let found: *const c_char =
|
||||
|
|
@ -168,6 +201,15 @@ 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)]
|
||||
pub unsafe extern "C" fn __xkbc_strchr(cs: *const c_char, c: c_int) -> *mut c_char {
|
||||
todo!()
|
||||
|
|
@ -198,3 +240,12 @@ pub unsafe extern "C" fn __xkbc_strtol(
|
|||
) -> c_long {
|
||||
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) }
|
||||
}
|
||||
21
firmware/acid-firmware/src/ffi/time.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
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
|
||||
}
|
||||
227
firmware/acid-firmware/src/logging.rs
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
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;
|
||||
|
|
@ -18,22 +18,30 @@ extern crate alloc;
|
|||
|
||||
use core::alloc::Layout;
|
||||
use core::cell::RefCell;
|
||||
use core::slice;
|
||||
use core::fmt::Write;
|
||||
use core::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use alloc::collections::vec_deque::VecDeque;
|
||||
use alloc::format;
|
||||
use alloc::string::String;
|
||||
use alloc::sync::Arc;
|
||||
use alloc::vec;
|
||||
use cfg_if::cfg_if;
|
||||
use alloc::vec::Vec;
|
||||
use embassy_embedded_hal::adapter::BlockingAsync;
|
||||
use embassy_embedded_hal::flash::partition::Partition;
|
||||
use embassy_executor::Spawner;
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_sync::channel::Channel;
|
||||
use embassy_sync::mutex::Mutex;
|
||||
use embassy_sync::signal::Signal;
|
||||
use embassy_time::{Duration, Instant};
|
||||
use embassy_time::{Duration, Timer};
|
||||
use esp_alloc::{HeapRegion, MemoryCapability};
|
||||
use esp_bootloader_esp_idf::partitions::PartitionTable;
|
||||
use esp_hal::Blocking;
|
||||
use esp_hal::clock::CpuClock;
|
||||
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::i2c::master::{I2c, I2cAddress};
|
||||
use esp_hal::interrupt::software::SoftwareInterruptControl;
|
||||
|
|
@ -41,47 +49,56 @@ use esp_hal::lcd_cam::LcdCam;
|
|||
use esp_hal::lcd_cam::lcd::dpi::Dpi;
|
||||
use esp_hal::mcpwm::{McPwm, PeripheralClockConfig};
|
||||
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::timer::timg::TimerGroup;
|
||||
use esp_rtos::embassy::Executor;
|
||||
use esp_storage::FlashStorage;
|
||||
use log::{LevelFilter, error, info, warn};
|
||||
use indoc::writedoc;
|
||||
use itertools::Itertools;
|
||||
use log::{error, info, warn};
|
||||
use rmk::channel::{CONTROLLER_CHANNEL, ControllerSub};
|
||||
use rmk::config::{BehaviorConfig, PositionalConfig, RmkConfig, StorageConfig, VialConfig};
|
||||
use rmk::config::{
|
||||
DeviceConfig, RmkConfig, StorageConfig,
|
||||
VialConfig,
|
||||
};
|
||||
use rmk::controller::{Controller, EventController};
|
||||
use rmk::debounce::default_debouncer::DefaultDebouncer;
|
||||
use rmk::descriptor::KeyboardReport;
|
||||
use rmk::event::ControllerEvent;
|
||||
use rmk::hid::Report;
|
||||
use rmk::input_device::Runnable;
|
||||
use rmk::join_all;
|
||||
use rmk::keyboard::Keyboard;
|
||||
use rmk::storage::async_flash_wrapper;
|
||||
use rmk::types::action::{Action, KeyAction};
|
||||
use rmk::types::keycode::{HidKeyCode, KeyCode};
|
||||
use rmk::join_all;
|
||||
use rmk::{initialize_keymap_and_storage, run_devices, run_rmk};
|
||||
use slint::ComponentHandle;
|
||||
use slint::platform::software_renderer::Rgb565Pixel;
|
||||
use static_cell::StaticCell;
|
||||
use ui::AppWindow;
|
||||
use xkbcommon::xkb::{self, FeedResult, KeyDirection, Keysym, Status};
|
||||
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::peripherals::st7701s::St7701s;
|
||||
use crate::proxy::create_hid_report_interceptor;
|
||||
use crate::ui::backend::{FramebufferPtr, SlintBackend};
|
||||
use crate::vial::{CustomKeycodes, VIAL_KEYBOARD_DEF, VIAL_KEYBOARD_ID};
|
||||
use crate::vial::{
|
||||
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"];
|
||||
|
||||
mod config;
|
||||
mod crypto;
|
||||
mod db;
|
||||
mod ffi;
|
||||
mod keymap;
|
||||
mod logging;
|
||||
mod matrix;
|
||||
mod peripherals;
|
||||
mod proxy;
|
||||
mod ui;
|
||||
mod util;
|
||||
mod vial;
|
||||
|
||||
#[cfg(feature = "alt-log")]
|
||||
|
|
@ -91,6 +108,15 @@ 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>
|
||||
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(100); // 10 FPS
|
||||
|
||||
|
|
@ -107,15 +133,6 @@ static SIGNAL_UI_RENDER: Signal<CriticalSectionRawMutex, ()> = Signal::new();
|
|||
|
||||
#[esp_rtos::main]
|
||||
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()
|
||||
.with_cpu_clock(CpuClock::max())
|
||||
.with_psram(PsramConfig {
|
||||
|
|
@ -125,30 +142,24 @@ async fn main(_spawner: Spawner) {
|
|||
ram_frequency: SpiRamFreq::Freq80m,
|
||||
});
|
||||
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")]
|
||||
let alt_uart_rx_task = {
|
||||
use esp_hal::uart::Uart;
|
||||
|
||||
let (uart_rx, uart_tx) = Uart::new(peripherals.UART2, Default::default())
|
||||
.unwrap()
|
||||
.with_tx(peripherals.GPIO12)
|
||||
.with_rx(peripherals.GPIO5)
|
||||
.split();
|
||||
logging::setup_alternative_logging(uart_tx, LOG_LEVEL_FILTER);
|
||||
info!("Logger initialized!");
|
||||
console::run_console(uart_rx.into_async())
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "alt-log"))]
|
||||
let alt_uart_rx_task = async {};
|
||||
let console_task =
|
||||
logging::uart::setup_logging(peripherals.UART2, peripherals.GPIO12, peripherals.GPIO5);
|
||||
#[cfg(feature = "rtt-log")]
|
||||
let console_task = logging::rtt::setup_logging();
|
||||
|
||||
// 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);
|
||||
// Memory reclaimed from the esp-idf bootloader.
|
||||
const HEAP_SIZE_RECLAIMED: usize = const {
|
||||
let range = esp_metadata_generated::memory_range!("DRAM2_UNINIT");
|
||||
range.end - range.start
|
||||
};
|
||||
|
||||
esp_alloc::heap_allocator!(#[ram(reclaimed)] size: HEAP_SIZE_RECLAIMED);
|
||||
esp_alloc::heap_allocator!(size: HEAP_SIZE - HEAP_SIZE_RECLAIMED);
|
||||
info!("Heap initialized! {:#?}", esp_alloc::HEAP.stats());
|
||||
|
||||
// Initialize the PSRAM allocator.
|
||||
|
|
@ -161,10 +172,12 @@ async fn main(_spawner: Spawner) {
|
|||
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);
|
||||
// io.set_interrupt_handler(interrupt_handler);
|
||||
|
||||
|
|
@ -187,6 +200,9 @@ async fn main(_spawner: Spawner) {
|
|||
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 sha_backend = ShaBackend::new(peripherals.SHA);
|
||||
let _sha_driver_handle = sha_backend.start();
|
||||
|
||||
let timg0 = TimerGroup::new(peripherals.TIMG0);
|
||||
let software_interrupt = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
|
||||
esp_rtos::start(
|
||||
|
|
@ -195,16 +211,16 @@ async fn main(_spawner: Spawner) {
|
|||
|
||||
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")]
|
||||
let mut host_resources = rmk::HostResources::new();
|
||||
#[cfg(feature = "ble")]
|
||||
let stack = {
|
||||
// Enable the TRNG source, so `Trng` can be constructed.
|
||||
use bt_hci::controller::ExternalController;
|
||||
use esp_hal::rng::TrngSource;
|
||||
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();
|
||||
static RADIO: StaticCell<RadioController<'static>> = StaticCell::new();
|
||||
let radio = RADIO.init(esp_radio::init().unwrap());
|
||||
|
|
@ -224,15 +240,15 @@ async fn main(_spawner: Spawner) {
|
|||
// Initialize USB
|
||||
#[cfg(not(feature = "no-usb"))]
|
||||
let usb_driver = {
|
||||
use core::ptr::addr_of_mut;
|
||||
use esp_hal::otg_fs::Usb;
|
||||
use esp_hal::otg_fs::asynch::{Config, Driver};
|
||||
|
||||
static mut EP_MEMORY: [u8; 1024] = [0; 1024];
|
||||
static EP_MEMORY: StaticCell<[u8; 1024]> = StaticCell::new();
|
||||
let ep_memory = EP_MEMORY.init_with(|| [0_u8; _]);
|
||||
let usb = Usb::new(peripherals.USB0, peripherals.GPIO20, peripherals.GPIO19);
|
||||
// Create the driver, from the HAL.
|
||||
let config = Config::default();
|
||||
let driver = Driver::new(usb, unsafe { &mut *addr_of_mut!(EP_MEMORY) }, config);
|
||||
let driver = Driver::new(usb, ep_memory, config);
|
||||
|
||||
info!("USB driver for RMK built!");
|
||||
|
||||
|
|
@ -240,16 +256,90 @@ async fn main(_spawner: Spawner) {
|
|||
};
|
||||
|
||||
// Initialize the flash
|
||||
let flash = FlashStorage::new(peripherals.FLASH)
|
||||
// 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.
|
||||
// However, it can also be configured to auto-park the other core, such that writing to
|
||||
// flash succeeds.
|
||||
// Alternatively, XiP from PSRAM could be used along with the `multicore_ignore` strategy,
|
||||
// to avoid having to park the other core, which could result in better performance.
|
||||
// Invalid configuration would then present itself as freezing/UB.
|
||||
.multicore_auto_park();
|
||||
let flash = async_flash_wrapper(flash);
|
||||
static PARTITION_TABLE_BUFFER: StaticCell<Vec<u8, &'static esp_alloc::EspHeap>> =
|
||||
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.
|
||||
// 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
|
||||
// flash succeeds.
|
||||
// Alternatively, XiP from PSRAM could be used along with the `multicore_ignore` strategy,
|
||||
// to avoid having to park the other core, which could result in better performance.
|
||||
// Invalid configuration would then present itself as freezing/UB.
|
||||
.multicore_auto_park();
|
||||
let partition_table = {
|
||||
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!");
|
||||
|
||||
|
|
@ -295,27 +385,55 @@ async fn main(_spawner: Spawner) {
|
|||
// RMK config
|
||||
let vial_config = VialConfig::new(VIAL_KEYBOARD_ID, VIAL_KEYBOARD_DEF, &[(0, 0), (1, 1)]);
|
||||
let storage_config = StorageConfig {
|
||||
start_addr: 0x3f0000,
|
||||
num_sectors: 16,
|
||||
start_addr: 0,
|
||||
num_sectors: {
|
||||
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()
|
||||
};
|
||||
// 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 {
|
||||
device_config,
|
||||
vial_config,
|
||||
storage_config,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Initialze keyboard stuffs
|
||||
// Initialize the storage and keymap
|
||||
let mut default_keymap = keymap::get_default_keymap();
|
||||
let mut behavior_config = BehaviorConfig::default();
|
||||
let mut per_key_config = PositionalConfig::default();
|
||||
let mut default_keymap = config::get_default_keymap();
|
||||
let mut behavior_config = config::get_behavior_config();
|
||||
let mut positional_config = config::get_positional_config();
|
||||
let (keymap, mut storage) = initialize_keymap_and_storage(
|
||||
&mut default_keymap,
|
||||
flash,
|
||||
flash_part_rmk,
|
||||
&storage_config,
|
||||
&mut behavior_config,
|
||||
&mut per_key_config,
|
||||
&mut positional_config,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
|
@ -355,7 +473,7 @@ async fn main(_spawner: Spawner) {
|
|||
let window_size = [framebuffer.height, framebuffer.width];
|
||||
let framebuffer_ptr = FramebufferPtr(framebuffer.as_target_pixels() as _);
|
||||
|
||||
static SECOND_CORE_STACK: StaticCell<Stack<{ 8192 * 2 }>> = StaticCell::new();
|
||||
static SECOND_CORE_STACK: StaticCell<Stack<STACK_SIZE_CORE_APP>> = StaticCell::new();
|
||||
let second_core_stack = SECOND_CORE_STACK.init(Stack::new());
|
||||
esp_rtos::start_second_core(
|
||||
peripherals.CPU_CTRL,
|
||||
|
|
@ -377,8 +495,10 @@ async fn main(_spawner: Spawner) {
|
|||
window_size,
|
||||
window: RefCell::new(None),
|
||||
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));
|
||||
spawner.must_spawn(ui::run_renderer_task(slint_backend, flash_part_acid));
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
@ -391,6 +511,7 @@ async fn main(_spawner: Spawner) {
|
|||
|
||||
// TODO: Probably want to select! instead and re-try.
|
||||
join_all![
|
||||
run_alloc_stats_reporter(),
|
||||
// We currently send the framebuffer data using the main core, which does not seem to slow
|
||||
// down the rest of the tasks too much.
|
||||
run_lcd(st7701s, framebuffer),
|
||||
|
|
@ -409,11 +530,39 @@ async fn main(_spawner: Spawner) {
|
|||
),
|
||||
create_hid_report_interceptor(),
|
||||
user_controller.event_loop(),
|
||||
alt_uart_rx_task
|
||||
console_task
|
||||
]
|
||||
.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 {
|
||||
sub: ControllerSub,
|
||||
}
|
||||
|
|
@ -430,25 +579,22 @@ impl Controller for UserController {
|
|||
type Event = ControllerEvent;
|
||||
|
||||
async fn process_event(&mut self, event: Self::Event) {
|
||||
if let ControllerEvent::Key(
|
||||
keyboard_event,
|
||||
KeyAction::Single(Action::User(user_key_index)),
|
||||
) = event
|
||||
if let ControllerEvent::Key(keyboard_event, KeyAction::Single(Action::User(user_key_index))) =
|
||||
event
|
||||
&& user_key_index == CustomKeycodes::FOCUS_LCD as u8
|
||||
&& keyboard_event.pressed
|
||||
{
|
||||
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 {
|
||||
false => {
|
||||
info!("Disabling LCD.");
|
||||
*rmk::channel::KEYBOARD_REPORT_SENDER.write().await =
|
||||
&rmk::channel::KEYBOARD_REPORT_RECEIVER;
|
||||
}
|
||||
true => {
|
||||
info!("Enabling LCD.");
|
||||
*rmk::channel::KEYBOARD_REPORT_SENDER.write().await =
|
||||
&KEYBOARD_REPORT_PROXY;
|
||||
}
|
||||
match enabled {
|
||||
false => {
|
||||
info!("Disabling LCD.");
|
||||
*rmk::channel::KEYBOARD_REPORT_SENDER.write().await =
|
||||
&rmk::channel::KEYBOARD_REPORT_RECEIVER;
|
||||
}
|
||||
true => {
|
||||
info!("Enabling LCD.");
|
||||
*rmk::channel::KEYBOARD_REPORT_SENDER.write().await = &KEYBOARD_REPORT_PROXY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -521,7 +667,7 @@ async fn run_lcd(mut st7701s: St7701s<'static, Blocking>, framebuffer: &'static
|
|||
|
||||
// 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
|
||||
// They need to be implemented in esp-hal.
|
||||
// This can be implemented as a `DmaTxBuffer`.
|
||||
let transfer = match st7701s.dpi.send(false, framebuffer.dma_buf.take().unwrap()) {
|
||||
Err((error, result_dpi, result_dma_buf)) => {
|
||||
error!(
|
||||
|
|
@ -13,6 +13,8 @@ use rmk::{
|
|||
matrix::{KeyState, MatrixTrait},
|
||||
};
|
||||
|
||||
use crate::config::{MATRIX_AREA, MATRIX_COLS, MATRIX_ROWS};
|
||||
|
||||
pub struct RaiiGuard<F: FnOnce()> {
|
||||
on_drop: Option<F>,
|
||||
}
|
||||
|
|
@ -33,10 +35,6 @@ 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
|
||||
pub struct IoeMatrix<D>
|
||||
where
|
||||
|
|
@ -107,6 +107,19 @@ pub async fn spi_read(
|
|||
|
||||
use crate::peripherals::st7701s::*;
|
||||
|
||||
// struct InitSequenceAction {
|
||||
// command: ArrayCommand<8>,
|
||||
// sleep: u64,
|
||||
// }
|
||||
|
||||
// pub const INIT_SEQUENCE: [InitSequenceAction; _] = [CmdCn2bkxsel(
|
||||
// CmdCn2bkxselArg0::new(),
|
||||
// CmdCn2bkxselArg1::new(),
|
||||
// CmdCn2bkxselArg2::new(),
|
||||
// CmdCn2bkxselArg3::new(),
|
||||
// CmdCn2bkxselArg4::new().with_bksel(0x3).with_cn2(true),
|
||||
// )];
|
||||
|
||||
lazy_static! {
|
||||
pub static ref INIT_SEQUENCE_COMMANDS: Vec<(Vec<Box<dyn Command + Send + Sync>>, u64)> = vec![
|
||||
(vec![
|
||||
|
|
@ -3,13 +3,15 @@ use esp_hal::{
|
|||
DriverMode,
|
||||
gpio::{Flex, Level, Output},
|
||||
lcd_cam::lcd::{
|
||||
ClockMode, DelayMode, Phase, Polarity, dpi::{Dpi, Format, FrameTiming}
|
||||
ClockMode, DelayMode, Phase, Polarity,
|
||||
dpi::{Dpi, Format, FrameTiming},
|
||||
},
|
||||
time::Rate,
|
||||
};
|
||||
use lcd::spi_write;
|
||||
use log::debug;
|
||||
use paste::paste;
|
||||
// use tinyvec::ArrayVec;
|
||||
|
||||
mod lcd;
|
||||
|
||||
|
|
@ -18,6 +20,30 @@ pub trait Command {
|
|||
fn args_iter(&'_ self) -> core::slice::Iter<'_, u8>;
|
||||
}
|
||||
|
||||
// pub struct ArrayCommand<const N: usize> {
|
||||
// pub address: u8,
|
||||
// pub args: ArrayVec<[u8; N]>,
|
||||
// }
|
||||
|
||||
// impl<const N: usize> const ArrayCommand<N> {
|
||||
// const fn from(command: impl Command) -> Self {
|
||||
// Self {
|
||||
// address: command.address(),
|
||||
// args: command.args_iter().copied().collect(),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl<const N: usize> Command for ArrayCommand<N> {
|
||||
// fn address(&self) -> u8 {
|
||||
// self.address
|
||||
// }
|
||||
|
||||
// fn args_iter(&'_ self) -> core::slice::Iter<'_, u8> {
|
||||
// self.args.iter()
|
||||
// }
|
||||
// }
|
||||
|
||||
pub struct CustomCommand {
|
||||
pub address: u8,
|
||||
pub args: &'static [u8],
|
||||
615
firmware/acid-firmware/src/proxy.rs
Normal file
|
|
@ -0,0 +1,615 @@
|
|||
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);
|
||||
169
firmware/acid-firmware/src/ui/backend.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
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(())
|
||||
}
|
||||
}
|
||||
50
firmware/acid-firmware/src/ui/messages.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
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 },
|
||||
}
|
||||
900
firmware/acid-firmware/src/ui/mod.rs
Normal file
|
|
@ -0,0 +1,900 @@
|
|||
// #![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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
160
firmware/acid-firmware/src/ui/storage.rs
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
48
firmware/acid-firmware/src/util.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
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."),
|
||||
}
|
||||
}
|
||||
3
firmware/acid-firmware/ui/globals.slint
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export global Style {
|
||||
in property <length> spacing: 8px;
|
||||
}
|
||||
1
firmware/acid-firmware/ui/images/help-circle.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 365 B |
1
firmware/acid-firmware/ui/images/key.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 352 B |
1
firmware/acid-firmware/ui/images/log-in.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 368 B |
1
firmware/acid-firmware/ui/images/log-out.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 367 B |
1
firmware/acid-firmware/ui/images/sliders.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 611 B |
1
firmware/acid-firmware/ui/images/trash-2.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 448 B |
1
firmware/acid-firmware/ui/images/users.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 400 B |
48
firmware/acid-firmware/ui/login-view.slint
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
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 { }
|
||||
}
|
||||
106
firmware/acid-firmware/ui/main.slint
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
firmware/acid-firmware/ui/user-edit-view.slint
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
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 { }
|
||||
}
|
||||
}
|
||||
61
firmware/acid-firmware/ui/user-sites-view.slint
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
57
firmware/acid-firmware/ui/users-view.slint
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
8
firmware/acid-firmware/ui/widgets/icon-button.slint
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Button } from "std-widgets.slint";
|
||||
|
||||
export component IconButton inherits Button {
|
||||
colorize-icon: true;
|
||||
icon-size: 24px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
|
@ -47,6 +47,36 @@
|
|||
"name": "FOCUS_LCD",
|
||||
"title": "Focus LCD",
|
||||
"shortName": "Focus\nLCD"
|
||||
},
|
||||
{
|
||||
"name": "FORK_ACUTE_ABOVERING_CS",
|
||||
"title": "Shift fork between ´ and ° on the CS layout",
|
||||
"shortName": "(CS)\n°\n´"
|
||||
},
|
||||
{
|
||||
"name": "FORK_CARON_DIAERESIS_CS",
|
||||
"title": "Shift fork between ˇ and ¨on the CS layout",
|
||||
"shortName": "(CS)\n¨\nˇ"
|
||||
},
|
||||
{
|
||||
"name": "FORK_ACUTE_ABOVERING_CSP",
|
||||
"title": "Shift fork between ´ and ° on the CSP layout",
|
||||
"shortName": "(CSP)\n°\n´"
|
||||
},
|
||||
{
|
||||
"name": "FORK_CARON_DIAERESIS_CSP",
|
||||
"title": "Shift fork between ˇ and ¨on the CSP layout",
|
||||
"shortName": "(CSP)\n¨\nˇ"
|
||||
},
|
||||
{
|
||||
"name": "FORK_9_FORWARDSLASH_EN_CSP",
|
||||
"title": "Shift fork between 9 and / on the EN or the CSP layout",
|
||||
"shortName": "(EN/CSP)\n/\n9"
|
||||
},
|
||||
{
|
||||
"name": "FORK_0_BACKWARDSLASH_EN_CSP",
|
||||
"title": "Shift fork between 0 and \\ on the EN or the CSP layout",
|
||||
"shortName": "(EN/CSP)\n\\\n0"
|
||||
}
|
||||
],
|
||||
"layouts": {
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
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
firmware/libsodium
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit d24faf56214469b354b01c8ba36257e04737101e
|
||||
22
firmware/libsodium-compile.sh
Executable file
|
|
@ -0,0 +1,22 @@
|
|||
#!/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
|
||||
6
firmware2/libxkbcommon-compile.sh → firmware/libxkbcommon-compile.sh
Normal file → Executable file
|
|
@ -10,10 +10,8 @@ BUILD_DIR_NAME="$LIBXKBCOMMON_DIR/$1"
|
|||
STATIC_LIB_PATH="$BUILD_DIR_NAME/libxkbcommon.a"
|
||||
SETUP_ARGS=${@:2}
|
||||
|
||||
git submodule update --init --recursive
|
||||
pushd "$LIBXKBCOMMON_DIR" >/dev/null
|
||||
meson setup "$BUILD_DIR_NAME" \
|
||||
--wipe \
|
||||
--cross-file "$SCRIPT_DIR/cross-esp32s3.txt" \
|
||||
-Denable-x11=false \
|
||||
-Denable-wayland=false \
|
||||
|
|
@ -23,8 +21,8 @@ pushd "$LIBXKBCOMMON_DIR" >/dev/null
|
|||
-Dxkb-config-root=/usr/share/X11/xkb \
|
||||
-Dx-locale-root=/usr/share/X11/locale \
|
||||
$SETUP_ARGS
|
||||
meson compile -C "$BUILD_DIR_NAME"
|
||||
$SCRIPT_DIR/libxkbcommon-redefine-syms.sh "$STATIC_LIB_PATH" "$STATIC_LIB_PATH"
|
||||
meson compile -C "$BUILD_DIR_NAME" xkbcommon
|
||||
$SCRIPT_DIR/redefine-syms.sh "__xkbc_" "$STATIC_LIB_PATH" "$STATIC_LIB_PATH"
|
||||
popd >/dev/null
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
2
firmware/password-hash/.cargo/config.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[env] # These must be kept in sync with /.zed/settings.json
|
||||
LIBSODIUM_INSTALL_DIR = "../libsodium/install-host"
|
||||
27
firmware/password-hash/Cargo.toml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
[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 }
|
||||
17
firmware/password-hash/README.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# 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]
|
||||
```
|
||||
35
firmware/password-hash/build.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
88
firmware/password-hash/src/bin.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
#![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()
|
||||
}
|
||||
51
firmware/password-hash/src/blocks.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
69
firmware/password-hash/src/lib.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
//! 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);
|
||||
}
|
||||
10
firmware2/libxkbcommon-redefine-syms.sh → firmware/redefine-syms.sh
Normal file → Executable file
|
|
@ -1,10 +1,10 @@
|
|||
#!/usr/bin/env bash
|
||||
INPUT_LIB=$1
|
||||
OUTPUT_LIB=$2
|
||||
PREFIX="__xkbc_"
|
||||
PREFIX=$1
|
||||
INPUT_LIB=$2
|
||||
OUTPUT_LIB=$3
|
||||
|
||||
if [ "$#" -ne 2 ]; then
|
||||
echo "Usage: $0 <input_lib.a> <output_lib.a>"
|
||||
if [ "$#" -ne 3 ]; then
|
||||
echo "Usage: $0 <prefix> <input_lib.a> <output_lib.a>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
[toolchain]
|
||||
channel = "esp"
|
||||
1
firmware/spectre-api-c
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 733e6d6d73bc9462d8ec9528dd96eee23846a3e3
|
||||
16
firmware/spectre-api-compile.sh
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
#!/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
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
#![no_std]
|
||||
#![deny(clippy::mem_forget)]
|
||||
#![feature(macro_metavar_expr)]
|
||||
|
||||
pub mod st7701s;
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
[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" }
|
||||