Compare commits

...

44 commits

Author SHA1 Message Date
Jakub Hlusička ea58ef0c8e Fix clippy lints 2026-02-13 03:00:46 +01:00
Jakub Hlusička 707c994a76 Use fork keys 2026-02-13 02:50:32 +01:00
Jakub Hlusička 5004e8dfdf Support RMK device config 2026-02-12 22:44:13 +01:00
Jakub Hlusička 8e304540ea Enable 4 layers in total. 2026-02-12 03:23:16 +01:00
Jakub Hlusička ab506de76a More host string sending improvements, add compiled czech coders'
layout, send site password to host
2026-02-12 02:39:31 +01:00
Jakub Hlusička a09da0c8a7 Improvements to sending strings to host 2026-02-10 03:12:49 +01:00
Jakub Hlusička 3b364c64c2 Naive implementation of sending strings to the host 2026-02-09 02:14:06 +01:00
Jakub Hlusička 1e2d43a628 Site password derivation 2026-02-08 21:04:28 +01:00
Jakub Hlusička 4a5ada0bb0 Fix password fields; show stored users on login page 2026-02-08 01:17:33 +01:00
Jakub Hlusička d4c8d69cf3 Use custom partition table to prevent firmware getting overwritten 2026-02-07 10:10:47 +01:00
Jakub Hlusička 40b9b5d278 Implement spectre user addition 2026-02-04 03:14:21 +01:00
Jakub Hlusička 3ac1656d33 Add .zed settings 2026-02-03 22:05:47 +01:00
Jakub Hlusička b6d9a71b59 Fix build profiles 2026-02-03 00:27:20 +01:00
Jakub Hlusička c2e3f1bec3 Logging cleanup and fix for RTT 2026-01-31 20:21:40 +01:00
Jakub Hlusička f8ef06ee0c Clean up some lints 2026-01-31 15:36:36 +01:00
Jakub Hlusička 5592708271 UI code cleanup 2026-01-31 15:26:59 +01:00
Jakub Hlusička 2b8dfa7b44 .gitattributes: Always check out *.sh files with LF rather than CRLF 2026-01-30 21:56:46 +01:00
Jakub Hlusička 3947215a23 Use a git rev for spectre-api-sys 2026-01-30 03:18:43 +01:00
Jakub Hlusička 5f34f078db Automated building 2026-01-30 02:02:59 +01:00
Jakub Hlusička d4aad0e8cd Use patched versions of some dependencies 2026-01-30 00:37:46 +01:00
Jakub Hlusička 3b24825677 Off-load some slint allocations to PSRAM 2026-01-28 22:46:14 +01:00
Jakub Hlusička 8426852d7c More GUI progress 2026-01-27 02:46:53 +01:00
Jakub Hlusička 9c2a614aff Change memory regions to prevent crashes; Improve GUI 2026-01-26 19:25:33 +01:00
Jakub Hlusička b33f4852b2 Fix a weird crash? 2026-01-25 20:52:38 +01:00
Jakub Hlusička 0cb6209d4b POC reading and writing spectre sites 2026-01-25 18:43:07 +01:00
Jakub Hlusička a3a95b179b Storage progress 2026-01-25 01:45:25 +01:00
Jakub Hlusička 9aa5430851 Improved db format 2026-01-24 21:47:21 +01:00
Jakub Hlusička bbbaea803b Basic db impl 2026-01-24 21:12:25 +01:00
Jakub Hlusička 6cd7b32bee Mount EKV on a separate partition 2026-01-24 00:42:16 +01:00
Jakub Hlusička 2a5779ffcf Derive correct salt for encryption key 2026-01-22 22:57:43 +01:00
Jakub Hlusička 299a1195f1 Avoid compiling tests, benches and fuzzing of libxkbcommon 2026-01-22 18:16:54 +01:00
Jakub Hlusička 7fca722f24 Disable -C force-frame-pointers, as it is only applicable to riscv 2026-01-22 18:16:37 +01:00
Jakub Hlusička 16ed51b19e Move acid-firmware into its own subfolder of the cargo workspace, and
some other improvements
2026-01-22 01:22:31 +01:00
Jakub Hlusička 810f21827b Make spectre-api-compile.sh always use the right path to
`cross-esp32s3.txt`
2026-01-21 23:18:48 +01:00
Jakub Hlusička ee17cc9f57 Rename directory firmware2 to firmware 2026-01-21 23:15:43 +01:00
Jakub Hlusička d1dd4abc06 Delete old firmware crate 2026-01-21 23:15:13 +01:00
Jakub Hlusička 47e6c890ca SHA HW accel attempts 2026-01-21 02:21:52 +01:00
Jakub Hlusička a5a5ee9330 Fix password-hash/build.rs which would cause rust-analyzer to crash 2026-01-21 00:48:17 +01:00
Jakub Hlusička dbdfa8ae44 Nicer (and correct) duration formatting 2026-01-20 22:01:12 +01:00
Jakub Hlusička 3c695be996 Implement Spectre site key derivation 2026-01-20 21:32:37 +01:00
Jakub Hlusička b5535d6f52 Slint handle acceptation 2026-01-20 02:55:17 +01:00
Jakub Hlusička 35c017535e Work-around for unsupported unicode control keys in Slint 2026-01-20 01:59:18 +01:00
Jakub Hlusička c98acc4da4 Spectre integration WIP 2026-01-19 20:13:25 +01:00
Jakub Hlusička 24daa0ad29 Fix some lints 2026-01-12 01:03:27 +01:00
108 changed files with 14113 additions and 12566 deletions

8
.gitmodules vendored
View file

@ -1,3 +1,9 @@
[submodule "firmware2/libxkbcommon"] [submodule "firmware2/libxkbcommon"]
path = firmware2/libxkbcommon path = firmware/libxkbcommon
url = https://github.com/xkbcommon/libxkbcommon url = https://github.com/xkbcommon/libxkbcommon
[submodule "firmware2/spectre-api-c"]
path = firmware/spectre-api-c
url = https://github.com/Limeth/spectre-api.git
[submodule "firmware/libsodium"]
path = firmware/libsodium
url = https://github.com/jedisct1/libsodium

View file

@ -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
View file

@ -0,0 +1,2 @@
# Convert to LF line endings on checkout.
*.sh text eol=lf

View file

@ -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
View file

@ -1,19 +1,2 @@
# will have compiled files and executables /.cargo
debug/ !/acid-firmware/partition-table.csv
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/

View file

@ -9,10 +9,18 @@
"chip": "esp32s3", "chip": "esp32s3",
"coreConfigs": [ "coreConfigs": [
{ {
"programBinary": "target/xtensa-esp32s3-none-elf/debug/acid-firmware" "programBinary": "target/xtensa-esp32s3-none-elf/debug/acid-firmware",
"rttEnabled": true,
"rttChannelFormats": [
{
"channelNumber": 0,
"dataFormat": "String",
"mode": "BlockIfFull"
} }
] ]
}, },
],
},
{ {
"name": "probe-rs release restart", "name": "probe-rs release restart",
"type": "probe-rs-debug", "type": "probe-rs-debug",
@ -21,10 +29,18 @@
"chip": "esp32s3", "chip": "esp32s3",
"coreConfigs": [ "coreConfigs": [
{ {
"programBinary": "target/xtensa-esp32s3-none-elf/release/acid-firmware" "programBinary": "target/xtensa-esp32s3-none-elf/release/acid-firmware",
"rttEnabled": true,
"rttChannelFormats": [
{
"channelNumber": 0,
"dataFormat": "String",
"mode": "BlockIfFull"
} }
] ]
}, },
],
},
{ {
"preLaunchTask": "rust: cargo build", "preLaunchTask": "rust: cargo build",
"name": "probe-rs debug", "name": "probe-rs debug",
@ -32,15 +48,26 @@
"request": "launch", "request": "launch",
"flashingConfig": { "flashingConfig": {
"flashingEnabled": true, "flashingEnabled": true,
"formatOptions": {
"idf_partition_table": "partition-table.csv"
}
}, },
"probe": "303a:1001", "probe": "303a:1001",
"chip": "esp32s3", "chip": "esp32s3",
"coreConfigs": [ "coreConfigs": [
{ {
"programBinary": "target/xtensa-esp32s3-none-elf/debug/acid-firmware" "programBinary": "target/xtensa-esp32s3-none-elf/debug/acid-firmware",
"rttEnabled": true,
"rttChannelFormats": [
{
"channelNumber": 0,
"dataFormat": "String",
"mode": "BlockIfFull"
} }
] ]
}, },
],
},
{ {
"preLaunchTask": "rust: cargo build --release", "preLaunchTask": "rust: cargo build --release",
"name": "probe-rs release", "name": "probe-rs release",
@ -48,14 +75,25 @@
"request": "launch", "request": "launch",
"flashingConfig": { "flashingConfig": {
"flashingEnabled": true, "flashingEnabled": true,
"formatOptions": {
"idf_partition_table": "partition-table.csv"
}
}, },
"probe": "303a:1001", "probe": "303a:1001",
"chip": "esp32s3", "chip": "esp32s3",
"coreConfigs": [ "coreConfigs": [
{ {
"programBinary": "target/xtensa-esp32s3-none-elf/release/acid-firmware" "programBinary": "target/xtensa-esp32s3-none-elf/release/acid-firmware",
} "rttEnabled": true,
] "rttChannelFormats": [
{
"channelNumber": 0,
"dataFormat": "String",
"mode": "BlockIfFull"
} }
] ]
},
],
},
],
} }

25
firmware/.vscode/settings.json vendored Normal file
View 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"
}
}

View file

@ -4,9 +4,15 @@
{ {
"label": "rust: cargo build", "label": "rust: cargo build",
"type": "cargo", "type": "cargo",
"options": {
"cwd": "${workspaceFolder}/acid-firmware",
"env": {
"ESP_LOG": "info"
}
},
"command": "build", "command": "build",
"args": [ "args": [
"--no-default-features", "--features=probe,info" "--no-default-features", "--features=probe"
], ],
"problemMatcher": [ "problemMatcher": [
"$rustc" "$rustc"
@ -19,9 +25,15 @@
{ {
"label": "rust: cargo build --release", "label": "rust: cargo build --release",
"type": "cargo", "type": "cargo",
"options": {
"cwd": "${workspaceFolder}/acid-firmware",
"env": {
"ESP_LOG": "info"
}
},
"command": "build", "command": "build",
"args": [ "args": [
"--release", "--no-default-features", "--features=probe,info" "--release", "--no-default-features", "--features=probe"
], ],
"problemMatcher": [ "problemMatcher": [
"$rustc" "$rustc"

14
firmware/.zed/debug.json Normal file
View 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": [],
},
]

View 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

File diff suppressed because it is too large Load diff

View file

@ -1,56 +1,26 @@
[package] [workspace]
edition = "2021" resolver = "3"
name = "acid-firmware" members = ["acid-firmware", "password-hash"]
rust-version = "1.86" default-members = ["acid-firmware"]
version = "0.1.0"
[[bin]] [workspace.dependencies]
name = "acid-firmware" spectre-api-sys = { git = "https://github.com/Limeth/spectre-api-sys", rev = "9e844eb056c3dfee8286ac21ec40fa689a8b8aa2" }
path = "./src/bin/main.rs"
[dependencies]
# TODO: Remove the `git = ...` fields, which are here because IntelliSense is broken in 1.0.0-rc.0
esp-bootloader-esp-idf = { version = "0.2.0", git = "https://github.com/esp-rs/esp-hal", features = ["esp32s3"] }
esp-hal = { version = "=1.0.0-rc.0", git = "https://github.com/esp-rs/esp-hal", features = [
"esp32s3",
"log-04",
"unstable",
"psram",
] }
log = "0.4.27"
critical-section = "1.2.0"
embassy-executor = { version = "0.9.0", features = [
"log",
# "task-arena-size-20480",
] }
embassy-time = { version = "0.4.0", features = ["log"] }
esp-alloc = { version = "0.8.0", git = "https://github.com/esp-rs/esp-hal" }
esp-backtrace = { version = "0.17.0", git = "https://github.com/esp-rs/esp-hal", features = [
"esp32s3",
# "exception-handler",
"panic-handler",
"println",
] }
esp-hal-embassy = { version = "0.9.0", git = "https://github.com/esp-rs/esp-hal", features = ["esp32s3", "log-04"] }
esp-println = { version = "0.15.0", git = "https://github.com/esp-rs/esp-hal", features = ["esp32s3", "log-04"] }
static_cell = "2.1.1"
itertools = { version = "0.14.0", default-features = false }
bitflags = "2.9.4"
paste = "1.0.15"
lazy_static = { version = "1.5.0", features = ["spin_no_std"], default-features = false }
[profile.dev.package.esp-storage]
opt-level = 3
[profile.dev] [profile.dev]
# Rust debug is too slow. # Rust debug is too slow.
# For debug builds always builds with some optimization # For debug builds always builds with some optimization
opt-level = "s" opt-level = "s"
lto = 'thin'
[profile.release] [profile.release]
codegen-units = 1 # LLVM can perform better optimizations using a single thread codegen-units = 1 # LLVM can perform better optimizations using a single thread
debug = 2 debug = 2
debug-assertions = false debug-assertions = false
incremental = false incremental = false
lto = 'fat' lto = 'thin'
opt-level = 's' opt-level = 3
overflow-checks = false overflow-checks = false

View file

@ -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?

View 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"]

View 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

View file

@ -43,14 +43,25 @@ This replaces the debugging symbols with paths that will be available when debug
Then compile the firmware with: Then compile the firmware with:
```ps1 ```ps1
$env: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 ### Creating keymaps
To generate an English (US) keymap, the following command may be used: To generate an English (US) keymap, the following command may be used:
`xkbcli compile-keymap --include [path-to-xkb-directory] --layout us >my_compose.txt` `xkbcli compile-keymap --include [path-to-xkb-directory] --layout us >my_keymap.xkb`
Substitute `us` for any other 2-letter country code. 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: Compose files to replace `[path-to-Compose-file]` with may be found in:
* the `/usr/share/X11/locale` directory on X11-based Linux distributions; * the `/usr/share/X11/locale` directory on X11-based Linux distributions;
* the [`libx11` git repository](https://gitlab.freedesktop.org/xorg/lib/libx11/-/tree/master). 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 # esp32s3 BLE example

View 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();
}

File diff suppressed because it is too large Load diff

View 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,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x6000
3 phy_init data phy 0xf000 0x1000
4 factory app factory 0x10000 4M
5 rmk data undefined 64K
6 acid data undefined 0x7E0000

View 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()
}

View file

@ -1,12 +1,14 @@
use core::fmt::Write; use core::fmt::Write;
use embedded_cli::cli::CliBuilder;
use embedded_cli::Command; use embedded_cli::Command;
use esp_hal::{Async, uart::{TxError, UartRx}}; use embedded_cli::cli::CliBuilder;
use log::{info, error}; 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; struct Writer;
@ -16,26 +18,21 @@ impl embedded_io::ErrorType for Writer {
impl embedded_io::Write for Writer { impl embedded_io::Write for Writer {
fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> { fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
with_uart_tx(|_, uart| { with_uart_tx(|_, uart| uart.write(buf))
uart.write(buf)
})
} }
fn flush(&mut self) -> Result<(), Self::Error> { fn flush(&mut self) -> Result<(), Self::Error> {
with_uart_tx(|_, uart| { with_uart_tx(|_, uart| uart.flush())
uart.flush()
})
} }
} }
#[derive(Command)] #[derive(Command)]
enum Base/*<'a>*/ { enum Base /*<'a>*/ {
// /// Say hello to World or someone else // /// Say hello to World or someone else
// Hello { // Hello {
// /// To whom to say hello (World by default) // /// To whom to say hello (World by default)
// name: Option<&'a str>, // name: Option<&'a str>,
// }, // },
/// Display the version of the firmware. /// Display the version of the firmware.
Version, Version,
@ -73,16 +70,26 @@ pub async fn run_console(mut uart_rx: UartRx<'_, Async>) {
// write!(cli.writer(), "Hello, {}", name.unwrap_or("World"))?; // write!(cli.writer(), "Hello, {}", name.unwrap_or("World"))?;
// } // }
Base::Version => { Base::Version => {
cli.writer().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 => { Base::Reset => {
cli.writer().write_str("Performing software reset.").unwrap(); cli.writer()
.write_str("Performing software reset.")
.unwrap();
esp_hal::system::software_reset(); esp_hal::system::software_reset();
} }
} }
Ok(()) Ok(())
}), }),
).unwrap(); )
.unwrap();
} }
} }
} }

View file

@ -0,0 +1 @@

View 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),
}
}
}
}

View file

@ -1,13 +1,14 @@
#![allow(unused_variables)] #![allow(unused_variables)]
use core::ffi::{c_size_t, c_void};
use core::alloc::GlobalAlloc; use core::alloc::GlobalAlloc;
use core::ffi::{c_size_t, c_void};
use enumset::EnumSet; use enumset::EnumSet;
use esp_alloc::EspHeap;
use crate::ffi::string::__xkbc_memcpy;
// Here we select the allocator to use for libxkbcommon. // Here we select the allocator to use for libxkbcommon.
static XKBC_ALLOCATOR: &EspHeap = &crate::PSRAM_ALLOCATOR; pub use crate::PSRAM_ALLOCATOR as XKBC_ALLOCATOR;
// Implementation based on esp-alloc's `compat` feature. // 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 { malloc_with_caps(size, EnumSet::empty()) as *mut _ }
} }
#[unsafe(no_mangle)]
pub unsafe extern "C" fn __spre_malloc(size: c_size_t) -> *mut c_void {
unsafe { __xkbc_malloc(size) }
}
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub unsafe extern "C" fn __xkbc_calloc(number: c_size_t, size: c_size_t) -> *mut c_void { pub unsafe extern "C" fn __xkbc_calloc(number: c_size_t, size: c_size_t) -> *mut c_void {
let total_size = number as usize * size; let total_size = number * size;
unsafe { unsafe {
let ptr = __xkbc_malloc(total_size) as *mut u8; 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)] #[unsafe(no_mangle)]
pub unsafe extern "C" fn __xkbc_realloc(ptr: *mut c_void, new_size: c_size_t) -> *mut c_void { pub unsafe extern "C" fn __xkbc_realloc(ptr: *mut c_void, new_size: c_size_t) -> *mut c_void {
unsafe { realloc_with_caps(ptr as *mut _, new_size, EnumSet::empty()) as *mut _ } unsafe { realloc_with_caps(ptr as *mut _, new_size, EnumSet::empty()) as *mut _ }
} }
#[unsafe(no_mangle)]
pub unsafe extern "C" fn __spre_realloc(ptr: *mut c_void, new_size: c_size_t) -> *mut c_void {
unsafe { __xkbc_realloc(ptr, new_size) }
}
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub unsafe extern "C" fn __xkbc_free(ptr: *mut c_void) { pub unsafe extern "C" fn __xkbc_free(ptr: *mut c_void) {
if ptr.is_null() { if ptr.is_null() {
@ -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 { unsafe fn malloc_with_caps(size: usize, caps: EnumSet<crate::MemoryCapability>) -> *mut u8 {
let total_size = size + 4; let total_size = size + 4;
@ -77,10 +100,6 @@ unsafe fn realloc_with_caps(
new_size: usize, new_size: usize,
caps: enumset::EnumSet<crate::MemoryCapability>, caps: enumset::EnumSet<crate::MemoryCapability>,
) -> *mut u8 { ) -> *mut u8 {
unsafe extern "C" {
fn memcpy(d: *mut u8, s: *const u8, l: usize);
}
unsafe { unsafe {
let p = malloc_with_caps(new_size, caps); let p = malloc_with_caps(new_size, caps);
if !p.is_null() && !ptr.is_null() { if !p.is_null() && !ptr.is_null() {
@ -88,7 +107,7 @@ unsafe fn realloc_with_caps(
(ptr as *const u32).sub(1).read_volatile() as usize, (ptr as *const u32).sub(1).read_volatile() as usize,
new_size, new_size,
); );
memcpy(p, ptr, len); __xkbc_memcpy(p as *mut _, ptr as *const _, len);
__xkbc_free(ptr as *mut _); __xkbc_free(ptr as *mut _);
} }
p p

View 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!()
}

View file

@ -2,7 +2,7 @@
use core::ffi::{c_char, c_int}; use core::ffi::{c_char, c_int};
#[allow(non_camel_case_types)] #[allow(non_camel_case_types, clippy::upper_case_acronyms)]
pub enum DIR {} pub enum DIR {}
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]

View file

@ -26,7 +26,11 @@ pub mod file;
pub unsafe extern "C" fn __xkbc_fopen(filename: *const c_char, mode: *const c_char) -> *mut FILE { pub unsafe extern "C" fn __xkbc_fopen(filename: *const c_char, mode: *const c_char) -> *mut FILE {
warn!( warn!(
"The xkbcommon library is attempting to open a file at path: {:?}", "The xkbcommon library is attempting to open a file at path: {:?}",
unsafe { CStr::from_ptr(filename) } if filename.is_null() {
None
} else {
Some(unsafe { CStr::from_ptr(filename) })
}
); );
null_mut() null_mut()
} }
@ -72,6 +76,15 @@ pub unsafe extern "C" fn __xkbc_fprintf(
unsafe { __xkbc_vfprintf(stream, format, args.as_va_list()) } unsafe { __xkbc_vfprintf(stream, format, args.as_va_list()) }
} }
#[unsafe(no_mangle)]
pub unsafe extern "C" fn __spre_fprintf(
stream: *mut FILE,
format: *const c_char,
args: ...
) -> c_int {
unsafe { __xkbc_fprintf(stream, format) }
}
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub unsafe extern "C" fn __xkbc_vfprintf( pub unsafe extern "C" fn __xkbc_vfprintf(
stream: *mut FILE, stream: *mut FILE,
@ -137,3 +150,43 @@ pub unsafe extern "C" fn __xkbc_vsnprintf(
string_length as c_int string_length as c_int
} }
} }
#[unsafe(no_mangle)]
pub unsafe extern "C" fn __spre_sprintf(
string: *mut c_char,
format: *const c_char,
mut args: ...
) -> c_int {
unsafe { vsprintf(string, format, args.as_va_list()) }
}
pub unsafe fn vsprintf(string: *mut c_char, format: *const c_char, ap: VaList) -> c_int {
let mut rust_buffer = String::new();
unsafe {
printf_compat::format(format, ap, fmt_write(&mut rust_buffer));
// __xkbc_strncpy would be preferrable, if it was available
__xkbc_memcpy(
string as *mut _,
rust_buffer.as_ptr() as *mut _,
rust_buffer.len(),
);
*string.add(rust_buffer.len()) = 0; // Add terminating null byte.
rust_buffer.len() as c_int
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn __spre_vsnprintf(
string: *mut c_char,
// Length in bytes **including the terminating null byte**.
size: c_size_t,
format: *const c_char,
ap: VaList,
) -> c_int {
unsafe { __xkbc_vsnprintf(string, size, format, ap) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn __spre_sscanf(s: *const c_char, format: *const c_char, ...) -> c_int {
todo!()
}

View file

@ -8,9 +8,11 @@ use core::{
use inout::file::{FILE, STDERR, STDIN, STDOUT}; use inout::file::{FILE, STDERR, STDIN, STDOUT};
pub mod alloc; pub mod alloc;
pub mod crypto;
pub mod gcc_runtime; pub mod gcc_runtime;
pub mod inout; pub mod inout;
pub mod string; pub mod string;
pub mod time;
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
pub type c_intmax_t = c_longlong; pub type c_intmax_t = c_longlong;
@ -38,6 +40,11 @@ pub unsafe extern "C" fn __xkbc___errno() -> *mut c_int {
todo!() todo!()
} }
#[unsafe(no_mangle)]
pub unsafe extern "C" fn __spre___errno() -> *mut c_int {
unsafe { __xkbc___errno() }
}
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub unsafe extern "C" fn __xkbc_qsort( pub unsafe extern "C" fn __xkbc_qsort(
base: *mut c_void, base: *mut c_void,
@ -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)] #[unsafe(no_mangle)]
pub unsafe extern "C" fn __xkbc_secure_getenv(name: *const c_char) -> *mut c_char { pub unsafe extern "C" fn __xkbc_secure_getenv(name: *const c_char) -> *mut c_char {
unsafe { __xkbc_getenv(name) } unsafe { __xkbc_getenv(name) }
@ -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 { pub unsafe extern "C" fn __xkbc_atoi(s: *const c_char) -> c_int {
todo!() todo!()
} }
// TODO: What is this even for?
#[unsafe(no_mangle)]
pub static __spre__ctype_: [c_char; 0] = [];
#[unsafe(no_mangle)]
pub unsafe extern "C" fn __spre_abort() -> ! {
panic!("`abort()` called.")
}

View file

@ -13,7 +13,7 @@ pub unsafe extern "C" fn __xkbc_memset(
c: c_int, c: c_int,
n: c_size_t, n: c_size_t,
) -> *mut c_void { ) -> *mut c_void {
if dest_original == null_mut() { if dest_original.is_null() {
if n > 0 { if n > 0 {
panic!("Attempted to memset a nullptr."); panic!("Attempted to memset a nullptr.");
} else { } else {
@ -33,6 +33,15 @@ pub unsafe extern "C" fn __xkbc_memset(
dest_original dest_original
} }
#[unsafe(no_mangle)]
pub unsafe extern "C" fn __spre_memset(
dest_original: *mut c_void,
c: c_int,
n: c_size_t,
) -> *mut c_void {
unsafe { __xkbc_memset(dest_original, c, n) }
}
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub unsafe extern "C" fn __xkbc_memmove( pub unsafe extern "C" fn __xkbc_memmove(
dst: *mut c_void, dst: *mut c_void,
@ -55,6 +64,15 @@ pub unsafe extern "C" fn __xkbc_memcpy(
dst dst
} }
#[unsafe(no_mangle)]
pub unsafe extern "C" fn __spre_memcpy(
dst: *mut c_void,
src: *const c_void,
count: c_size_t,
) -> *mut c_void {
unsafe { __xkbc_memcpy(dst, src, count) }
}
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub unsafe extern "C" fn __xkbc_memchr(s: *const c_void, c: c_int, n: c_size_t) -> *mut c_void { pub unsafe extern "C" fn __xkbc_memchr(s: *const c_void, c: c_int, n: c_size_t) -> *mut c_void {
let mut s = s as *const c_uchar; let mut s = s as *const c_uchar;
@ -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)] #[unsafe(no_mangle)]
pub unsafe extern "C" fn __xkbc_strpbrk(s: *const c_char, accept: *const c_char) -> *mut c_char { pub unsafe extern "C" fn __xkbc_strpbrk(s: *const c_char, accept: *const c_char) -> *mut c_char {
todo!() todo!()
@ -100,6 +123,11 @@ pub unsafe extern "C" fn __xkbc_strerror(errnum: c_int) -> *mut c_char {
todo!() todo!()
} }
#[unsafe(no_mangle)]
pub unsafe extern "C" fn __spre_strerror(errnum: c_int) -> *mut c_char {
unsafe { __xkbc_strerror(errnum) }
}
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub unsafe extern "C" fn __xkbc_strdup(string: *const c_char) -> *mut c_char { pub unsafe extern "C" fn __xkbc_strdup(string: *const c_char) -> *mut c_char {
strndup_inner(string, unsafe { __xkbc_strlen(string) }) strndup_inner(string, unsafe { __xkbc_strlen(string) })
@ -136,6 +164,11 @@ pub unsafe extern "C" fn __xkbc_strlen(mut s: *const c_char) -> c_size_t {
result result
} }
#[unsafe(no_mangle)]
pub unsafe extern "C" fn __spre_strlen(s: *const c_char) -> c_size_t {
unsafe { __xkbc_strlen(s) }
}
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub unsafe extern "C" fn __xkbc_strnlen(s: *const c_char, maxlen: c_size_t) -> c_size_t { pub unsafe extern "C" fn __xkbc_strnlen(s: *const c_char, maxlen: c_size_t) -> c_size_t {
let found: *const c_char = let found: *const c_char =
@ -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)] #[unsafe(no_mangle)]
pub unsafe extern "C" fn __xkbc_strchr(cs: *const c_char, c: c_int) -> *mut c_char { pub unsafe extern "C" fn __xkbc_strchr(cs: *const c_char, c: c_int) -> *mut c_char {
todo!() todo!()
@ -198,3 +240,12 @@ pub unsafe extern "C" fn __xkbc_strtol(
) -> c_long { ) -> c_long {
todo!() todo!()
} }
#[unsafe(no_mangle)]
pub unsafe extern "C" fn __spre_strtol(
nptr: *const c_char,
endptr: *mut *mut c_char,
base: c_int,
) -> c_long {
unsafe { __xkbc_strtol(nptr, endptr, base) }
}

View 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
}

View 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;

View file

@ -18,22 +18,30 @@ extern crate alloc;
use core::alloc::Layout; use core::alloc::Layout;
use core::cell::RefCell; use core::cell::RefCell;
use core::slice; use core::fmt::Write;
use core::sync::atomic::{AtomicBool, Ordering}; use core::sync::atomic::{AtomicBool, Ordering};
use alloc::boxed::Box; use alloc::boxed::Box;
use alloc::collections::vec_deque::VecDeque;
use alloc::format;
use alloc::string::String; use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec; use alloc::vec;
use 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_executor::Spawner;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::channel::Channel; use embassy_sync::channel::Channel;
use embassy_sync::mutex::Mutex;
use embassy_sync::signal::Signal; use embassy_sync::signal::Signal;
use embassy_time::{Duration, Instant}; use embassy_time::{Duration, Timer};
use esp_alloc::{HeapRegion, MemoryCapability}; use esp_alloc::{HeapRegion, MemoryCapability};
use esp_bootloader_esp_idf::partitions::PartitionTable;
use esp_hal::Blocking; use esp_hal::Blocking;
use esp_hal::clock::CpuClock; use esp_hal::clock::CpuClock;
use esp_hal::dma::{BurstConfig, DmaDescriptor, DmaTxBuf, ExternalBurstConfig}; use esp_hal::dma::{BurstConfig, DmaDescriptor, DmaTxBuf, ExternalBurstConfig};
use esp_hal::efuse::Efuse;
use esp_hal::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull}; use esp_hal::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull};
use esp_hal::i2c::master::{I2c, I2cAddress}; use esp_hal::i2c::master::{I2c, I2cAddress};
use esp_hal::interrupt::software::SoftwareInterruptControl; use esp_hal::interrupt::software::SoftwareInterruptControl;
@ -41,47 +49,56 @@ use esp_hal::lcd_cam::LcdCam;
use esp_hal::lcd_cam::lcd::dpi::Dpi; use esp_hal::lcd_cam::lcd::dpi::Dpi;
use esp_hal::mcpwm::{McPwm, PeripheralClockConfig}; use esp_hal::mcpwm::{McPwm, PeripheralClockConfig};
use esp_hal::psram::{FlashFreq, PsramConfig, PsramSize, SpiRamFreq, SpiTimingConfigCoreClock}; use esp_hal::psram::{FlashFreq, PsramConfig, PsramSize, SpiRamFreq, SpiTimingConfigCoreClock};
use esp_hal::ram;
use esp_hal::rng::TrngSource;
use esp_hal::sha::ShaBackend;
use esp_hal::system::Stack; use esp_hal::system::Stack;
use esp_hal::timer::timg::TimerGroup; use esp_hal::timer::timg::TimerGroup;
use esp_rtos::embassy::Executor; use esp_rtos::embassy::Executor;
use esp_storage::FlashStorage; use esp_storage::FlashStorage;
use log::{LevelFilter, error, info, warn}; use indoc::writedoc;
use itertools::Itertools;
use log::{error, info, warn};
use rmk::channel::{CONTROLLER_CHANNEL, ControllerSub}; 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::controller::{Controller, EventController};
use rmk::debounce::default_debouncer::DefaultDebouncer; use rmk::debounce::default_debouncer::DefaultDebouncer;
use rmk::descriptor::KeyboardReport;
use rmk::event::ControllerEvent; use rmk::event::ControllerEvent;
use rmk::hid::Report; use rmk::hid::Report;
use rmk::input_device::Runnable; use rmk::input_device::Runnable;
use rmk::join_all;
use rmk::keyboard::Keyboard; use rmk::keyboard::Keyboard;
use rmk::storage::async_flash_wrapper; use rmk::storage::async_flash_wrapper;
use rmk::types::action::{Action, KeyAction}; use rmk::types::action::{Action, KeyAction};
use rmk::types::keycode::{HidKeyCode, KeyCode}; use rmk::join_all;
use rmk::{initialize_keymap_and_storage, run_devices, run_rmk}; use rmk::{initialize_keymap_and_storage, run_devices, run_rmk};
use slint::ComponentHandle;
use slint::platform::software_renderer::Rgb565Pixel; use slint::platform::software_renderer::Rgb565Pixel;
use static_cell::StaticCell; use static_cell::StaticCell;
use ui::AppWindow;
use xkbcommon::xkb::{self, FeedResult, KeyDirection, Keysym, Status};
use {esp_alloc as _, esp_backtrace as _}; use {esp_alloc as _, esp_backtrace as _};
use crate::keymap::{KEY_MESSAGE_CHANNEL, create_hid_report_interceptor};
use crate::logging::LOG_LEVEL_FILTER;
use crate::matrix::IoeMatrix; use crate::matrix::IoeMatrix;
use crate::peripherals::st7701s::St7701s; use crate::peripherals::st7701s::St7701s;
use crate::proxy::create_hid_report_interceptor;
use crate::ui::backend::{FramebufferPtr, SlintBackend}; use crate::ui::backend::{FramebufferPtr, SlintBackend};
use crate::vial::{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"]; mutually_exclusive_features::none_or_one_of!["usb-log", "alt-log", "rtt-log"];
mod config;
mod crypto;
mod db;
mod ffi; mod ffi;
mod keymap;
mod logging; mod logging;
mod matrix; mod matrix;
mod peripherals; mod peripherals;
mod proxy;
mod ui; mod ui;
mod util;
mod vial; mod vial;
#[cfg(feature = "alt-log")] #[cfg(feature = "alt-log")]
@ -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> // For more information see: <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/app_image_format.html#application-description>
esp_bootloader_esp_idf::esp_app_desc!(); esp_bootloader_esp_idf::esp_app_desc!();
// Memory allocation regions.
// These can be debugged using `xtensa-esp32s3-elf-size -A <path-to-binary>`.
// A panic such as `memory allocation of 3740121773 bytes failed` is caused by a heap overflow. The size is `DEEDBAAD` in hex.
/// Total heap size
const HEAP_SIZE: usize = 112 * 1024;
/// Size of the app core's stack
const STACK_SIZE_CORE_APP: usize = 80 * 1024;
// const FRAME_DURATION_MIN: Duration = Duration::from_millis(40); // 25 FPS // const FRAME_DURATION_MIN: Duration = Duration::from_millis(40); // 25 FPS
const FRAME_DURATION_MIN: Duration = Duration::from_millis(100); // 10 FPS const FRAME_DURATION_MIN: Duration = Duration::from_millis(100); // 10 FPS
@ -107,15 +133,6 @@ static SIGNAL_UI_RENDER: Signal<CriticalSectionRawMutex, ()> = Signal::new();
#[esp_rtos::main] #[esp_rtos::main]
async fn main(_spawner: Spawner) { async fn main(_spawner: Spawner) {
#[cfg(feature = "usb-log")]
{
esp_println::logger::init_logger(LOG_LEVEL_FILTER);
info!("Logger initialized!");
}
#[cfg(feature = "rtt-log")]
rtt_target::rtt_init_log!(LOG_LEVEL_FILTER);
let config = esp_hal::Config::default() let config = esp_hal::Config::default()
.with_cpu_clock(CpuClock::max()) .with_cpu_clock(CpuClock::max())
.with_psram(PsramConfig { .with_psram(PsramConfig {
@ -125,30 +142,24 @@ async fn main(_spawner: Spawner) {
ram_frequency: SpiRamFreq::Freq80m, ram_frequency: SpiRamFreq::Freq80m,
}); });
let peripherals: esp_hal::peripherals::Peripherals = esp_hal::init(config); let peripherals: esp_hal::peripherals::Peripherals = esp_hal::init(config);
info!("System initialized!");
#[cfg(feature = "usb-log")]
let console_task = logging::usb::setup_logging();
#[cfg(feature = "alt-log")] #[cfg(feature = "alt-log")]
let alt_uart_rx_task = { let console_task =
use esp_hal::uart::Uart; logging::uart::setup_logging(peripherals.UART2, peripherals.GPIO12, peripherals.GPIO5);
#[cfg(feature = "rtt-log")]
let (uart_rx, uart_tx) = Uart::new(peripherals.UART2, Default::default()) let console_task = logging::rtt::setup_logging();
.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 {};
// Use the internal DRAM as the heap. // Use the internal DRAM as the heap.
// TODO: Can we combine the regular link section with dram2? // Memory reclaimed from the esp-idf bootloader.
// esp_alloc::heap_allocator!(size: 80 * 1024); const HEAP_SIZE_RECLAIMED: usize = const {
// esp_alloc::heap_allocator!(#[unsafe(link_section = ".dram2_uninit")] size: 72 * 1024); let range = esp_metadata_generated::memory_range!("DRAM2_UNINIT");
esp_alloc::heap_allocator!(#[unsafe(link_section = ".dram2_uninit")] size: 64 * 1024); 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()); info!("Heap initialized! {:#?}", esp_alloc::HEAP.stats());
// Initialize the PSRAM allocator. // Initialize the PSRAM allocator.
@ -161,10 +172,12 @@ async fn main(_spawner: Spawner) {
MemoryCapability::External.into(), MemoryCapability::External.into(),
)); ));
} }
info!(
"PSRAM allocator initialized with capacity of {} MiB!",
psram_size / 1024 / 1024
);
} }
info!("PSRAM allocator initialized!");
// let mut io = Io::new(peripherals.IO_MUX); // let mut io = Io::new(peripherals.IO_MUX);
// io.set_interrupt_handler(interrupt_handler); // io.set_interrupt_handler(interrupt_handler);
@ -187,6 +200,9 @@ async fn main(_spawner: Spawner) {
let mut _pwm = McPwm::new(peripherals.MCPWM0, PeripheralClockConfig::with_prescaler(1)); let mut _pwm = McPwm::new(peripherals.MCPWM0, PeripheralClockConfig::with_prescaler(1));
let mut _pwm_pin = Output::new(peripherals.GPIO21, Level::High, OutputConfig::default()); let mut _pwm_pin = Output::new(peripherals.GPIO21, Level::High, OutputConfig::default());
let mut sha_backend = ShaBackend::new(peripherals.SHA);
let _sha_driver_handle = sha_backend.start();
let timg0 = TimerGroup::new(peripherals.TIMG0); let timg0 = TimerGroup::new(peripherals.TIMG0);
let software_interrupt = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); let software_interrupt = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
esp_rtos::start( esp_rtos::start(
@ -195,16 +211,16 @@ async fn main(_spawner: Spawner) {
info!("ESP-RTOS started!"); info!("ESP-RTOS started!");
// Enable the TRNG source, so `Trng` can be constructed.
let _trng_source = TrngSource::new(peripherals.RNG, peripherals.ADC1);
#[cfg(feature = "ble")] #[cfg(feature = "ble")]
let mut host_resources = rmk::HostResources::new(); let mut host_resources = rmk::HostResources::new();
#[cfg(feature = "ble")] #[cfg(feature = "ble")]
let stack = { let stack = {
// Enable the TRNG source, so `Trng` can be constructed.
use bt_hci::controller::ExternalController; use bt_hci::controller::ExternalController;
use esp_hal::rng::TrngSource;
use esp_radio::{Controller as RadioController, ble::controller::BleConnector}; use esp_radio::{Controller as RadioController, ble::controller::BleConnector};
let _trng_source = TrngSource::new(peripherals.RNG, peripherals.ADC1);
let mut rng = esp_hal::rng::Trng::try_new().unwrap(); let mut rng = esp_hal::rng::Trng::try_new().unwrap();
static RADIO: StaticCell<RadioController<'static>> = StaticCell::new(); static RADIO: StaticCell<RadioController<'static>> = StaticCell::new();
let radio = RADIO.init(esp_radio::init().unwrap()); let radio = RADIO.init(esp_radio::init().unwrap());
@ -224,15 +240,15 @@ async fn main(_spawner: Spawner) {
// Initialize USB // Initialize USB
#[cfg(not(feature = "no-usb"))] #[cfg(not(feature = "no-usb"))]
let usb_driver = { let usb_driver = {
use core::ptr::addr_of_mut;
use esp_hal::otg_fs::Usb; use esp_hal::otg_fs::Usb;
use esp_hal::otg_fs::asynch::{Config, Driver}; use esp_hal::otg_fs::asynch::{Config, Driver};
static 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); let usb = Usb::new(peripherals.USB0, peripherals.GPIO20, peripherals.GPIO19);
// Create the driver, from the HAL. // Create the driver, from the HAL.
let config = Config::default(); let config = Config::default();
let driver = Driver::new(usb, unsafe { &mut *addr_of_mut!(EP_MEMORY) }, config); let driver = Driver::new(usb, ep_memory, config);
info!("USB driver for RMK built!"); info!("USB driver for RMK built!");
@ -240,7 +256,20 @@ async fn main(_spawner: Spawner) {
}; };
// Initialize the flash // Initialize the flash
let flash = FlashStorage::new(peripherals.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. // Flash memory may not be written to while another core is executing from it.
// By default, `FlashStorage` is configured to abort the operation and log an error message. // By default, `FlashStorage` is configured to abort the operation and log an error message.
// However, it can also be configured to auto-park the other core, such that writing to // However, it can also be configured to auto-park the other core, such that writing to
@ -249,7 +278,68 @@ async fn main(_spawner: Spawner) {
// to avoid having to park the other core, which could result in better performance. // to avoid having to park the other core, which could result in better performance.
// Invalid configuration would then present itself as freezing/UB. // Invalid configuration would then present itself as freezing/UB.
.multicore_auto_park(); .multicore_auto_park();
let flash = async_flash_wrapper(flash); 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!"); info!("Flash memory configured!");
@ -295,27 +385,55 @@ async fn main(_spawner: Spawner) {
// RMK config // RMK config
let vial_config = VialConfig::new(VIAL_KEYBOARD_ID, VIAL_KEYBOARD_DEF, &[(0, 0), (1, 1)]); let vial_config = VialConfig::new(VIAL_KEYBOARD_ID, VIAL_KEYBOARD_DEF, &[(0, 0), (1, 1)]);
let storage_config = StorageConfig { let storage_config = StorageConfig {
start_addr: 0x3f0000, start_addr: 0,
num_sectors: 16, 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() ..Default::default()
}; };
// Retrieve the hardware-unique MAC address.
let mac_address = Efuse::read_base_mac_address();
static SERIAL_NUMBER: StaticCell<Box<str>> = StaticCell::new();
let serial_number = SERIAL_NUMBER.init_with(|| {
/// A magic prefix string that is required for the device to be recognized by the Vial GUI.
const VIAL_SERIAL_PREFIX: &str = "vial:f64c2b3c";
format!(
"{VIAL_SERIAL_PREFIX}:acid:{:02x}",
mac_address.iter().format("")
)
.into_boxed_str()
});
let device_config = DeviceConfig {
vid: VIAL_VENDOR_ID,
pid: VIAL_PRODUCT_ID,
manufacturer: "",
product_name: VIAL_KEYBOARD_NAME,
serial_number,
};
info!("RMK Device Config: {device_config:#04x?}");
let rmk_config = RmkConfig { let rmk_config = RmkConfig {
device_config,
vial_config, vial_config,
storage_config, storage_config,
..Default::default()
}; };
// Initialze keyboard stuffs // Initialze keyboard stuffs
// Initialize the storage and keymap // Initialize the storage and keymap
let mut default_keymap = keymap::get_default_keymap(); let mut default_keymap = config::get_default_keymap();
let mut behavior_config = BehaviorConfig::default(); let mut behavior_config = config::get_behavior_config();
let mut per_key_config = PositionalConfig::default(); let mut positional_config = config::get_positional_config();
let (keymap, mut storage) = initialize_keymap_and_storage( let (keymap, mut storage) = initialize_keymap_and_storage(
&mut default_keymap, &mut default_keymap,
flash, flash_part_rmk,
&storage_config, &storage_config,
&mut behavior_config, &mut behavior_config,
&mut per_key_config, &mut positional_config,
) )
.await; .await;
@ -355,7 +473,7 @@ async fn main(_spawner: Spawner) {
let window_size = [framebuffer.height, framebuffer.width]; let window_size = [framebuffer.height, framebuffer.width];
let framebuffer_ptr = FramebufferPtr(framebuffer.as_target_pixels() as _); let framebuffer_ptr = FramebufferPtr(framebuffer.as_target_pixels() as _);
static SECOND_CORE_STACK: StaticCell<Stack<{ 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()); let second_core_stack = SECOND_CORE_STACK.init(Stack::new());
esp_rtos::start_second_core( esp_rtos::start_second_core(
peripherals.CPU_CTRL, peripherals.CPU_CTRL,
@ -377,8 +495,10 @@ async fn main(_spawner: Spawner) {
window_size, window_size,
window: RefCell::new(None), window: RefCell::new(None),
framebuffer: framebuffer_ptr, framebuffer: framebuffer_ptr,
quit_event_loop: Default::default(),
events: Arc::new(critical_section::Mutex::new(RefCell::new(VecDeque::new()))),
}; };
spawner.must_spawn(ui::run_renderer_task(slint_backend)); 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. // TODO: Probably want to select! instead and re-try.
join_all![ join_all![
run_alloc_stats_reporter(),
// We currently send the framebuffer data using the main core, which does not seem to slow // We currently send the framebuffer data using the main core, which does not seem to slow
// down the rest of the tasks too much. // down the rest of the tasks too much.
run_lcd(st7701s, framebuffer), run_lcd(st7701s, framebuffer),
@ -409,11 +530,39 @@ async fn main(_spawner: Spawner) {
), ),
create_hid_report_interceptor(), create_hid_report_interceptor(),
user_controller.event_loop(), user_controller.event_loop(),
alt_uart_rx_task console_task
] ]
.await; .await;
} }
async fn run_alloc_stats_reporter() {
let mut psram_used_prev = 0;
let mut heap_used_prev = 0;
loop {
let psram_stats = PSRAM_ALLOCATOR.stats();
let heap_stats = esp_alloc::HEAP.stats();
if psram_stats.current_usage != psram_used_prev {
let difference = psram_stats.current_usage as isize - psram_used_prev as isize;
psram_used_prev = psram_stats.current_usage;
warn!(
"PSRAM usage changed: {}{}\n{psram_stats}",
if difference < 0 { '-' } else { '+' },
difference.abs()
);
}
if heap_stats.current_usage != heap_used_prev {
let difference = heap_stats.current_usage as isize - heap_used_prev as isize;
heap_used_prev = heap_stats.current_usage;
warn!(
"HEAP usage changed: {}{}\n{heap_stats}",
if difference < 0 { '-' } else { '+' },
difference.abs()
);
}
Timer::after_secs(1).await;
}
}
struct UserController { struct UserController {
sub: ControllerSub, sub: ControllerSub,
} }
@ -430,12 +579,11 @@ impl Controller for UserController {
type Event = ControllerEvent; type Event = ControllerEvent;
async fn process_event(&mut self, event: Self::Event) { async fn process_event(&mut self, event: Self::Event) {
if let ControllerEvent::Key( if let ControllerEvent::Key(keyboard_event, KeyAction::Single(Action::User(user_key_index))) =
keyboard_event, event
KeyAction::Single(Action::User(user_key_index)), && user_key_index == CustomKeycodes::FOCUS_LCD as u8
) = event && 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 { match enabled {
@ -446,9 +594,7 @@ impl Controller for UserController {
} }
true => { true => {
info!("Enabling LCD."); info!("Enabling LCD.");
*rmk::channel::KEYBOARD_REPORT_SENDER.write().await = *rmk::channel::KEYBOARD_REPORT_SENDER.write().await = &KEYBOARD_REPORT_PROXY;
&KEYBOARD_REPORT_PROXY;
}
} }
} }
} }
@ -521,7 +667,7 @@ async fn run_lcd(mut st7701s: St7701s<'static, Blocking>, framebuffer: &'static
// TODO: Use bounce buffers: // TODO: Use bounce buffers:
// https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32s3/api-reference/peripherals/lcd.html#bounce-buffer-with-single-psram-frame-buffer // https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32s3/api-reference/peripherals/lcd.html#bounce-buffer-with-single-psram-frame-buffer
// 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()) { let transfer = match st7701s.dpi.send(false, framebuffer.dma_buf.take().unwrap()) {
Err((error, result_dpi, result_dma_buf)) => { Err((error, result_dpi, result_dma_buf)) => {
error!( error!(

View file

@ -13,6 +13,8 @@ use rmk::{
matrix::{KeyState, MatrixTrait}, matrix::{KeyState, MatrixTrait},
}; };
use crate::config::{MATRIX_AREA, MATRIX_COLS, MATRIX_ROWS};
pub struct RaiiGuard<F: FnOnce()> { pub struct RaiiGuard<F: FnOnce()> {
on_drop: Option<F>, on_drop: Option<F>,
} }
@ -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 /// IO Expander Matrix
pub struct IoeMatrix<D> pub struct IoeMatrix<D>
where where

View file

@ -107,6 +107,19 @@ pub async fn spi_read(
use crate::peripherals::st7701s::*; 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! { lazy_static! {
pub static ref INIT_SEQUENCE_COMMANDS: Vec<(Vec<Box<dyn Command + Send + Sync>>, u64)> = vec![ pub static ref INIT_SEQUENCE_COMMANDS: Vec<(Vec<Box<dyn Command + Send + Sync>>, u64)> = vec![
(vec![ (vec![

View file

@ -3,13 +3,15 @@ use esp_hal::{
DriverMode, DriverMode,
gpio::{Flex, Level, Output}, gpio::{Flex, Level, Output},
lcd_cam::lcd::{ lcd_cam::lcd::{
ClockMode, DelayMode, Phase, Polarity, dpi::{Dpi, Format, FrameTiming} ClockMode, DelayMode, Phase, Polarity,
dpi::{Dpi, Format, FrameTiming},
}, },
time::Rate, time::Rate,
}; };
use lcd::spi_write; use lcd::spi_write;
use log::debug; use log::debug;
use paste::paste; use paste::paste;
// use tinyvec::ArrayVec;
mod lcd; mod lcd;
@ -18,6 +20,30 @@ pub trait Command {
fn args_iter(&'_ self) -> core::slice::Iter<'_, u8>; 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 struct CustomCommand {
pub address: u8, pub address: u8,
pub args: &'static [u8], pub args: &'static [u8],

View 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);

View 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(())
}
}

View 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 },
}

View 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(),
}
}
}

View 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)
}
}

View 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."),
}
}

View file

@ -0,0 +1,3 @@
export global Style {
in property <length> spacing: 8px;
}

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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 { }
}

View 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;
}
}
}
}
}

View 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 { }
}
}

View 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");
}
}
}

View 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");
}
}
}

View 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;
}

View file

@ -47,6 +47,36 @@
"name": "FOCUS_LCD", "name": "FOCUS_LCD",
"title": "Focus LCD", "title": "Focus LCD",
"shortName": "Focus\nLCD" "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": { "layouts": {

View file

@ -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
View 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

View file

@ -10,10 +10,8 @@ BUILD_DIR_NAME="$LIBXKBCOMMON_DIR/$1"
STATIC_LIB_PATH="$BUILD_DIR_NAME/libxkbcommon.a" STATIC_LIB_PATH="$BUILD_DIR_NAME/libxkbcommon.a"
SETUP_ARGS=${@:2} SETUP_ARGS=${@:2}
git submodule update --init --recursive
pushd "$LIBXKBCOMMON_DIR" >/dev/null pushd "$LIBXKBCOMMON_DIR" >/dev/null
meson setup "$BUILD_DIR_NAME" \ meson setup "$BUILD_DIR_NAME" \
--wipe \
--cross-file "$SCRIPT_DIR/cross-esp32s3.txt" \ --cross-file "$SCRIPT_DIR/cross-esp32s3.txt" \
-Denable-x11=false \ -Denable-x11=false \
-Denable-wayland=false \ -Denable-wayland=false \
@ -23,8 +21,8 @@ pushd "$LIBXKBCOMMON_DIR" >/dev/null
-Dxkb-config-root=/usr/share/X11/xkb \ -Dxkb-config-root=/usr/share/X11/xkb \
-Dx-locale-root=/usr/share/X11/locale \ -Dx-locale-root=/usr/share/X11/locale \
$SETUP_ARGS $SETUP_ARGS
meson compile -C "$BUILD_DIR_NAME" meson compile -C "$BUILD_DIR_NAME" xkbcommon
$SCRIPT_DIR/libxkbcommon-redefine-syms.sh "$STATIC_LIB_PATH" "$STATIC_LIB_PATH" $SCRIPT_DIR/redefine-syms.sh "__xkbc_" "$STATIC_LIB_PATH" "$STATIC_LIB_PATH"
popd >/dev/null popd >/dev/null
GREEN='\033[0;32m' GREEN='\033[0;32m'

View file

@ -0,0 +1,2 @@
[env] # These must be kept in sync with /.zed/settings.json
LIBSODIUM_INSTALL_DIR = "../libsodium/install-host"

View 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 }

View 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]
```

View 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!");
}
}
}

View 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()
}

View 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);
}
}
}

View 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);
}

View file

@ -1,10 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
INPUT_LIB=$1 PREFIX=$1
OUTPUT_LIB=$2 INPUT_LIB=$2
PREFIX="__xkbc_" OUTPUT_LIB=$3
if [ "$#" -ne 2 ]; then if [ "$#" -ne 3 ]; then
echo "Usage: $0 <input_lib.a> <output_lib.a>" echo "Usage: $0 <prefix> <input_lib.a> <output_lib.a>"
exit 1 exit 1
fi fi

View file

@ -1,2 +0,0 @@
[toolchain]
channel = "esp"

@ -0,0 +1 @@
Subproject commit 733e6d6d73bc9462d8ec9528dd96eee23846a3e3

16
firmware/spectre-api-compile.sh Executable file
View 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

File diff suppressed because it is too large Load diff

View file

@ -1,5 +0,0 @@
#![no_std]
#![deny(clippy::mem_forget)]
#![feature(macro_metavar_expr)]
pub mod st7701s;

File diff suppressed because it is too large Load diff

View file

@ -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" }

Some files were not shown because too many files have changed in this diff Show more