Compare commits
69 commits
libxkbcomm
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
014b099986 | ||
|
|
a24774c925 | ||
|
|
d555c908a2 | ||
|
|
c7e0ec45ca | ||
|
|
8a99905a84 | ||
|
|
c3f58178fc | ||
|
|
e0b0aded7b | ||
|
|
76ed47d687 | ||
|
|
3e52243efe | ||
|
|
bf5957a8bf | ||
|
|
682522d556 | ||
|
|
6f42bf7a7b | ||
|
|
3b291669ca | ||
|
|
07efc876f3 | ||
|
|
2ea4e57857 | ||
|
|
95464568c7 | ||
|
|
376416c32e | ||
|
|
735d0a48bb | ||
|
|
8f55f23840 | ||
|
|
f20f4e7993 | ||
|
|
ebf8205f2d | ||
|
|
04f4070634 | ||
|
|
7e1f0d1b68 | ||
|
|
b8e3987139 | ||
|
|
74d7b6df5f | ||
|
|
ea58ef0c8e | ||
|
|
707c994a76 | ||
|
|
5004e8dfdf | ||
|
|
8e304540ea | ||
|
|
ab506de76a | ||
|
|
a09da0c8a7 | ||
|
|
3b364c64c2 | ||
|
|
1e2d43a628 | ||
|
|
4a5ada0bb0 | ||
|
|
d4c8d69cf3 | ||
|
|
40b9b5d278 | ||
|
|
3ac1656d33 | ||
|
|
b6d9a71b59 | ||
|
|
c2e3f1bec3 | ||
|
|
f8ef06ee0c | ||
|
|
5592708271 | ||
|
|
2b8dfa7b44 | ||
|
|
3947215a23 | ||
|
|
5f34f078db | ||
|
|
d4aad0e8cd | ||
|
|
3b24825677 | ||
|
|
8426852d7c | ||
|
|
9c2a614aff | ||
|
|
b33f4852b2 | ||
|
|
0cb6209d4b | ||
|
|
a3a95b179b | ||
|
|
9aa5430851 | ||
|
|
bbbaea803b | ||
|
|
6cd7b32bee | ||
|
|
2a5779ffcf | ||
|
|
299a1195f1 | ||
|
|
7fca722f24 | ||
|
|
16ed51b19e | ||
|
|
810f21827b | ||
|
|
ee17cc9f57 | ||
|
|
d1dd4abc06 | ||
|
|
47e6c890ca | ||
|
|
a5a5ee9330 | ||
|
|
dbdfa8ae44 | ||
|
|
3c695be996 | ||
|
|
b5535d6f52 | ||
|
|
35c017535e | ||
|
|
c98acc4da4 | ||
|
|
24daa0ad29 |
8
.gitmodules
vendored
|
|
@ -1,3 +1,9 @@
|
||||||
[submodule "firmware2/libxkbcommon"]
|
[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
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
[target.xtensa-esp32s3-none-elf]
|
|
||||||
runner = "espflash flash --monitor --chip esp32s3"
|
|
||||||
|
|
||||||
[env]
|
|
||||||
ESP_LOG="info"
|
|
||||||
|
|
||||||
[build]
|
|
||||||
rustflags = [
|
|
||||||
"-C", "link-arg=-nostartfiles",
|
|
||||||
"-Z", "stack-protector=all",
|
|
||||||
]
|
|
||||||
|
|
||||||
target = "xtensa-esp32s3-none-elf"
|
|
||||||
|
|
||||||
[unstable]
|
|
||||||
build-std = ["alloc", "core"]
|
|
||||||
2
firmware/.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Convert to LF line endings on checkout.
|
||||||
|
*.sh text eol=lf
|
||||||
42
firmware/.github/workflows/rust_ci.yml
vendored
|
|
@ -1,42 +0,0 @@
|
||||||
name: Continuous Integration
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths-ignore:
|
|
||||||
- "**/README.md"
|
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
rust-checks:
|
|
||||||
name: Rust Checks
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
action:
|
|
||||||
- command: build
|
|
||||||
args: --release
|
|
||||||
- command: fmt
|
|
||||||
args: --all -- --check
|
|
||||||
- command: clippy
|
|
||||||
args: --all-features --workspace -- -D warnings
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Setup Rust
|
|
||||||
uses: esp-rs/xtensa-toolchain@v1.5
|
|
||||||
with:
|
|
||||||
default: true
|
|
||||||
buildtargets: esp32s3
|
|
||||||
ldproxy: false
|
|
||||||
- name: Enable caching
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
- name: Run command
|
|
||||||
run: cargo ${{ matrix.action.command }} ${{ matrix.action.args }}
|
|
||||||
21
firmware/.gitignore
vendored
|
|
@ -1,19 +1,2 @@
|
||||||
# will have compiled files and executables
|
/.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/
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"rust-analyzer.linkedProjects": [
|
||||||
|
"acid-firmware/Cargo.toml"
|
||||||
|
],
|
||||||
|
"rust-analyzer.cargo.noDefaultFeatures": true,
|
||||||
|
"rust-analyzer.cargo.features": ["develop"],
|
||||||
|
"rust-analyzer.cargo.target": "xtensa-esp32s3-none-elf",
|
||||||
|
"rust-analyzer.cargo.targetDir": "target/rust-analyzer",
|
||||||
|
"rust-analyzer.cargo.extraEnv": {
|
||||||
|
"RUSTUP_TOOLCHAIN": "esp"
|
||||||
|
},
|
||||||
|
"rust-analyzer.check.extraArgs": [
|
||||||
|
"-Zbuild-std=core,alloc"
|
||||||
|
],
|
||||||
|
"rust-analyzer.check.extraEnv": {
|
||||||
|
"EXPLICITLY_INCLUDE_DEFAULT_DIRS": "true",
|
||||||
|
"XKBCOMMON_BUILD_DIR": "../libxkbcommon/build-esp32s3",
|
||||||
|
"SPECTRE_API_BUILD_DIR": "../spectre-api-c/build-esp32s3",
|
||||||
|
"SODIUM_INSTALL_DIR": "../libsodium/install",
|
||||||
|
"XKBCOMMON_BUILD_DIR_NAME": "build-esp32s3",
|
||||||
|
"SPECTRE_API_BUILD_DIR_NAME": "build-esp32s3",
|
||||||
|
"SODIUM_INSTALL_DIR_NAME": "install",
|
||||||
|
"SLINT_FONT_SIZES": "8,11,10,12,13,14,15,16,18,20,22,24,32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,9 +4,15 @@
|
||||||
{
|
{
|
||||||
"label": "rust: cargo build",
|
"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
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Project-local debug tasks
|
||||||
|
//
|
||||||
|
// For more documentation on how to configure debug tasks,
|
||||||
|
// see: https://zed.dev/docs/debugger
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"label": "Launch probe-rs debugging",
|
||||||
|
"adapter": "probe-rs",
|
||||||
|
"request": "launch",
|
||||||
|
"cwd": "$ZED_WORKTREE_ROOT",
|
||||||
|
"server": "127.0.0.1:50000 ",
|
||||||
|
"coreConfigs": [],
|
||||||
|
},
|
||||||
|
]
|
||||||
39
firmware/.zed/settings.json
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"lsp": {
|
||||||
|
"rust-analyzer": {
|
||||||
|
"initialization_options": {
|
||||||
|
// Which project to enable
|
||||||
|
"linkedProjects": ["acid-firmware/Cargo.toml"],
|
||||||
|
"cargo": {
|
||||||
|
// This must match the target MCU's target
|
||||||
|
"target": "xtensa-esp32s3-none-elf",
|
||||||
|
// Prevents rust-analyzer from blocking cargo
|
||||||
|
"targetDir": "target/rust-analyzer",
|
||||||
|
"extraEnv": {
|
||||||
|
// ESP32-S3 is an Xtensa MCU, we need to work with the esp toolchain
|
||||||
|
"RUSTUP_TOOLCHAIN": "esp",
|
||||||
|
// Rest of the environment variables must be kept in sync with `acid-firmware/.cargo/config.toml`:
|
||||||
|
"EXPLICITLY_INCLUDE_DEFAULT_DIRS": "true",
|
||||||
|
"XKBCOMMON_BUILD_DIR": "../libxkbcommon/build-esp32s3",
|
||||||
|
"SPECTRE_API_BUILD_DIR": "../spectre-api-c/build-esp32s3",
|
||||||
|
"SODIUM_INSTALL_DIR": "../libsodium/install",
|
||||||
|
"XKBCOMMON_BUILD_DIR_NAME": "build-esp32s3",
|
||||||
|
"SPECTRE_API_BUILD_DIR_NAME": "build-esp32s3",
|
||||||
|
"SODIUM_INSTALL_DIR_NAME": "install",
|
||||||
|
"SPECTRE_API_SYS_CC": "xtensa-esp32s3-elf-cc.exe",
|
||||||
|
"ESP_LOG": "warn",
|
||||||
|
"ESP_BACKTRACE_CONFIG_BACKTRACE_FRAMES": "20",
|
||||||
|
"SLINT_FONT_SIZES": "8,11,10,12,13,14,15,16,18,20,22,24,32",
|
||||||
|
"ACID_KEYMAP_PATH": "../keymaps/cz_coder.xkb",
|
||||||
|
"ACID_COMPOSE_PATH": "../compose/cs_CZ Compose.txt",
|
||||||
|
"ACID_COMPOSE_LOCALE": "cs_CZ.UTF-8",
|
||||||
|
},
|
||||||
|
"extraArgs": ["-Zbuild-std=core,alloc"],
|
||||||
|
// Enable device support and a wide set of features on the esp-rtos crate.
|
||||||
|
"noDefaultFeatures": true,
|
||||||
|
"features": ["develop"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
7994
firmware/Cargo.lock
generated
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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?
|
|
||||||
54
firmware/acid-firmware/.cargo/config.toml
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
[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",
|
||||||
|
|
||||||
|
# Output linker map
|
||||||
|
# "-C", "link-arg=-Wl,-Map=target/linker.map"
|
||||||
|
]
|
||||||
|
|
||||||
|
[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"]
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
esp-backtrace = { git = "https://github.com/Limeth/esp-hal.git", rev = "114977583886be4ed866ad7b7c6f16865148e899" }
|
||||||
|
esp-println = { git = "https://github.com/Limeth/esp-hal.git", rev = "114977583886be4ed866ad7b7c6f16865148e899" }
|
||||||
|
# esp-hal = { git = "https://github.com/Limeth/esp-hal.git", rev = "95d8c8b046e945e41294d5577528d0a1c4b03247" }
|
||||||
|
# esp-storage = { git = "https://github.com/Limeth/esp-hal.git", rev = "95d8c8b046e945e41294d5577528d0a1c4b03247" }
|
||||||
|
# esp-alloc = { git = "https://github.com/Limeth/esp-hal.git", rev = "95d8c8b046e945e41294d5577528d0a1c4b03247" }
|
||||||
|
# esp-radio = { git = "https://github.com/Limeth/esp-hal.git", rev = "95d8c8b046e945e41294d5577528d0a1c4b03247" }
|
||||||
|
# esp-rtos = { git = "https://github.com/Limeth/esp-hal.git", rev = "95d8c8b046e945e41294d5577528d0a1c4b03247" }
|
||||||
|
# esp-bootloader-esp-idf = { git = "https://github.com/Limeth/esp-hal.git", rev = "95d8c8b046e945e41294d5577528d0a1c4b03247" }
|
||||||
|
# esp-sync = { git = "https://github.com/Limeth/esp-hal.git", rev = "95d8c8b046e945e41294d5577528d0a1c4b03247" }
|
||||||
134
firmware/acid-firmware/Cargo.toml
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
[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", "no-alloc-tracing"]
|
||||||
|
# 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 = []
|
||||||
|
# Avoid entering the critical section for the whole duration of printing a message to console.
|
||||||
|
racy-logging = []
|
||||||
|
# Global allocator tracing proxy
|
||||||
|
no-alloc-tracing = ["esp-alloc/global-allocator"]
|
||||||
|
|
||||||
|
[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", git = "https://github.com/esp-rs/esp-hal", rev = "ee6e26f2fefa4da2168c95839bf618e1ecc22cc1", default-features = false, features = ["esp32s3", "nightly", "compat"] }
|
||||||
|
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", "latest_stable_rust"] }
|
||||||
|
esp-metadata-generated = { version = "0.3.0", features = ["esp32s3"] }
|
||||||
|
hex = { version = "0.4.3", default-features = false, features = ["alloc"] }
|
||||||
|
indoc = "2.0.7"
|
||||||
|
ouroboros = "0.18.5"
|
||||||
|
esp-hal-bounce-buffers = { git = "https://forgejo.limeth.cz/limeth/esp-hal-bounce-buffers", rev = "8d3763a190368f476aed6d98777264c959bfdc2d", features = ["esp32s3"] }
|
||||||
|
|
||||||
|
# A fork of slint with patches for `allocator_api` support.
|
||||||
|
# Don't forget to change `slint-build` in build dependencies, if this is changed.
|
||||||
|
slint = { version = "1.14.1", git = "https://github.com/Limeth/slint", rev = "c2e5d05df2476557a299a78664e148d2fe62427d", default-features = false, features = ["compat-1-2", "libm", "log", "unsafe-single-threaded", "renderer-software", "serde"] }
|
||||||
|
i-slint-common = { version = "1.14.1", git = "https://github.com/Limeth/slint", rev = "c2e5d05df2476557a299a78664e148d2fe62427d" }
|
||||||
|
i-slint-core = { version = "1.14.1", git = "https://github.com/Limeth/slint", rev = "c2e5d05df2476557a299a78664e148d2fe62427d", default-features = false }
|
||||||
|
|
||||||
|
# Crates for serial UART CLI
|
||||||
|
embedded-cli = { version = "0.2.1", default-features = false, features = ["help", "macros"] }
|
||||||
|
embedded-io = "0.6"
|
||||||
|
mutually_exclusive_features = "0.1.0"
|
||||||
|
|
||||||
|
[dependencies.ekv]
|
||||||
|
version = "1.0.0"
|
||||||
|
features = [
|
||||||
|
# TODO: "defmt",
|
||||||
|
"crc",
|
||||||
|
"max-page-count-2048",
|
||||||
|
"max-key-size-256",
|
||||||
|
# "max-value-size-65536",
|
||||||
|
"max-value-size-1024",
|
||||||
|
# These must adhere to `FlashStorage`'s parameters.
|
||||||
|
"align-4",
|
||||||
|
"page-size-4096",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
xz2 = "0.1.7"
|
||||||
|
json = "0.12"
|
||||||
|
const-gen = "1.6"
|
||||||
|
embuild = "0.33"
|
||||||
|
cc = "1.2.9"
|
||||||
|
slint-build = { version = "1.14.1", git = "https://github.com/Limeth/slint", rev = "c2e5d05df2476557a299a78664e148d2fe62427d" }
|
||||||
|
gix = { version = "0.78", default-features = false, features = ["max-performance", "status"] }
|
||||||
|
indoc = "2.0.7"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "acid-firmware"
|
||||||
|
test = false
|
||||||
|
bench = false
|
||||||
|
|
@ -43,14 +43,25 @@ This replaces the debugging symbols with paths that will be available when debug
|
||||||
|
|
||||||
Then compile the firmware with:
|
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
|
||||||
|
|
||||||
245
firmware/acid-firmware/build.rs
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
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(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
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.display());
|
||||||
|
println!("cargo:rustc-link-lib=static={library}");
|
||||||
|
println!("cargo:rerun-if-changed={}", lib_library_path.display());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn link_static_lib_or_build(env_var: &str, library: &str, build: impl FnOnce()) {
|
||||||
|
if link_static_lib(env_var, library).is_err() {
|
||||||
|
(build)();
|
||||||
|
link_static_lib(env_var, library)
|
||||||
|
.unwrap_or_else(|err| panic!("Failed to link library after building it: {err:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
link_static_lib_or_build("XKBCOMMON_BUILD_DIR", "xkbcommon", || {
|
||||||
|
build_xkbcommon();
|
||||||
|
});
|
||||||
|
|
||||||
|
link_static_lib_or_build("SPECTRE_API_BUILD_DIR", "spectre", || {
|
||||||
|
build_sodium();
|
||||||
|
build_spectre();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Slint config and compilation
|
||||||
|
{
|
||||||
|
// Don't think this does anything:
|
||||||
|
// println!("cargo:rerun-if-env-changed=SLINT_FONT_SIZES");
|
||||||
|
|
||||||
|
let slint_config = CompilerConfiguration::new()
|
||||||
|
// .with_scale_factor(2.0)
|
||||||
|
.with_style("cosmic-dark".to_string())
|
||||||
|
.embed_resources(EmbedResourcesKind::EmbedForSoftwareRenderer);
|
||||||
|
slint_build::compile_with_config("ui/main.slint", slint_config)
|
||||||
|
.expect("Slint build failed");
|
||||||
|
slint_build::print_rustc_flags().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_vial_config() {
|
||||||
|
// Generated vial config file
|
||||||
|
let path = Path::new(&env::var_os("OUT_DIR").unwrap()).join("config_generated.rs");
|
||||||
|
let mut out_file = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.write(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(path)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let p = Path::new("vial.json");
|
||||||
|
let mut content = String::new();
|
||||||
|
match File::open(p) {
|
||||||
|
Ok(mut file) => {
|
||||||
|
file.read_to_string(&mut content)
|
||||||
|
.expect("Cannot read vial.json");
|
||||||
|
}
|
||||||
|
Err(e) => println!("Cannot find vial.json {p:?}: {e}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let vial_cfg = json::parse(&content).unwrap();
|
||||||
|
|
||||||
|
let keyboard_def_compressed: Vec<u8> = {
|
||||||
|
let vial_cfg_string = json::stringify(vial_cfg.clone());
|
||||||
|
let mut keyboard_def_compressed = Vec::new();
|
||||||
|
XzEncoder::new(vial_cfg_string.as_bytes(), 6)
|
||||||
|
.read_to_end(&mut keyboard_def_compressed)
|
||||||
|
.unwrap();
|
||||||
|
keyboard_def_compressed
|
||||||
|
};
|
||||||
|
// This is a firmware-unique randomly generated ID.
|
||||||
|
// If you fork this repo to make your own firmware, you should change this.
|
||||||
|
let keyboard_id: Vec<u8> = vec![0x9a, 0x8a, 0x08, 0xae, 0x87, 0xcd, 0xc7, 0xb9];
|
||||||
|
let keyboard_name: &str = {
|
||||||
|
let JsonValue::Object(vial_cfg) = &vial_cfg else {
|
||||||
|
panic!("The root element in `vial.json` is not an object.");
|
||||||
|
};
|
||||||
|
vial_cfg
|
||||||
|
.get("name")
|
||||||
|
.expect("No `name` in `vial.json`.")
|
||||||
|
.as_str()
|
||||||
|
.expect("`name` in `vial.json` is not a string.")
|
||||||
|
};
|
||||||
|
let vendor_id: u16 = {
|
||||||
|
let JsonValue::Object(vial_cfg) = &vial_cfg else {
|
||||||
|
panic!("The root element in `vial.json` is not an object.");
|
||||||
|
};
|
||||||
|
let vendor_id_string = vial_cfg
|
||||||
|
.get("vendorId")
|
||||||
|
.expect("No `vendorId` in `vial.json`.")
|
||||||
|
.as_str()
|
||||||
|
.expect("`vendorId` in `vial.json` is not a string.");
|
||||||
|
assert!(vendor_id_string.starts_with("0x"));
|
||||||
|
u16::from_str_radix(&vendor_id_string[2..], 16).expect("Invalid vendor ID.")
|
||||||
|
};
|
||||||
|
let product_id: u16 = {
|
||||||
|
let JsonValue::Object(vial_cfg) = &vial_cfg else {
|
||||||
|
panic!("The root element in `vial.json` is not an object.");
|
||||||
|
};
|
||||||
|
let product_id_string = vial_cfg
|
||||||
|
.get("productId")
|
||||||
|
.expect("No `productId` in `vial.json`.")
|
||||||
|
.as_str()
|
||||||
|
.expect("`productId` in `vial.json` is not a string.");
|
||||||
|
assert!(product_id_string.starts_with("0x"));
|
||||||
|
u16::from_str_radix(&product_id_string[2..], 16).expect("Invalid product ID.")
|
||||||
|
};
|
||||||
|
let const_declarations = [
|
||||||
|
const_declaration!(pub VIAL_KEYBOARD_DEF = keyboard_def_compressed),
|
||||||
|
const_declaration!(pub VIAL_KEYBOARD_ID = keyboard_id),
|
||||||
|
const_declaration!(pub VIAL_KEYBOARD_NAME = keyboard_name),
|
||||||
|
const_declaration!(pub VIAL_VENDOR_ID = vendor_id),
|
||||||
|
const_declaration!(pub VIAL_PRODUCT_ID = product_id),
|
||||||
|
]
|
||||||
|
.map(|s| "#[allow(clippy::redundant_static_lifetimes)]\n".to_owned() + s.as_str())
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
writeln!(out_file, "{}", const_declarations).unwrap();
|
||||||
|
|
||||||
|
writedoc!(
|
||||||
|
out_file,
|
||||||
|
"
|
||||||
|
#[allow(dead_code, non_camel_case_types, clippy::upper_case_acronyms)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum CustomKeycodes {{
|
||||||
|
"
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// const CUSTOM_KEYCODE_FIRST: u16 = 0x840;
|
||||||
|
|
||||||
|
#[allow(clippy::collapsible_if)]
|
||||||
|
if let JsonValue::Object(vial_cfg) = vial_cfg {
|
||||||
|
if let Some(JsonValue::Array(custom_keycodes)) = vial_cfg.get("customKeycodes") {
|
||||||
|
for (index, custom_keycode) in custom_keycodes.iter().enumerate() {
|
||||||
|
if let JsonValue::Object(custom_keycode) = custom_keycode {
|
||||||
|
let name = custom_keycode
|
||||||
|
.get("name")
|
||||||
|
.expect("A custom keycode in vial.json is missing a name.")
|
||||||
|
.as_str()
|
||||||
|
.expect("A custom keycode's name must be a string.");
|
||||||
|
writeln!(
|
||||||
|
out_file,
|
||||||
|
" {} = {},",
|
||||||
|
name,
|
||||||
|
// CUSTOM_KEYCODE_FIRST + index as u16
|
||||||
|
index as u8
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(out_file, "}}").unwrap();
|
||||||
|
}
|
||||||
1914
firmware/acid-firmware/keymaps/cz_coder.xkb
Normal file
6
firmware/acid-firmware/partition-table.csv
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
|
nvs, data, nvs, 0x9000, 0x6000,
|
||||||
|
phy_init, data, phy, 0xf000, 0x1000,
|
||||||
|
factory, app, factory, 0x10000, 4M,
|
||||||
|
rmk, data, undefined, , 64K,
|
||||||
|
acid, data, undefined, , 0x7E0000,
|
||||||
|
130
firmware/acid-firmware/src/config.rs
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
|
||||||
|
use rmk::config::{BehaviorConfig, ForksConfig, PositionalConfig};
|
||||||
|
use rmk::fork::{Fork, StateBits};
|
||||||
|
use rmk::types::action::{Action, KeyAction};
|
||||||
|
use rmk::types::modifier::ModifierCombination;
|
||||||
|
use rmk::{a, heapless, k, layer};
|
||||||
|
|
||||||
|
use crate::vial::CustomKeycodes;
|
||||||
|
|
||||||
|
pub const NUM_LAYER: usize = 8;
|
||||||
|
pub const MATRIX_ROWS: usize = 5;
|
||||||
|
pub const MATRIX_COLS: usize = 12;
|
||||||
|
pub const MATRIX_AREA: usize = MATRIX_ROWS * MATRIX_COLS;
|
||||||
|
|
||||||
|
const T: KeyAction = a!(Transparent);
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub const fn get_default_keymap() -> [[[KeyAction; MATRIX_COLS]; MATRIX_ROWS]; NUM_LAYER] {
|
||||||
|
[
|
||||||
|
layer!([
|
||||||
|
[k!(Escape), k!(Kc1), k!(Kc2), k!(Kc3), k!(Kc4), k!(Kc5), k!(Kc6), k!(Kc7), k!(Kc8), k!(Kc9), k!(Kc0), k!(Backspace)],
|
||||||
|
[k!(Tab), k!(Q), k!(W), k!(E), k!(R), k!(T), k!(Z), k!(U), k!(I), k!(O), k!(P), k!(Delete)],
|
||||||
|
[k!(LCtrl), k!(A), k!(S), k!(D), k!(F), k!(G), k!(H), k!(J), k!(K), k!(L), k!(Comma), k!(Enter)],
|
||||||
|
[k!(LShift), k!(Y), k!(X), k!(C), k!(V), k!(B), k!(N), k!(M), a!(No), a!(No), k!(Up), KeyAction::Single(Action::User(CustomKeycodes::FOCUS_LCD as u8))],
|
||||||
|
[a!(No), a!(No), k!(LGui), k!(LAlt), KeyAction::Single(Action::TriLayerLower), k!(Space), k!(Space), KeyAction::Single(Action::TriLayerLower), k!(RAlt), k!(Left), k!(Down), k!(Right)]
|
||||||
|
// [a!(No), a!(No), k!(LGui), k!(LAlt), k!(TriLayerLower), k!(Space), k!(Space), k!(TriLayerLower), k!(RAlt), k!(Left), k!(Down), k!(Right)]
|
||||||
|
]),
|
||||||
|
layer!([[T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T]]),
|
||||||
|
layer!([[T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T]]),
|
||||||
|
layer!([[T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T]]),
|
||||||
|
layer!([[T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T]]),
|
||||||
|
layer!([[T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T]]),
|
||||||
|
layer!([[T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T]]),
|
||||||
|
layer!([[T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T], [T, T, T, T, T, T, T, T, T, T, T, T]]),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fork_by_shift(
|
||||||
|
trigger: CustomKeycodes,
|
||||||
|
negative_output: KeyAction,
|
||||||
|
positive_output: KeyAction,
|
||||||
|
keep_shift: bool,
|
||||||
|
) -> Fork {
|
||||||
|
Fork::new(
|
||||||
|
KeyAction::Single(Action::User(trigger as u8)),
|
||||||
|
negative_output,
|
||||||
|
positive_output,
|
||||||
|
StateBits::new_from(
|
||||||
|
ModifierCombination::new()
|
||||||
|
.with_left_shift(true)
|
||||||
|
.with_right_shift(true),
|
||||||
|
Default::default(),
|
||||||
|
Default::default(),
|
||||||
|
),
|
||||||
|
StateBits::default(),
|
||||||
|
ModifierCombination::new()
|
||||||
|
.with_left_shift(keep_shift)
|
||||||
|
.with_right_shift(keep_shift),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_behavior_config() -> BehaviorConfig {
|
||||||
|
BehaviorConfig {
|
||||||
|
fork: ForksConfig {
|
||||||
|
forks: {
|
||||||
|
let mut forks = heapless::Vec::new();
|
||||||
|
forks
|
||||||
|
.push(fork_by_shift(
|
||||||
|
CustomKeycodes::FORK_ACUTE_ABOVERING_CS,
|
||||||
|
rmk::k!(Equal),
|
||||||
|
rmk::k!(Grave), // Shift is kept
|
||||||
|
true,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
forks
|
||||||
|
.push(fork_by_shift(
|
||||||
|
CustomKeycodes::FORK_CARON_DIAERESIS_CS,
|
||||||
|
rmk::shifted!(Equal),
|
||||||
|
rmk::wm!(Minus, ModifierCombination::new().with_right_alt(true)),
|
||||||
|
false,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
forks
|
||||||
|
.push(fork_by_shift(
|
||||||
|
CustomKeycodes::FORK_ACUTE_ABOVERING_CSP,
|
||||||
|
rmk::wm!(Equal, ModifierCombination::new().with_right_alt(true)),
|
||||||
|
rmk::wm!(Grave, ModifierCombination::new().with_right_alt(true)), // Shift is kept
|
||||||
|
true,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
forks
|
||||||
|
.push(fork_by_shift(
|
||||||
|
CustomKeycodes::FORK_CARON_DIAERESIS_CSP,
|
||||||
|
rmk::wm!(
|
||||||
|
Equal,
|
||||||
|
ModifierCombination::new()
|
||||||
|
.with_left_shift(true)
|
||||||
|
.with_right_alt(true)
|
||||||
|
),
|
||||||
|
rmk::wm!(Backslash, ModifierCombination::new().with_right_alt(true)),
|
||||||
|
false,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
forks
|
||||||
|
.push(fork_by_shift(
|
||||||
|
CustomKeycodes::FORK_9_FORWARDSLASH_EN_CSP,
|
||||||
|
rmk::k!(Kc9),
|
||||||
|
rmk::k!(Slash),
|
||||||
|
false,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
forks
|
||||||
|
.push(fork_by_shift(
|
||||||
|
CustomKeycodes::FORK_0_BACKWARDSLASH_EN_CSP,
|
||||||
|
rmk::k!(Kc0),
|
||||||
|
rmk::k!(Backslash),
|
||||||
|
false,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
forks
|
||||||
|
},
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_positional_config() -> PositionalConfig<MATRIX_ROWS, MATRIX_COLS> {
|
||||||
|
PositionalConfig::default()
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
|
|
||||||
use core::fmt::Write;
|
use 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1
firmware/acid-firmware/src/crypto.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
322
firmware/acid-firmware/src/db/mod.rs
Normal file
|
|
@ -0,0 +1,322 @@
|
||||||
|
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 embassy_sync_old::blocking_mutex::raw::CriticalSectionRawMutex as CriticalSectionRawMutexOld;
|
||||||
|
use embedded_storage_async::nor_flash::{NorFlash, ReadNorFlash};
|
||||||
|
use esp_hal::rng::Trng;
|
||||||
|
use esp_storage::FlashStorage;
|
||||||
|
use log::{debug, info};
|
||||||
|
|
||||||
|
use crate::ram::{PSRAM_ALLOCATOR, PsramAllocator};
|
||||||
|
|
||||||
|
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 }>, PsramAllocator>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> EkvFlash<T> {
|
||||||
|
fn new(flash: T) -> Self {
|
||||||
|
Self {
|
||||||
|
flash,
|
||||||
|
buffer: {
|
||||||
|
// Allocate the buffer directly on the heap.
|
||||||
|
let buffer = Box::new_zeroed_in(&PSRAM_ALLOCATOR);
|
||||||
|
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>, CriticalSectionRawMutexOld>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for AcidDatabase {
|
||||||
|
type Target = Database<EkvFlash<PartitionAcid>, CriticalSectionRawMutexOld>;
|
||||||
|
|
||||||
|
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::<_, CriticalSectionRawMutexOld>::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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
256
firmware/acid-firmware/src/dpi.rs
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
use alloc::{alloc::Global, boxed::Box, vec::Vec};
|
||||||
|
use embassy_time::Timer;
|
||||||
|
use esp_hal::{
|
||||||
|
Blocking,
|
||||||
|
dma::{BurstConfig, ExternalBurstConfig, InternalBurstConfig},
|
||||||
|
gpio::{Flex, Level, Output, OutputConfig},
|
||||||
|
lcd_cam::{LcdCam, lcd::dpi::Dpi},
|
||||||
|
ledc::{self, LSGlobalClkSource, Ledc, LowSpeed},
|
||||||
|
spi::master::AnySpi,
|
||||||
|
};
|
||||||
|
use esp_hal_bounce_buffers::{DmaBounce, Swapchain, SwapchainWriter, allocate_dma_buffer_in};
|
||||||
|
use i_slint_core::software_renderer::{Rgb565Pixel, TargetPixel};
|
||||||
|
use log::{error, info};
|
||||||
|
|
||||||
|
use crate::{DmaBounceController, PSRAM_ALLOCATOR, peripherals::st7701s::St7701s};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub struct DisplayPeripherals {
|
||||||
|
pub DMA_CH2: esp_hal::peripherals::DMA_CH2<'static>,
|
||||||
|
pub LCD_CAM: esp_hal::peripherals::LCD_CAM<'static>,
|
||||||
|
pub LEDC: esp_hal::peripherals::LEDC<'static>,
|
||||||
|
pub GPIO0: Output<'static>,
|
||||||
|
pub GPIO1: esp_hal::peripherals::GPIO1<'static>,
|
||||||
|
pub GPIO2: esp_hal::peripherals::GPIO2<'static>,
|
||||||
|
pub GPIO3: esp_hal::peripherals::GPIO3<'static>,
|
||||||
|
pub GPIO4: esp_hal::peripherals::GPIO4<'static>,
|
||||||
|
#[cfg(not(feature = "alt-log"))]
|
||||||
|
pub GPIO5: esp_hal::peripherals::GPIO5<'static>,
|
||||||
|
pub GPIO6: esp_hal::peripherals::GPIO6<'static>,
|
||||||
|
#[cfg(not(feature = "alt-log"))]
|
||||||
|
pub GPIO12: esp_hal::peripherals::GPIO12<'static>,
|
||||||
|
pub GPIO13: esp_hal::peripherals::GPIO13<'static>,
|
||||||
|
pub GPIO14: esp_hal::peripherals::GPIO14<'static>,
|
||||||
|
pub GPIO15: esp_hal::peripherals::GPIO15<'static>,
|
||||||
|
pub GPIO16: esp_hal::peripherals::GPIO16<'static>,
|
||||||
|
pub GPIO21: esp_hal::peripherals::GPIO21<'static>,
|
||||||
|
pub GPIO34: esp_hal::peripherals::GPIO34<'static>,
|
||||||
|
pub GPIO35: esp_hal::peripherals::GPIO35<'static>,
|
||||||
|
pub GPIO36: esp_hal::peripherals::GPIO36<'static>,
|
||||||
|
pub GPIO37: esp_hal::peripherals::GPIO37<'static>,
|
||||||
|
pub GPIO38: esp_hal::peripherals::GPIO38<'static>,
|
||||||
|
pub GPIO39: esp_hal::peripherals::GPIO39<'static>,
|
||||||
|
pub GPIO40: esp_hal::peripherals::GPIO40<'static>,
|
||||||
|
pub GPIO41: esp_hal::peripherals::GPIO41<'static>,
|
||||||
|
pub GPIO42: esp_hal::peripherals::GPIO42<'static>,
|
||||||
|
pub GPIO43: esp_hal::peripherals::GPIO43<'static>,
|
||||||
|
pub GPIO44: esp_hal::peripherals::GPIO44<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayPeripherals {
|
||||||
|
pub async fn into_display(self) -> St7701s<'static, Blocking> {
|
||||||
|
let mut ledc = Ledc::new(self.LEDC);
|
||||||
|
ledc.set_global_slow_clock(LSGlobalClkSource::APBClk);
|
||||||
|
let bl_timer = ledc.timer::<LowSpeed>(ledc::timer::Number::Timer0);
|
||||||
|
let bl_channel = ledc.channel::<LowSpeed>(ledc::channel::Number::Channel0, self.GPIO21);
|
||||||
|
|
||||||
|
let sck = Output::new(self.GPIO36, Level::High, OutputConfig::default());
|
||||||
|
let mosi = Flex::new(self.GPIO35);
|
||||||
|
let cs = Output::new(self.GPIO6, Level::High, OutputConfig::default());
|
||||||
|
|
||||||
|
let lcd = LcdCam::new(self.LCD_CAM).lcd;
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut unconfigured_dpi = Dpi::new(lcd, self.DMA_CH2, Default::default())
|
||||||
|
.unwrap()
|
||||||
|
.with_de(self.GPIO37)
|
||||||
|
.with_pclk(self.GPIO34)
|
||||||
|
.with_hsync(self.GPIO44)
|
||||||
|
.with_vsync(self.GPIO43)
|
||||||
|
// Blue
|
||||||
|
.with_data0(self.GPIO38)
|
||||||
|
.with_data1(self.GPIO39)
|
||||||
|
.with_data2(self.GPIO40)
|
||||||
|
.with_data3(self.GPIO41)
|
||||||
|
.with_data4(self.GPIO42)
|
||||||
|
// Green
|
||||||
|
.with_data7(self.GPIO13)
|
||||||
|
.with_data8(self.GPIO14)
|
||||||
|
.with_data9(self.GPIO15)
|
||||||
|
.with_data10(self.GPIO16)
|
||||||
|
// Red
|
||||||
|
.with_data11(self.GPIO0)
|
||||||
|
.with_data12(self.GPIO1)
|
||||||
|
.with_data13(self.GPIO2)
|
||||||
|
.with_data14(self.GPIO3)
|
||||||
|
.with_data15(self.GPIO4);
|
||||||
|
|
||||||
|
#[cfg(not(feature = "alt-log"))]
|
||||||
|
{
|
||||||
|
unconfigured_dpi = unconfigured_dpi
|
||||||
|
// Green
|
||||||
|
.with_data5(peripherals.GPIO5)
|
||||||
|
.with_data6(peripherals.GPIO12);
|
||||||
|
}
|
||||||
|
|
||||||
|
let st7701s = St7701s::new(sck, mosi, cs, unconfigured_dpi, bl_timer, bl_channel).await;
|
||||||
|
info!("ST7701S-based LCD display initialized!");
|
||||||
|
st7701s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Rename or get rid of.
|
||||||
|
pub struct Framebuffer {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub swapchain: Option<SwapchainWriter>,
|
||||||
|
pub bounce_buffers: Option<DmaBounce>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Framebuffer {
|
||||||
|
pub fn new(
|
||||||
|
channel: esp_hal::peripherals::DMA_CH0<'static>,
|
||||||
|
peripheral_src: AnySpi<'static>,
|
||||||
|
peripheral_dst: Dpi<'static, Blocking>,
|
||||||
|
burst_config: BurstConfig,
|
||||||
|
front_porch_pixels: u32,
|
||||||
|
width_pixels: u32,
|
||||||
|
height_pixels: u32,
|
||||||
|
rows_per_window: usize,
|
||||||
|
cyclic: bool,
|
||||||
|
) -> Self {
|
||||||
|
const BYTES_PER_PIXEL: usize = core::mem::size_of::<u16>();
|
||||||
|
let buffer_size = width_pixels as usize * height_pixels as usize * BYTES_PER_PIXEL;
|
||||||
|
let framebuffers = [
|
||||||
|
Box::leak(allocate_dma_buffer_in(
|
||||||
|
buffer_size,
|
||||||
|
burst_config,
|
||||||
|
&PSRAM_ALLOCATOR,
|
||||||
|
)),
|
||||||
|
Box::leak(allocate_dma_buffer_in(
|
||||||
|
buffer_size,
|
||||||
|
burst_config,
|
||||||
|
&PSRAM_ALLOCATOR,
|
||||||
|
)),
|
||||||
|
];
|
||||||
|
let (swapchain_reader, swapchain_writer) = Swapchain { framebuffers }.into_reader_writer();
|
||||||
|
let bounce_buffers = DmaBounce::new(
|
||||||
|
Global,
|
||||||
|
channel,
|
||||||
|
peripheral_src,
|
||||||
|
peripheral_dst,
|
||||||
|
swapchain_reader,
|
||||||
|
front_porch_pixels as usize * BYTES_PER_PIXEL,
|
||||||
|
width_pixels as usize * BYTES_PER_PIXEL,
|
||||||
|
rows_per_window,
|
||||||
|
burst_config,
|
||||||
|
cyclic,
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
width: width_pixels,
|
||||||
|
height: height_pixels,
|
||||||
|
swapchain: Some(swapchain_writer),
|
||||||
|
bounce_buffers: Some(bounce_buffers),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub async fn test_bounce_buffers(
|
||||||
|
channel: esp_hal::peripherals::DMA_CH0<'static>,
|
||||||
|
peripheral: esp_hal::peripherals::SPI2<'static>,
|
||||||
|
display_peripherals: DisplayPeripherals,
|
||||||
|
) {
|
||||||
|
error!("TEST BOUNCE BUFFERS SECTION ENTERED");
|
||||||
|
|
||||||
|
const BYTES_PER_PIXEL: usize = core::mem::size_of::<u16>();
|
||||||
|
// Assume highest burst config setting.
|
||||||
|
const EXTERNAL_BURST_CONFIG: ExternalBurstConfig = ExternalBurstConfig::Size32;
|
||||||
|
const ALIGNMENT_PIXELS: usize = EXTERNAL_BURST_CONFIG as usize / BYTES_PER_PIXEL;
|
||||||
|
// The total number of pixels demanded by the DPI, per row.
|
||||||
|
const WIDTH_TOTAL_PIXELS: usize = 368;
|
||||||
|
// The total number of rows demanded by the DPI, per frame.
|
||||||
|
const HEIGHT_PIXELS: usize = 960;
|
||||||
|
// The number of unused pixels at the start of the row.
|
||||||
|
const FRONT_PORCH_ACTUAL_PIXELS: usize = 120;
|
||||||
|
// The number of actually visible pixels, per row.
|
||||||
|
const WIDTH_VISIBLE_PIXELS: usize = 240;
|
||||||
|
// The number of pixels not stored in a bounce buffer, per row.
|
||||||
|
// This many arbitrary pixels are sent to the DPI.
|
||||||
|
const FRONT_PORCH_SKIPPED_PIXELS: usize =
|
||||||
|
(FRONT_PORCH_ACTUAL_PIXELS / ALIGNMENT_PIXELS) * ALIGNMENT_PIXELS;
|
||||||
|
const WIDTH_STORED_PIXELS: usize = WIDTH_TOTAL_PIXELS - FRONT_PORCH_SKIPPED_PIXELS;
|
||||||
|
const VISIBLE_OFFSET_IN_BUFFER_PIXELS: usize =
|
||||||
|
FRONT_PORCH_ACTUAL_PIXELS - FRONT_PORCH_SKIPPED_PIXELS;
|
||||||
|
const ROWS_PER_WINDOW: usize = 16;
|
||||||
|
let burst_config = BurstConfig {
|
||||||
|
internal_memory: InternalBurstConfig::Enabled,
|
||||||
|
external_memory: EXTERNAL_BURST_CONFIG,
|
||||||
|
};
|
||||||
|
let (swapchain_reader, mut swapchain_writer) = Swapchain {
|
||||||
|
framebuffers: [
|
||||||
|
Box::leak(allocate_dma_buffer_in(
|
||||||
|
HEIGHT_PIXELS * WIDTH_STORED_PIXELS * BYTES_PER_PIXEL,
|
||||||
|
burst_config,
|
||||||
|
&PSRAM_ALLOCATOR,
|
||||||
|
)),
|
||||||
|
Box::leak(allocate_dma_buffer_in(
|
||||||
|
HEIGHT_PIXELS * WIDTH_STORED_PIXELS * BYTES_PER_PIXEL,
|
||||||
|
burst_config,
|
||||||
|
&PSRAM_ALLOCATOR,
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
.into_reader_writer();
|
||||||
|
|
||||||
|
{
|
||||||
|
let write_guard = &mut swapchain_writer.write();
|
||||||
|
let buffer_src = bytemuck::cast_slice_mut::<u8, Rgb565Pixel>(write_guard);
|
||||||
|
let colors = (0..WIDTH_VISIBLE_PIXELS as u8 / 2)
|
||||||
|
.rev()
|
||||||
|
.map(|val| Rgb565Pixel::from_rgb(0xFF, val * 2, 0))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for (index, pixel) in buffer_src.iter_mut().enumerate() {
|
||||||
|
let mut x =
|
||||||
|
(index % WIDTH_STORED_PIXELS) as i16 - VISIBLE_OFFSET_IN_BUFFER_PIXELS as i16;
|
||||||
|
let mut y = (index / WIDTH_STORED_PIXELS) as i16;
|
||||||
|
|
||||||
|
if x < WIDTH_VISIBLE_PIXELS as i16 {
|
||||||
|
x = core::cmp::min(x, WIDTH_VISIBLE_PIXELS as i16 - 1 - x);
|
||||||
|
y = core::cmp::min(y, HEIGHT_PIXELS as i16 - 1 - y);
|
||||||
|
let min = core::cmp::min(x, y);
|
||||||
|
|
||||||
|
*pixel = colors[min as usize % colors.len()];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
*pixel = Rgb565Pixel::default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut st7701s = display_peripherals.into_display().await;
|
||||||
|
let mut dma_bounce = DmaBounce::new(
|
||||||
|
Global,
|
||||||
|
channel,
|
||||||
|
AnySpi::from(peripheral),
|
||||||
|
st7701s.dpi,
|
||||||
|
swapchain_reader,
|
||||||
|
FRONT_PORCH_SKIPPED_PIXELS * BYTES_PER_PIXEL,
|
||||||
|
WIDTH_STORED_PIXELS * BYTES_PER_PIXEL,
|
||||||
|
ROWS_PER_WINDOW,
|
||||||
|
burst_config,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
let mut bb_controller = DmaBounceController::new(dma_bounce);
|
||||||
|
|
||||||
|
error!("TEST BOUNCE BUFFERS TASK LAUNCHED");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
bb_controller.start().await.unwrap();
|
||||||
|
st7701s.controller.sleep_off().await;
|
||||||
|
Timer::after_secs(1).await;
|
||||||
|
st7701s.controller.sleep_on().await;
|
||||||
|
bb_controller.stop().await.unwrap();
|
||||||
|
Timer::after_secs(1).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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::ram::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
|
||||||
211
firmware/acid-firmware/src/ffi/crypto.rs
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
use core::{
|
||||||
|
cell::{Cell, RefCell},
|
||||||
|
ffi::{c_char, c_int, c_size_t, c_uchar, c_ulonglong},
|
||||||
|
};
|
||||||
|
|
||||||
|
use 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: blocking_mutex::Mutex<CriticalSectionRawMutex, Cell<Key>> =
|
||||||
|
blocking_mutex::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 = ACTIVE_ENCRYPTED_USER_KEY.lock(|user_key| user_key.get());
|
||||||
|
|
||||||
|
password_hash::decrypt_with(&mut user_key, &encryption_key);
|
||||||
|
output.copy_from_slice(&user_key);
|
||||||
|
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
unsafe extern "C" fn __spre_crypto_generichash_blake2b_salt_personal(
|
||||||
|
data_out: NonNullPtr<*mut c_uchar>,
|
||||||
|
data_out_len: c_size_t,
|
||||||
|
data_in: *const c_uchar,
|
||||||
|
data_in_len: c_ulonglong,
|
||||||
|
key: *const c_uchar,
|
||||||
|
keylen: c_size_t,
|
||||||
|
salt: *const c_uchar,
|
||||||
|
personal: *const c_uchar,
|
||||||
|
) -> c_int {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
#[repr(C)]
|
||||||
|
struct crypto_hash_sha256_state {
|
||||||
|
state: [u32; 8],
|
||||||
|
count: u64,
|
||||||
|
buf: [u8; 64],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types, unused)]
|
||||||
|
struct crypto_auth_hmacsha256_state {
|
||||||
|
ictx: crypto_hash_sha256_state,
|
||||||
|
octx: crypto_hash_sha256_state,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UnsafeSend<T>(pub T);
|
||||||
|
|
||||||
|
unsafe impl<T> Send for UnsafeSend<T> {}
|
||||||
|
|
||||||
|
// TODO: The software implementation is currently faster.
|
||||||
|
// My branch `sha-traits` of `esp-hal` implements the necessary traits
|
||||||
|
// to make `Sha256Context` work with `SimpleHmac`, but it would likely
|
||||||
|
// have to be optimized for `Hmac` instead to be faster.
|
||||||
|
// type HmacImpl = hmac::SimpleHmac<esp_hal::sha::Sha256Context>;
|
||||||
|
type HmacImpl = hmac::Hmac<sha2::Sha256>;
|
||||||
|
|
||||||
|
static HMAC: blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<Option<UnsafeSend<HmacImpl>>>> =
|
||||||
|
blocking_mutex::Mutex::new(RefCell::new(None));
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
unsafe extern "C" fn __spre_crypto_auth_hmacsha256_init(
|
||||||
|
state: NonNullPtr<*mut crypto_auth_hmacsha256_state>,
|
||||||
|
key: NonNullPtr<*const c_uchar>,
|
||||||
|
key_len: c_size_t,
|
||||||
|
) -> c_int {
|
||||||
|
let key: &[u8] = unsafe { core::slice::from_raw_parts(key, key_len) };
|
||||||
|
// TODO: Hardware-accelerated hashing via the SHA peripheral.
|
||||||
|
// SHA.lock(|sha| {
|
||||||
|
// let sha = sha
|
||||||
|
// .borrow_mut()
|
||||||
|
// .as_mut()
|
||||||
|
// .expect("HMAC peripheral not initialized.");
|
||||||
|
|
||||||
|
// sha.
|
||||||
|
// });
|
||||||
|
HMAC.lock(|hmac| {
|
||||||
|
let mut hmac = hmac.borrow_mut();
|
||||||
|
|
||||||
|
if hmac.is_some() {
|
||||||
|
panic!("HMAC already initialized. Cannot handle multiple HMAC's at once.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement a lower level `Sha256BlockContext` that would work with `Hmac`.
|
||||||
|
// This requires the implementation of a few `core_api` traits.
|
||||||
|
*hmac = Some(UnsafeSend(HmacImpl::new_from_slice(key).unwrap()));
|
||||||
|
});
|
||||||
|
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
unsafe extern "C" fn __spre_crypto_auth_hmacsha256_update(
|
||||||
|
state: NonNullPtr<*mut crypto_auth_hmacsha256_state>,
|
||||||
|
data_in: *const c_uchar,
|
||||||
|
data_in_len: c_ulonglong,
|
||||||
|
) -> c_int {
|
||||||
|
let data_in: &[u8] = unsafe { core::slice::from_raw_parts(data_in, data_in_len as usize) };
|
||||||
|
|
||||||
|
HMAC.lock(|hmac| {
|
||||||
|
let mut hmac = hmac.borrow_mut();
|
||||||
|
let UnsafeSend(hmac) = hmac
|
||||||
|
.as_mut()
|
||||||
|
.expect("HMAC must first be initialized before it is updated.");
|
||||||
|
|
||||||
|
hmac.update(data_in);
|
||||||
|
});
|
||||||
|
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
unsafe extern "C" fn __spre_crypto_auth_hmacsha256_final(
|
||||||
|
state: NonNullPtr<*mut crypto_auth_hmacsha256_state>,
|
||||||
|
out: NonNullPtr<*mut c_uchar>,
|
||||||
|
) -> c_int {
|
||||||
|
let out = unsafe { &mut *(out as *mut GenericArray<u8, U32>) };
|
||||||
|
|
||||||
|
HMAC.lock(|hmac| {
|
||||||
|
let mut hmac = hmac.borrow_mut();
|
||||||
|
let UnsafeSend(hmac) = hmac
|
||||||
|
.take()
|
||||||
|
.expect("HMAC must first be initialized before it is updated.");
|
||||||
|
|
||||||
|
hmac.finalize_into(out);
|
||||||
|
});
|
||||||
|
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
unsafe extern "C" fn __spre_sodium_bin2base64(
|
||||||
|
b64: NonNullPtr<*mut c_char>,
|
||||||
|
b64_maxlen: c_size_t,
|
||||||
|
bin: *const c_uchar,
|
||||||
|
bin_len: c_size_t,
|
||||||
|
variant: c_int,
|
||||||
|
) -> *mut c_char {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
unsafe extern "C" fn __spre_sodium_base642bin(
|
||||||
|
bin: NonNullPtr<*mut c_uchar>,
|
||||||
|
bin_maxlen: c_size_t,
|
||||||
|
b64: *const c_char,
|
||||||
|
b64_len: c_size_t,
|
||||||
|
ignore: *const c_char,
|
||||||
|
bin_len: *mut c_size_t,
|
||||||
|
b64_end: *mut *const c_char,
|
||||||
|
variant: c_int,
|
||||||
|
) -> c_int {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use core::ffi::{c_char, c_int};
|
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)]
|
||||||
|
|
@ -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!()
|
||||||
|
}
|
||||||
|
|
@ -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,13 @@ 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!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A pointer to an array of character attributes.
|
||||||
|
// This is used by `isdigit()`, `isalpha()`, `isspace()`, etc.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub static __spre__ctype_: [c_char; 0] = [];
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn __spre_abort() -> ! {
|
||||||
|
panic!("`abort()` called.")
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ pub unsafe extern "C" fn __xkbc_memset(
|
||||||
c: c_int,
|
c: c_int,
|
||||||
n: c_size_t,
|
n: c_size_t,
|
||||||
) -> *mut c_void {
|
) -> *mut c_void {
|
||||||
if dest_original == 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) }
|
||||||
|
}
|
||||||
21
firmware/acid-firmware/src/ffi/time.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
use embassy_time::Instant;
|
||||||
|
|
||||||
|
// TODO: Is this right?
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub type time_t = i64;
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn __spre_time(time: *mut time_t) -> time_t {
|
||||||
|
// TODO: This currently measures the time since boot,
|
||||||
|
// but it should actually measure the time since the unix epoch.
|
||||||
|
let duration = Instant::now().duration_since(Instant::MIN);
|
||||||
|
let seconds_since_epoch = duration.as_secs() as time_t;
|
||||||
|
|
||||||
|
if !time.is_null() {
|
||||||
|
unsafe {
|
||||||
|
*time = seconds_since_epoch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seconds_since_epoch
|
||||||
|
}
|
||||||
117
firmware/acid-firmware/src/flash.rs
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
use core::fmt::Write;
|
||||||
|
|
||||||
|
use alloc::{string::String, vec::Vec};
|
||||||
|
use embassy_embedded_hal::adapter::BlockingAsync;
|
||||||
|
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex};
|
||||||
|
use esp_bootloader_esp_idf::partitions::PartitionTable;
|
||||||
|
use esp_storage::FlashStorage;
|
||||||
|
use indoc::writedoc;
|
||||||
|
use log::info;
|
||||||
|
use rmk::storage::async_flash_wrapper;
|
||||||
|
use static_cell::StaticCell;
|
||||||
|
|
||||||
|
use crate::{PSRAM_ALLOCATOR, ram::PsramAllocator};
|
||||||
|
|
||||||
|
pub type Partition = embassy_embedded_hal::flash::partition::Partition<
|
||||||
|
'static,
|
||||||
|
CriticalSectionRawMutex,
|
||||||
|
BlockingAsync<FlashStorage<'static>>,
|
||||||
|
>;
|
||||||
|
|
||||||
|
pub struct Partitions {
|
||||||
|
pub rmk: Partition,
|
||||||
|
pub acid: Partition,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize the flash
|
||||||
|
pub fn initialize(flash_peripheral: esp_hal::peripherals::FLASH<'static>) -> Partitions {
|
||||||
|
static PARTITION_TABLE_BUFFER: StaticCell<Vec<u8, PsramAllocator>> = StaticCell::new();
|
||||||
|
let partition_table_buffer = PARTITION_TABLE_BUFFER.init_with(|| {
|
||||||
|
let mut buffer = Vec::<u8, _>::new_in(&PSRAM_ALLOCATOR);
|
||||||
|
buffer.resize(
|
||||||
|
esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN,
|
||||||
|
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(flash_peripheral)
|
||||||
|
// Flash memory may not be written to while another core is executing from it.
|
||||||
|
// By default, `FlashStorage` is configured to abort the operation and log an error message.
|
||||||
|
// However, it can also be configured to auto-park the other core, such that writing to
|
||||||
|
// flash succeeds.
|
||||||
|
// Alternatively, XiP from PSRAM could be used along with the `multicore_ignore` strategy,
|
||||||
|
// to avoid having to park the other core, which could result in better performance.
|
||||||
|
// Invalid configuration would then present itself as freezing/UB.
|
||||||
|
.multicore_auto_park();
|
||||||
|
let partition_table = {
|
||||||
|
esp_bootloader_esp_idf::partitions::read_partition_table(
|
||||||
|
&mut flash,
|
||||||
|
partition_table_buffer,
|
||||||
|
)
|
||||||
|
.expect("Failed to read the partition table.")
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
Mutex::<CriticalSectionRawMutex, _>::new(async_flash_wrapper(flash)),
|
||||||
|
partition_table,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut buffer = String::new();
|
||||||
|
|
||||||
|
writeln!(buffer, "Partition table:").unwrap();
|
||||||
|
|
||||||
|
for (index, partition) in partition_table.iter().enumerate() {
|
||||||
|
writedoc!(
|
||||||
|
buffer,
|
||||||
|
"
|
||||||
|
Partition #{index} {label:?}:
|
||||||
|
offset: 0x{offset:x}
|
||||||
|
length: 0x{len:x}
|
||||||
|
type: 0x{type:?}
|
||||||
|
read only: {read_only}
|
||||||
|
encrypted: {encrypted}
|
||||||
|
magic: {magic}
|
||||||
|
",
|
||||||
|
label = partition.label_as_str(),
|
||||||
|
offset = partition.offset(),
|
||||||
|
len = partition.len(),
|
||||||
|
type = partition.partition_type(),
|
||||||
|
read_only = partition.is_read_only(),
|
||||||
|
encrypted = partition.is_encrypted(),
|
||||||
|
magic = partition.magic(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("{}", buffer);
|
||||||
|
}
|
||||||
|
let flash_part_info_rmk = partition_table
|
||||||
|
.iter()
|
||||||
|
.find(|partition| partition.label_as_str() == "rmk")
|
||||||
|
.expect("No \"rmk\" partition found. Make sure to use the custom partition-table.csv when flashing.");
|
||||||
|
let flash_part_info_acid = partition_table
|
||||||
|
.iter()
|
||||||
|
.find(|partition| partition.label_as_str() == "acid")
|
||||||
|
.expect("No \"acid\" partition found. Make sure to use the custom partition-table.csv when flashing.");
|
||||||
|
|
||||||
|
Partitions {
|
||||||
|
rmk: Partition::new(
|
||||||
|
flash,
|
||||||
|
flash_part_info_rmk.offset(),
|
||||||
|
flash_part_info_rmk.len(),
|
||||||
|
),
|
||||||
|
acid: Partition::new(
|
||||||
|
flash,
|
||||||
|
flash_part_info_acid.offset(),
|
||||||
|
flash_part_info_acid.len(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
249
firmware/acid-firmware/src/logging.rs
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
use core::fmt::Arguments;
|
||||||
|
|
||||||
|
use log::LevelFilter;
|
||||||
|
|
||||||
|
// TODO: Replace with `log`'s `STATIC_MAX_LEVEL` set via crate features.
|
||||||
|
pub const LOG_LEVEL_FILTER: LevelFilter = {
|
||||||
|
if let Some(string) = option_env!("ESP_LOG") {
|
||||||
|
if string.eq_ignore_ascii_case("OFF") {
|
||||||
|
LevelFilter::Off
|
||||||
|
} else 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() {
|
||||||
|
esp_println::logger::init_logger(LOG_LEVEL_FILTER);
|
||||||
|
log::info!("Logger initialized!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Alternative logger via UART.
|
||||||
|
#[cfg(feature = "alt-log")]
|
||||||
|
#[macro_use]
|
||||||
|
pub mod uart {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use core::{cell::RefCell, fmt::Write};
|
||||||
|
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||||
|
use esp_hal::{Blocking, uart::UartTx};
|
||||||
|
use log::{Log, info};
|
||||||
|
|
||||||
|
#[cfg(feature = "racy-logging")]
|
||||||
|
static ALT_LOGGER_UART: embassy_sync::mutex::Mutex<
|
||||||
|
CriticalSectionRawMutex,
|
||||||
|
RefCell<Option<UartTx<'static, Blocking>>>,
|
||||||
|
> = embassy_sync::mutex::Mutex::new(RefCell::new(None));
|
||||||
|
|
||||||
|
#[cfg(not(feature = "racy-logging"))]
|
||||||
|
static ALT_LOGGER_UART: embassy_sync::blocking_mutex::Mutex<
|
||||||
|
CriticalSectionRawMutex,
|
||||||
|
RefCell<Option<UartTx<'static, Blocking>>>,
|
||||||
|
> = embassy_sync::blocking_mutex::Mutex::new(RefCell::new(None));
|
||||||
|
|
||||||
|
#[cfg(feature = "racy-logging")]
|
||||||
|
pub fn with_uart_tx<R>(f: impl FnOnce(&'_ mut UartTx<'static, Blocking>) -> R) -> R {
|
||||||
|
use crate::util::MutexExt;
|
||||||
|
|
||||||
|
// Safety:
|
||||||
|
// * The guard is not held across yield points.
|
||||||
|
// * **CARE MUST BE TAKEN NOT TO INVOKE THIS FUNCTION FROM AN INTERRUPT HANDLER.**
|
||||||
|
let uart = unsafe { ALT_LOGGER_UART.lock_blocking() };
|
||||||
|
let mut uart = uart.borrow_mut();
|
||||||
|
let uart = uart.as_mut().unwrap();
|
||||||
|
|
||||||
|
(f)(uart)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "racy-logging"))]
|
||||||
|
pub fn with_uart_tx<R>(f: impl FnOnce(&'_ mut UartTx<'static, Blocking>) -> R) -> R {
|
||||||
|
ALT_LOGGER_UART.lock(|uart| {
|
||||||
|
let mut uart = uart.borrow_mut();
|
||||||
|
let uart = uart.as_mut().unwrap();
|
||||||
|
|
||||||
|
(f)(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(|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_tx: UartTx<'static, Blocking>) {
|
||||||
|
{
|
||||||
|
#[cfg(feature = "racy-logging")]
|
||||||
|
{
|
||||||
|
use crate::util::MutexExt;
|
||||||
|
|
||||||
|
// Safety:
|
||||||
|
// * The guard is not held across yield points.
|
||||||
|
// * This function is not invoked from an interrupt handler.
|
||||||
|
let uart = unsafe { ALT_LOGGER_UART.lock_blocking() };
|
||||||
|
*uart.borrow_mut() = Some(uart_tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "racy-logging"))]
|
||||||
|
ALT_LOGGER_UART.lock(move |uart| {
|
||||||
|
*uart.borrow_mut() = Some(uart_tx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
log::set_logger_racy(&UartLogger).unwrap();
|
||||||
|
log::set_max_level_racy(LOG_LEVEL_FILTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Logger initialized!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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() {
|
||||||
|
rtt_target::rtt_init_log!(LOG_LEVEL_FILTER, ChannelMode::BlockIfFull);
|
||||||
|
log::info!("Logger initialized!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[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;
|
||||||
625
firmware/acid-firmware/src/main.rs
Normal file
|
|
@ -0,0 +1,625 @@
|
||||||
|
//! TODO:
|
||||||
|
//! * GUI event dispatch.
|
||||||
|
//! * Async interrupt handling of keyboard input. Reduces LCD glitching.
|
||||||
|
//! * Attempt to reduce the size of the framebuffer to 240x960 by changing the front/back porch of
|
||||||
|
//! the LCD driver. Reduces LCD glitching.
|
||||||
|
//! * Bounce buffers to get rid of glitching completely.
|
||||||
|
//! https://esp32.com/viewtopic.php?t=28230
|
||||||
|
//! https://esp32.com/viewtopic.php?f=12&t=26793&start=20#p95677
|
||||||
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
#![feature(allocator_api)]
|
||||||
|
#![feature(btreemap_alloc)]
|
||||||
|
#![feature(macro_metavar_expr)]
|
||||||
|
#![feature(c_variadic)]
|
||||||
|
#![feature(c_size_t)]
|
||||||
|
#![feature(debug_closure_helpers)]
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
use core::cell::RefCell;
|
||||||
|
use core::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
|
use alloc::boxed::Box;
|
||||||
|
use alloc::collections::vec_deque::VecDeque;
|
||||||
|
use alloc::format;
|
||||||
|
use alloc::sync::Arc;
|
||||||
|
use embassy_executor::Spawner;
|
||||||
|
use embassy_sync::blocking_mutex;
|
||||||
|
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||||
|
use embassy_sync::channel::Channel;
|
||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
use esp_alloc::MemoryCapability;
|
||||||
|
use esp_hal::clock::CpuClock;
|
||||||
|
use esp_hal::dma::{BurstConfig, ExternalBurstConfig, InternalBurstConfig};
|
||||||
|
use esp_hal::efuse::Efuse;
|
||||||
|
use esp_hal::gpio::{Input, InputConfig, Level, Output, OutputConfig, Pull};
|
||||||
|
use esp_hal::i2c::master::{I2c, I2cAddress};
|
||||||
|
use esp_hal::interrupt::software::{SoftwareInterrupt, SoftwareInterruptControl};
|
||||||
|
use esp_hal::psram::{FlashFreq, PsramConfig, PsramSize, SpiRamFreq, SpiTimingConfigCoreClock};
|
||||||
|
use esp_hal::rng::TrngSource;
|
||||||
|
use esp_hal::sha::ShaBackend;
|
||||||
|
use esp_hal::system::Stack;
|
||||||
|
use esp_hal::timer::timg::TimerGroup;
|
||||||
|
use esp_hal::uart::{Uart, UartRx};
|
||||||
|
use esp_hal::{Blocking, interrupt};
|
||||||
|
use esp_hal_bounce_buffers::{DmaBounce, RunningDmaBounceHandle};
|
||||||
|
use esp_rtos::embassy::{Executor, InterruptExecutor};
|
||||||
|
use esp_storage::FlashStorage;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use log::{info, warn};
|
||||||
|
use rmk::channel::{CONTROLLER_CHANNEL, ControllerSub};
|
||||||
|
use rmk::config::{DeviceConfig, RmkConfig, StorageConfig, VialConfig};
|
||||||
|
use rmk::controller::{Controller, EventController};
|
||||||
|
use rmk::debounce::default_debouncer::DefaultDebouncer;
|
||||||
|
use rmk::event::ControllerEvent;
|
||||||
|
use rmk::hid::Report;
|
||||||
|
use rmk::input_device::Runnable;
|
||||||
|
use rmk::join_all;
|
||||||
|
use rmk::keyboard::Keyboard;
|
||||||
|
use rmk::types::action::{Action, KeyAction};
|
||||||
|
use rmk::{initialize_keymap_and_storage, run_devices, run_rmk};
|
||||||
|
use static_cell::StaticCell;
|
||||||
|
use {esp_alloc as _, esp_backtrace as _};
|
||||||
|
|
||||||
|
use crate::dpi::{DisplayPeripherals, Framebuffer};
|
||||||
|
use crate::flash::Partition;
|
||||||
|
use crate::matrix::IoeMatrix;
|
||||||
|
use crate::peripherals::st7701s::St7701sController;
|
||||||
|
use crate::proxy::create_hid_report_interceptor;
|
||||||
|
use crate::ram::{PSRAM_ALLOCATOR, STACK_SIZE_CORE_APP};
|
||||||
|
use crate::ui::backend::SlintBackend;
|
||||||
|
use crate::vial::{
|
||||||
|
CustomKeycodes, VIAL_KEYBOARD_DEF, VIAL_KEYBOARD_ID, VIAL_KEYBOARD_NAME, VIAL_PRODUCT_ID,
|
||||||
|
VIAL_VENDOR_ID,
|
||||||
|
};
|
||||||
|
|
||||||
|
mutually_exclusive_features::none_or_one_of!["usb-log", "alt-log", "rtt-log"];
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod crypto;
|
||||||
|
mod db;
|
||||||
|
mod dpi;
|
||||||
|
mod ffi;
|
||||||
|
mod flash;
|
||||||
|
mod logging;
|
||||||
|
mod matrix;
|
||||||
|
mod peripherals;
|
||||||
|
mod proxy;
|
||||||
|
mod ram;
|
||||||
|
mod ui;
|
||||||
|
mod util;
|
||||||
|
mod vial;
|
||||||
|
|
||||||
|
#[cfg(feature = "alt-log")]
|
||||||
|
mod console;
|
||||||
|
|
||||||
|
// This creates a default app-descriptor required by the esp-idf bootloader.
|
||||||
|
// 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!();
|
||||||
|
|
||||||
|
// const FRAME_DURATION_MIN: Duration = Duration::from_millis(40); // 25 FPS
|
||||||
|
const FRAME_DURATION_MIN: Duration = Duration::from_millis(100); // 10 FPS
|
||||||
|
|
||||||
|
static KEYBOARD_REPORT_PROXY: Channel<CriticalSectionRawMutex, Report, 16> = Channel::new();
|
||||||
|
static LCD_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
#[esp_rtos::main]
|
||||||
|
async fn main(_spawner: Spawner) {
|
||||||
|
let config = esp_hal::Config::default()
|
||||||
|
.with_cpu_clock(CpuClock::max())
|
||||||
|
.with_psram(PsramConfig {
|
||||||
|
size: PsramSize::AutoDetect,
|
||||||
|
core_clock: Some(SpiTimingConfigCoreClock::SpiTimingConfigCoreClock80m),
|
||||||
|
flash_frequency: FlashFreq::FlashFreq120m,
|
||||||
|
ram_frequency: SpiRamFreq::Freq120m,
|
||||||
|
});
|
||||||
|
let peripherals: esp_hal::peripherals::Peripherals = esp_hal::init(config);
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
let (uart_rx, uart_tx) = {
|
||||||
|
#[cfg(feature = "alt-log")]
|
||||||
|
let (tx, rx) = (peripherals.GPIO12, peripherals.GPIO5);
|
||||||
|
#[cfg(not(feature = "alt-log"))]
|
||||||
|
let (tx, rx) = (esp_hal::gpio::NoPin, esp_hal::gpio::NoPin);
|
||||||
|
|
||||||
|
Uart::new(peripherals.UART2, Default::default())
|
||||||
|
.unwrap()
|
||||||
|
.with_tx(tx)
|
||||||
|
.with_rx(rx)
|
||||||
|
.split()
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "usb-log")]
|
||||||
|
logging::usb::setup_logging();
|
||||||
|
#[cfg(feature = "alt-log")]
|
||||||
|
logging::uart::setup_logging(uart_tx);
|
||||||
|
#[cfg(feature = "rtt-log")]
|
||||||
|
logging::rtt::setup_logging();
|
||||||
|
|
||||||
|
// Set up allocators.
|
||||||
|
ram::initialize(peripherals.PSRAM);
|
||||||
|
|
||||||
|
// let mut io = Io::new(peripherals.IO_MUX);
|
||||||
|
// io.set_interrupt_handler(interrupt_handler);
|
||||||
|
|
||||||
|
// info!("IO Mux initialized!");
|
||||||
|
|
||||||
|
// Enable pull-up on GPIO0 to prevent booting into download mode.
|
||||||
|
let gpio0 = Output::new(
|
||||||
|
peripherals.GPIO0,
|
||||||
|
Level::High,
|
||||||
|
OutputConfig::default().with_pull(Pull::Up),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Enable LDO2
|
||||||
|
let _ = Output::new(peripherals.GPIO17, Level::High, OutputConfig::default());
|
||||||
|
|
||||||
|
// Enable antenna
|
||||||
|
let _ = Output::new(peripherals.GPIO11, Level::Low, OutputConfig::default());
|
||||||
|
|
||||||
|
let mut sha_backend = ShaBackend::new(peripherals.SHA);
|
||||||
|
let _sha_driver_handle = sha_backend.start();
|
||||||
|
|
||||||
|
let timg0 = TimerGroup::new(peripherals.TIMG0);
|
||||||
|
let software_interrupt = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
|
||||||
|
esp_rtos::start(
|
||||||
|
timg0.timer0,
|
||||||
|
// software_interrupt.software_interrupt0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// A task executor that is able to handle interrupts, and then return back to executing tasks.
|
||||||
|
static EXECUTOR_CORE_0: StaticCell<InterruptExecutor<2>> = StaticCell::new();
|
||||||
|
let executor_core_0 = InterruptExecutor::new(software_interrupt.software_interrupt2);
|
||||||
|
let executor_core_0 = EXECUTOR_CORE_0.init(executor_core_0);
|
||||||
|
let interrupt_core_0_spawner = executor_core_0.start(interrupt::Priority::Priority1);
|
||||||
|
|
||||||
|
// static EXECUTOR_CORE_1: StaticCell<InterruptExecutor<3>> = StaticCell::new();
|
||||||
|
// let executor_core_1 = InterruptExecutor::new(software_interrupt.software_interrupt3);
|
||||||
|
// let executor_core_1 = EXECUTOR_CORE_1.init(executor_core_1);
|
||||||
|
// let interrupt_core_1_spawner = executor_core_1.start(interrupt::Priority::Priority2);
|
||||||
|
|
||||||
|
info!("ESP-RTOS started!");
|
||||||
|
|
||||||
|
let main_task_peripherals = MainPeripherals {
|
||||||
|
// high_priority_task_spawner: interrupt_core_1_spawner,
|
||||||
|
uart_rx,
|
||||||
|
software_interrupt1: software_interrupt.software_interrupt1,
|
||||||
|
software_interrupt0: software_interrupt.software_interrupt0,
|
||||||
|
RNG: peripherals.RNG,
|
||||||
|
ADC1: peripherals.ADC1,
|
||||||
|
USB0: peripherals.USB0,
|
||||||
|
FLASH: peripherals.FLASH,
|
||||||
|
DMA_CH0: peripherals.DMA_CH0,
|
||||||
|
SPI2: peripherals.SPI2,
|
||||||
|
CPU_CTRL: peripherals.CPU_CTRL,
|
||||||
|
GPIO19: peripherals.GPIO19,
|
||||||
|
GPIO20: peripherals.GPIO20,
|
||||||
|
display: DisplayPeripherals {
|
||||||
|
DMA_CH2: peripherals.DMA_CH2,
|
||||||
|
LCD_CAM: peripherals.LCD_CAM,
|
||||||
|
LEDC: peripherals.LEDC,
|
||||||
|
GPIO0: gpio0,
|
||||||
|
GPIO1: peripherals.GPIO1,
|
||||||
|
GPIO2: peripherals.GPIO2,
|
||||||
|
GPIO3: peripherals.GPIO3,
|
||||||
|
GPIO4: peripherals.GPIO4,
|
||||||
|
#[cfg(not(feature = "alt-log"))]
|
||||||
|
GPIO5: peripherals.GPIO5,
|
||||||
|
GPIO6: peripherals.GPIO6,
|
||||||
|
#[cfg(not(feature = "alt-log"))]
|
||||||
|
GPIO12: peripherals.GPIO12,
|
||||||
|
GPIO13: peripherals.GPIO13,
|
||||||
|
GPIO14: peripherals.GPIO14,
|
||||||
|
GPIO15: peripherals.GPIO15,
|
||||||
|
GPIO16: peripherals.GPIO16,
|
||||||
|
GPIO21: peripherals.GPIO21,
|
||||||
|
GPIO34: peripherals.GPIO34,
|
||||||
|
GPIO35: peripherals.GPIO35,
|
||||||
|
GPIO36: peripherals.GPIO36,
|
||||||
|
GPIO37: peripherals.GPIO37,
|
||||||
|
GPIO38: peripherals.GPIO38,
|
||||||
|
GPIO39: peripherals.GPIO39,
|
||||||
|
GPIO40: peripherals.GPIO40,
|
||||||
|
GPIO41: peripherals.GPIO41,
|
||||||
|
GPIO42: peripherals.GPIO42,
|
||||||
|
GPIO43: peripherals.GPIO43,
|
||||||
|
GPIO44: peripherals.GPIO44,
|
||||||
|
},
|
||||||
|
matrix: MatrixPeripherals {
|
||||||
|
I2C0: peripherals.I2C0,
|
||||||
|
GPIO7: peripherals.GPIO7,
|
||||||
|
GPIO8: peripherals.GPIO8,
|
||||||
|
GPIO9: peripherals.GPIO9,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
interrupt_core_0_spawner.must_spawn(main_task(main_task_peripherals));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Peripherals passed to the main task.
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct MainPeripherals {
|
||||||
|
uart_rx: UartRx<'static, Blocking>,
|
||||||
|
software_interrupt1: SoftwareInterrupt<'static, 1>,
|
||||||
|
software_interrupt0: SoftwareInterrupt<'static, 0>,
|
||||||
|
RNG: esp_hal::peripherals::RNG<'static>,
|
||||||
|
ADC1: esp_hal::peripherals::ADC1<'static>,
|
||||||
|
USB0: esp_hal::peripherals::USB0<'static>,
|
||||||
|
FLASH: esp_hal::peripherals::FLASH<'static>,
|
||||||
|
DMA_CH0: esp_hal::peripherals::DMA_CH0<'static>,
|
||||||
|
SPI2: esp_hal::peripherals::SPI2<'static>,
|
||||||
|
CPU_CTRL: esp_hal::peripherals::CPU_CTRL<'static>,
|
||||||
|
// GPIO10: esp_hal::peripherals::GPIO10<'static>,
|
||||||
|
// GPIO18: esp_hal::peripherals::GPIO18<'static>,
|
||||||
|
GPIO19: esp_hal::peripherals::GPIO19<'static>,
|
||||||
|
GPIO20: esp_hal::peripherals::GPIO20<'static>,
|
||||||
|
// GPIO33: esp_hal::peripherals::GPIO33<'static>,
|
||||||
|
// GPIO45: esp_hal::peripherals::GPIO45<'static>,
|
||||||
|
// GPIO46: esp_hal::peripherals::GPIO46<'static>,
|
||||||
|
// GPIO47: esp_hal::peripherals::GPIO47<'static>,
|
||||||
|
// GPIO48: esp_hal::peripherals::GPIO48<'static>,
|
||||||
|
display: DisplayPeripherals,
|
||||||
|
matrix: MatrixPeripherals,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct MatrixPeripherals {
|
||||||
|
I2C0: esp_hal::peripherals::I2C0<'static>,
|
||||||
|
GPIO7: esp_hal::peripherals::GPIO7<'static>,
|
||||||
|
GPIO8: esp_hal::peripherals::GPIO8<'static>,
|
||||||
|
GPIO9: esp_hal::peripherals::GPIO9<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn main_task(peripherals: MainPeripherals) {
|
||||||
|
// let _spawner = unsafe { Spawner::for_current_executor() }.await;
|
||||||
|
|
||||||
|
// Enable the TRNG source, so `Trng` can be constructed.
|
||||||
|
let _trng_source = TrngSource::new(peripherals.RNG, peripherals.ADC1);
|
||||||
|
|
||||||
|
#[cfg(feature = "ble")]
|
||||||
|
let mut host_resources = rmk::HostResources::new();
|
||||||
|
#[cfg(feature = "ble")]
|
||||||
|
let stack = {
|
||||||
|
use bt_hci::controller::ExternalController;
|
||||||
|
use esp_radio::{Controller as RadioController, ble::controller::BleConnector};
|
||||||
|
|
||||||
|
let mut rng = esp_hal::rng::Trng::try_new().unwrap();
|
||||||
|
static RADIO: StaticCell<RadioController<'static>> = StaticCell::new();
|
||||||
|
let radio = RADIO.init(esp_radio::init().unwrap());
|
||||||
|
let bluetooth = peripherals.BT;
|
||||||
|
let connector = BleConnector::new(radio, bluetooth, Default::default()).unwrap();
|
||||||
|
let controller: ExternalController<_, 20> = ExternalController::new(connector);
|
||||||
|
let central_addr = [0x18, 0xe2, 0x21, 0x80, 0xc0, 0xc7];
|
||||||
|
let ble_stack =
|
||||||
|
rmk::ble::build_ble_stack(controller, central_addr, &mut rng, &mut host_resources)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
info!("BLE stack for RMK built!");
|
||||||
|
|
||||||
|
ble_stack
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize USB
|
||||||
|
#[cfg(not(feature = "no-usb"))]
|
||||||
|
let usb_driver = {
|
||||||
|
use esp_hal::otg_fs::Usb;
|
||||||
|
use esp_hal::otg_fs::asynch::{Config, Driver};
|
||||||
|
|
||||||
|
static EP_MEMORY: StaticCell<[u8; 1024]> = StaticCell::new();
|
||||||
|
let ep_memory = EP_MEMORY.init_with(|| [0_u8; _]);
|
||||||
|
let usb = Usb::new(peripherals.USB0, peripherals.GPIO20, peripherals.GPIO19);
|
||||||
|
// Create the driver, from the HAL.
|
||||||
|
let config = Config::default();
|
||||||
|
let driver = Driver::new(usb, ep_memory, config);
|
||||||
|
|
||||||
|
info!("USB driver for RMK built!");
|
||||||
|
|
||||||
|
driver
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize the flash
|
||||||
|
let flash_partitions = flash::initialize(peripherals.FLASH);
|
||||||
|
|
||||||
|
info!("Flash memory configured!");
|
||||||
|
|
||||||
|
// Uncomment this to run bounce buffer test code instead.
|
||||||
|
// dpi::test_bounce_buffers(peripherals.DMA_CH0, peripherals.SPI2, peripherals.display).await;
|
||||||
|
// return;
|
||||||
|
|
||||||
|
// RMK config
|
||||||
|
let vial_config = VialConfig::new(VIAL_KEYBOARD_ID, VIAL_KEYBOARD_DEF, &[(0, 0), (1, 1)]);
|
||||||
|
let storage_config = StorageConfig {
|
||||||
|
start_addr: 0,
|
||||||
|
num_sectors: {
|
||||||
|
assert!(
|
||||||
|
flash_partitions.rmk.size() % FlashStorage::SECTOR_SIZE == 0,
|
||||||
|
"The size of the RMK partition must be a multiple of {} bytes. Current size: {}",
|
||||||
|
FlashStorage::SECTOR_SIZE,
|
||||||
|
flash_partitions.rmk.size()
|
||||||
|
);
|
||||||
|
(flash_partitions.rmk.size() / FlashStorage::SECTOR_SIZE) as u8
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
// Retrieve the hardware-unique MAC address.
|
||||||
|
let mac_address = Efuse::read_base_mac_address();
|
||||||
|
static SERIAL_NUMBER: StaticCell<Box<str>> = StaticCell::new();
|
||||||
|
let serial_number = SERIAL_NUMBER.init_with(|| {
|
||||||
|
/// A magic prefix string that is required for the device to be recognized by the Vial GUI.
|
||||||
|
const VIAL_SERIAL_PREFIX: &str = "vial:f64c2b3c";
|
||||||
|
format!(
|
||||||
|
"{VIAL_SERIAL_PREFIX}:acid:{:02x}",
|
||||||
|
mac_address.iter().format("")
|
||||||
|
)
|
||||||
|
.into_boxed_str()
|
||||||
|
});
|
||||||
|
let device_config = DeviceConfig {
|
||||||
|
vid: VIAL_VENDOR_ID,
|
||||||
|
pid: VIAL_PRODUCT_ID,
|
||||||
|
manufacturer: "",
|
||||||
|
product_name: VIAL_KEYBOARD_NAME,
|
||||||
|
serial_number,
|
||||||
|
};
|
||||||
|
info!("RMK Device Config: {device_config:#04x?}");
|
||||||
|
let rmk_config = RmkConfig {
|
||||||
|
device_config,
|
||||||
|
vial_config,
|
||||||
|
storage_config,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialze keyboard stuffs
|
||||||
|
// Initialize the storage and keymap
|
||||||
|
let mut default_keymap = config::get_default_keymap();
|
||||||
|
let mut behavior_config = config::get_behavior_config();
|
||||||
|
let mut positional_config = config::get_positional_config();
|
||||||
|
let (keymap, mut storage) = initialize_keymap_and_storage(
|
||||||
|
&mut default_keymap,
|
||||||
|
flash_partitions.rmk,
|
||||||
|
&storage_config,
|
||||||
|
&mut behavior_config,
|
||||||
|
&mut positional_config,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
info!("Initialized keymap and storage for RMK!");
|
||||||
|
|
||||||
|
let mut keyboard = Keyboard::new(&keymap); // Initialize the light controller
|
||||||
|
|
||||||
|
info!("Keyboard initialized!");
|
||||||
|
info!("Awaiting on all tasks...");
|
||||||
|
|
||||||
|
// TODO: Probably want to select! instead and re-try.
|
||||||
|
join_all![
|
||||||
|
ram::run_alloc_stats_reporter(),
|
||||||
|
initialize_and_run_rmk_devices(peripherals.matrix),
|
||||||
|
keyboard.run(), // Keyboard is special
|
||||||
|
run_rmk(
|
||||||
|
&keymap,
|
||||||
|
#[cfg(not(feature = "no-usb"))]
|
||||||
|
usb_driver,
|
||||||
|
#[cfg(feature = "ble")]
|
||||||
|
&stack,
|
||||||
|
&mut storage,
|
||||||
|
rmk_config,
|
||||||
|
),
|
||||||
|
create_hid_report_interceptor(),
|
||||||
|
initialize_display_and_renderer(
|
||||||
|
peripherals.display,
|
||||||
|
peripherals.DMA_CH0,
|
||||||
|
peripherals.SPI2,
|
||||||
|
peripherals.CPU_CTRL,
|
||||||
|
peripherals.software_interrupt0,
|
||||||
|
peripherals.software_interrupt1,
|
||||||
|
flash_partitions.acid
|
||||||
|
),
|
||||||
|
console::run_console(peripherals.uart_rx.into_async())
|
||||||
|
]
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn initialize_and_run_rmk_devices(matrix_peripherals: MatrixPeripherals) {
|
||||||
|
// Initialize the matrix and keyboard
|
||||||
|
const I2C_ADDR_MATRIX_LEFT: I2cAddress = I2cAddress::SevenBit(0b0100000);
|
||||||
|
const I2C_ADDR_MATRIX_RIGHT: I2cAddress = I2cAddress::SevenBit(0b0100001);
|
||||||
|
|
||||||
|
let i2c = I2c::new(matrix_peripherals.I2C0, Default::default())
|
||||||
|
.unwrap()
|
||||||
|
.with_sda(matrix_peripherals.GPIO8)
|
||||||
|
.with_scl(matrix_peripherals.GPIO9);
|
||||||
|
let matrix_interrupt_low = Input::new(matrix_peripherals.GPIO7, InputConfig::default());
|
||||||
|
let mut matrix = IoeMatrix::new(
|
||||||
|
matrix_interrupt_low,
|
||||||
|
i2c.into_async(),
|
||||||
|
DefaultDebouncer::new(),
|
||||||
|
[I2C_ADDR_MATRIX_LEFT, I2C_ADDR_MATRIX_RIGHT],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
run_devices! (
|
||||||
|
(matrix) => rmk::channel::EVENT_CHANNEL,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn initialize_display_and_renderer(
|
||||||
|
display_peripherals: DisplayPeripherals,
|
||||||
|
dma_ch0: esp_hal::peripherals::DMA_CH0<'static>,
|
||||||
|
spi2: esp_hal::peripherals::SPI2<'static>,
|
||||||
|
cpu_ctrl: esp_hal::peripherals::CPU_CTRL<'static>,
|
||||||
|
software_interrupt0: SoftwareInterrupt<'static, 0>,
|
||||||
|
software_interrupt1: SoftwareInterrupt<'static, 1>,
|
||||||
|
partition_acid: Partition,
|
||||||
|
) {
|
||||||
|
let st7701s = display_peripherals.into_display().await;
|
||||||
|
|
||||||
|
static FRAMEBUFFER: StaticCell<Framebuffer> = StaticCell::new();
|
||||||
|
let framebuffer = FRAMEBUFFER.init_with(move || {
|
||||||
|
Framebuffer::new(
|
||||||
|
dma_ch0,
|
||||||
|
spi2.into(),
|
||||||
|
st7701s.dpi,
|
||||||
|
BurstConfig {
|
||||||
|
internal_memory: InternalBurstConfig::Enabled,
|
||||||
|
external_memory: ExternalBurstConfig::Size32,
|
||||||
|
},
|
||||||
|
// The burst config (16/32/64) doesn't seem to affect the alignment of the row size.
|
||||||
|
//
|
||||||
|
// | | ( displayed range ) |
|
||||||
|
// | [ pad ] [ pad ]
|
||||||
|
// | [ DMA-transmissible range ]
|
||||||
|
// [ DMA-t. left overscan ] | | |
|
||||||
|
// 0 112 120 360 368 (index of u16 pixel)
|
||||||
|
// ^ aligned ^ aligned ^ aligned
|
||||||
|
//
|
||||||
|
// TODO: Compute the appropriate ranges to pass to the renderer and DPI peripheral.
|
||||||
|
// The renderer should pass the size of the `pad`ding to the GUI is parameters,
|
||||||
|
// to align the content to the displayed range.
|
||||||
|
112,
|
||||||
|
368 - 112,
|
||||||
|
960,
|
||||||
|
16,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
info!("Framebuffer created!");
|
||||||
|
|
||||||
|
let window_size = [framebuffer.height, framebuffer.width];
|
||||||
|
let swapchain_writer = framebuffer.swapchain.take().unwrap();
|
||||||
|
|
||||||
|
static SECOND_CORE_STACK: StaticCell<Stack<{ STACK_SIZE_CORE_APP }>> = StaticCell::new();
|
||||||
|
let second_core_stack = SECOND_CORE_STACK.init(Stack::new());
|
||||||
|
esp_rtos::start_second_core(
|
||||||
|
cpu_ctrl,
|
||||||
|
software_interrupt0,
|
||||||
|
software_interrupt1,
|
||||||
|
second_core_stack,
|
||||||
|
move || {
|
||||||
|
// static EXECUTOR: StaticCell<InterruptExecutor<2>> = StaticCell::new();
|
||||||
|
// let exec = EXECUTOR.init(InterruptExecutor::new(
|
||||||
|
// peripherals.software_interrupt2,
|
||||||
|
// ));
|
||||||
|
// let spawner = exec.start(Priority::Priority3);
|
||||||
|
// spawner.must_spawn(run_renderer_task());
|
||||||
|
static EXECUTOR: StaticCell<Executor> = StaticCell::new();
|
||||||
|
let executor: &mut Executor = EXECUTOR.init(Executor::new());
|
||||||
|
executor.run(|spawner| {
|
||||||
|
let slint_backend = SlintBackend {
|
||||||
|
// peripherals: RefCell::new(Some(peripherals)),
|
||||||
|
window_size,
|
||||||
|
window: RefCell::new(None),
|
||||||
|
swapchain: RefCell::new(swapchain_writer),
|
||||||
|
quit_event_loop: Default::default(),
|
||||||
|
events: Arc::new(blocking_mutex::Mutex::new(RefCell::new(VecDeque::new()))),
|
||||||
|
};
|
||||||
|
spawner.must_spawn(ui::run_renderer_task(slint_backend, partition_acid));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
info!("Second core started!");
|
||||||
|
|
||||||
|
let bb_controller = DmaBounceController::new(framebuffer.bounce_buffers.take().unwrap());
|
||||||
|
let mut user_controller = UserController::new(st7701s.controller, bb_controller);
|
||||||
|
|
||||||
|
user_controller.event_loop().await
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DmaBounceControllerInner {
|
||||||
|
Idle(DmaBounce),
|
||||||
|
Transmitting(RunningDmaBounceHandle),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DmaBounceController {
|
||||||
|
inner: Option<DmaBounceControllerInner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DmaBounceController {
|
||||||
|
pub fn new(dma_bounce: DmaBounce) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Some(DmaBounceControllerInner::Idle(dma_bounce)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start(&mut self) -> Result<(), ()> {
|
||||||
|
let DmaBounceControllerInner::Idle(dma_bounce) = self.inner.take().unwrap() else {
|
||||||
|
return Err(());
|
||||||
|
};
|
||||||
|
self.inner = Some(DmaBounceControllerInner::Transmitting(
|
||||||
|
dma_bounce.launch_interrupt_driven_task().await,
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn stop(&mut self) -> Result<(), ()> {
|
||||||
|
let DmaBounceControllerInner::Transmitting(running_dma_bounce) = self.inner.take().unwrap()
|
||||||
|
else {
|
||||||
|
return Err(());
|
||||||
|
};
|
||||||
|
self.inner = Some(DmaBounceControllerInner::Idle(
|
||||||
|
running_dma_bounce.stop().await,
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UserController<'a> {
|
||||||
|
sub: ControllerSub,
|
||||||
|
lcd_controller: St7701sController<'a>,
|
||||||
|
bb_controller: DmaBounceController,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> UserController<'a> {
|
||||||
|
fn new(lcd_controller: St7701sController<'a>, bb_controller: DmaBounceController) -> Self {
|
||||||
|
Self {
|
||||||
|
sub: CONTROLLER_CHANNEL.subscriber().unwrap(),
|
||||||
|
lcd_controller,
|
||||||
|
bb_controller,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Controller for UserController<'a> {
|
||||||
|
type Event = ControllerEvent;
|
||||||
|
|
||||||
|
async fn process_event(&mut self, event: Self::Event) {
|
||||||
|
if let ControllerEvent::Key(keyboard_event, KeyAction::Single(Action::User(user_key_index))) =
|
||||||
|
event
|
||||||
|
&& user_key_index == CustomKeycodes::FOCUS_LCD as u8
|
||||||
|
&& keyboard_event.pressed
|
||||||
|
{
|
||||||
|
let enabled = !LCD_ENABLED.fetch_xor(true, Ordering::SeqCst);
|
||||||
|
|
||||||
|
match enabled {
|
||||||
|
false => {
|
||||||
|
info!("Disabling LCD.");
|
||||||
|
*rmk::channel::KEYBOARD_REPORT_SENDER.write().await =
|
||||||
|
&rmk::channel::KEYBOARD_REPORT_RECEIVER;
|
||||||
|
self.lcd_controller.sleep_on().await;
|
||||||
|
self.bb_controller.stop().await.unwrap();
|
||||||
|
}
|
||||||
|
true => {
|
||||||
|
info!("Enabling LCD.");
|
||||||
|
*rmk::channel::KEYBOARD_REPORT_SENDER.write().await = &KEYBOARD_REPORT_PROXY;
|
||||||
|
self.bb_controller.start().await.unwrap();
|
||||||
|
self.lcd_controller.sleep_off().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn next_message(&mut self) -> Self::Event {
|
||||||
|
self.sub.next_message_pure().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// // TODO: Not needed currently. If it is ever enabled, don't forget to register it in Io.
|
||||||
|
// #[handler]
|
||||||
|
// #[ram] // Improves performance.
|
||||||
|
// fn interrupt_handler() {
|
||||||
|
// // esp_println::println!(
|
||||||
|
// // "GPIO Interrupt with priority {}",
|
||||||
|
// // esp_hal::xtensa_lx::interrupt::get_level()
|
||||||
|
// // );
|
||||||
|
// }
|
||||||
|
|
@ -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
|
||||||
|
|
@ -1,17 +1,41 @@
|
||||||
use bitflags::bitflags;
|
|
||||||
use esp_hal::gpio::OutputPin;
|
|
||||||
use paste::paste;
|
use paste::paste;
|
||||||
|
use tinyvec::ArrayVec;
|
||||||
|
|
||||||
pub trait Command {
|
pub trait Command {
|
||||||
fn address(&self) -> u8;
|
fn address(&self) -> u8;
|
||||||
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> 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],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CustomCommand {
|
||||||
|
pub const fn address_const(&self) -> u8 {
|
||||||
|
self.address
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn args_slice(&self) -> &[u8] {
|
||||||
|
self.args
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Command for CustomCommand {
|
impl Command for CustomCommand {
|
||||||
fn address(&self) -> u8 {
|
fn address(&self) -> u8 {
|
||||||
self.address
|
self.address
|
||||||
|
|
@ -135,6 +159,21 @@ macro_rules! define_commands {
|
||||||
$(#[$docs])*
|
$(#[$docs])*
|
||||||
$visibility struct [< Cmd $command >]($(pub [< Cmd $command Arg ${index()} >], $(${ignore($body)})*)*);
|
$visibility struct [< Cmd $command >]($(pub [< Cmd $command Arg ${index()} >], $(${ignore($body)})*)*);
|
||||||
|
|
||||||
|
impl [< Cmd $command >] {
|
||||||
|
pub const fn address_const(&self) -> u8 {
|
||||||
|
$address
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn args_slice(&self) -> &[u8] {
|
||||||
|
unsafe {
|
||||||
|
core::slice::from_raw_parts(
|
||||||
|
self as *const [< Cmd $command >] as *const u8,
|
||||||
|
0 $(+ 1 $(${ignore($body)})*)*,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Command for [< Cmd $command >] {
|
impl Command for [< Cmd $command >] {
|
||||||
fn address(&self) -> u8 {
|
fn address(&self) -> u8 {
|
||||||
$address
|
$address
|
||||||
|
|
@ -150,12 +189,7 @@ macro_rules! define_commands {
|
||||||
type IntoIter = core::slice::Iter<'a, u8>;
|
type IntoIter = core::slice::Iter<'a, u8>;
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
unsafe {
|
self.args_slice().iter()
|
||||||
core::slice::from_raw_parts(
|
|
||||||
self as *const [< Cmd $command >] as *const u8,
|
|
||||||
0 $(+ 1 $(${ignore($body)})*)*,
|
|
||||||
)
|
|
||||||
}.iter()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)*
|
)*
|
||||||
|
|
@ -1195,18 +1229,3 @@ define_commands! {
|
||||||
arg { const INIT = 0b1010_1010; }
|
arg { const INIT = 0b1010_1010; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct St7701s {}
|
|
||||||
|
|
||||||
impl St7701s {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let _ = CmdNop::default();
|
|
||||||
let x = CmdGsl(
|
|
||||||
CmdGslArg0::new().with_tesl_msb(1),
|
|
||||||
CmdGslArg1::new().with_tesl_lsb(0),
|
|
||||||
);
|
|
||||||
x.address();
|
|
||||||
x.args_iter();
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
405
firmware/acid-firmware/src/peripherals/st7701s/init_sequence.rs
Normal file
|
|
@ -0,0 +1,405 @@
|
||||||
|
use tinyvec::ArrayVec;
|
||||||
|
|
||||||
|
use crate::peripherals::st7701s::commands::*;
|
||||||
|
|
||||||
|
pub enum InitSequenceAction {
|
||||||
|
// TODO: Each command takes up 17 bytes. Consider storing compressed.
|
||||||
|
Command(ArrayCommand<16>),
|
||||||
|
SleepMs(u64),
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! make_init_action {
|
||||||
|
(command: $command:expr$(,)?) => {
|
||||||
|
InitSequenceAction::Command({
|
||||||
|
let command = $command;
|
||||||
|
let args_slice = command.args_slice();
|
||||||
|
let mut args_array = [0; _];
|
||||||
|
let mut args_len = 0;
|
||||||
|
while args_len < args_slice.len() {
|
||||||
|
args_array[args_len] = args_slice[args_len];
|
||||||
|
args_len += 1;
|
||||||
|
}
|
||||||
|
ArrayCommand {
|
||||||
|
address: command.address_const(),
|
||||||
|
args: match ArrayVec::try_from_array_len(args_array, args_len) {
|
||||||
|
Ok(args) => args,
|
||||||
|
Err(_) => panic!("too many command args"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
(sleep_ms: $expr:expr) => {
|
||||||
|
InitSequenceAction::SleepMs($expr)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! make_init_sequence {
|
||||||
|
($({ $($tt:tt)* }),*$(,)?) => {
|
||||||
|
[
|
||||||
|
$( make_init_action!($($tt)*) ),*
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const INIT_SEQUENCE_COMMANDS: [InitSequenceAction; 46] = make_init_sequence! [
|
||||||
|
{ command: CmdCn2bkxsel(
|
||||||
|
CmdCn2bkxselArg0::new(),
|
||||||
|
CmdCn2bkxselArg1::new(),
|
||||||
|
CmdCn2bkxselArg2::new(),
|
||||||
|
CmdCn2bkxselArg3::new(),
|
||||||
|
CmdCn2bkxselArg4::new().with_bksel(0x3).with_cn2(true),
|
||||||
|
) },
|
||||||
|
{ command: CustomCommand {
|
||||||
|
address: 0xEF,
|
||||||
|
args: &[0x08],
|
||||||
|
} },
|
||||||
|
{ command: CmdCn2bkxsel(
|
||||||
|
CmdCn2bkxselArg0::new(),
|
||||||
|
CmdCn2bkxselArg1::new(),
|
||||||
|
CmdCn2bkxselArg2::new(),
|
||||||
|
CmdCn2bkxselArg3::new(),
|
||||||
|
CmdCn2bkxselArg4::new().with_bksel(0x0).with_cn2(true),
|
||||||
|
) },
|
||||||
|
{ command: CmdLneset(
|
||||||
|
CmdLnesetArg0::new().with_bar(119).with_lde_en(false),
|
||||||
|
CmdLnesetArg1::new().with_line_delta(0),
|
||||||
|
) },
|
||||||
|
{ command: CmdPorctrl(
|
||||||
|
CmdPorctrlArg0::new().with_vbp(17),
|
||||||
|
CmdPorctrlArg1::new().with_vfp(12),
|
||||||
|
) },
|
||||||
|
{ command: CmdInvsel(
|
||||||
|
CmdInvselArg0::new().with_nlinv(0b111),
|
||||||
|
CmdInvselArg1::new().with_rtni(2),
|
||||||
|
) },
|
||||||
|
{ command: CustomCommand {
|
||||||
|
address: 0xCC,
|
||||||
|
args: &[0x30],
|
||||||
|
} },
|
||||||
|
{ command: CmdPvgamctrl(
|
||||||
|
CmdPvgamctrlArg0::new().with_vc0p(6).with_aj0p(0),
|
||||||
|
CmdPvgamctrlArg1::new().with_vc4p(15).with_aj1p(3),
|
||||||
|
CmdPvgamctrlArg2::new().with_vc8p(14).with_aj2p(0),
|
||||||
|
CmdPvgamctrlArg3::new().with_vc16p(12),
|
||||||
|
CmdPvgamctrlArg4::new().with_vc24p(15).with_aj3p(0),
|
||||||
|
CmdPvgamctrlArg5::new().with_vc52p(3),
|
||||||
|
CmdPvgamctrlArg6::new().with_vc80p(0),
|
||||||
|
CmdPvgamctrlArg7::new().with_vc108p(10),
|
||||||
|
CmdPvgamctrlArg8::new().with_vc147p(7),
|
||||||
|
CmdPvgamctrlArg9::new().with_vc175p(27),
|
||||||
|
CmdPvgamctrlArg10::new().with_vc203p(3),
|
||||||
|
CmdPvgamctrlArg11::new().with_vc231p(18).with_aj4p(0),
|
||||||
|
CmdPvgamctrlArg12::new().with_vc239p(16),
|
||||||
|
CmdPvgamctrlArg13::new().with_vc247p(37).with_aj5p(0),
|
||||||
|
CmdPvgamctrlArg14::new().with_vc251p(54).with_aj6p(0),
|
||||||
|
CmdPvgamctrlArg15::new().with_vc255p(30).with_aj7p(0),
|
||||||
|
) },
|
||||||
|
{ command: CmdNvgamctrl(
|
||||||
|
CmdNvgamctrlArg0::new().with_vc0n(12).with_aj0n(0),
|
||||||
|
CmdNvgamctrlArg1::new().with_vc4n(14).with_aj1n(3),
|
||||||
|
CmdNvgamctrlArg2::new().with_vc8n(18).with_aj2n(0),
|
||||||
|
CmdNvgamctrlArg3::new().with_vc16n(12),
|
||||||
|
CmdNvgamctrlArg4::new().with_vc24n(14).with_aj3n(0),
|
||||||
|
CmdNvgamctrlArg5::new().with_vc52n(6),
|
||||||
|
CmdNvgamctrlArg6::new().with_vc80n(3),
|
||||||
|
CmdNvgamctrlArg7::new().with_vc108n(6),
|
||||||
|
CmdNvgamctrlArg8::new().with_vc147n(8),
|
||||||
|
CmdNvgamctrlArg9::new().with_vc175n(35),
|
||||||
|
CmdNvgamctrlArg10::new().with_vc203n(6),
|
||||||
|
CmdNvgamctrlArg11::new().with_vc231n(18).with_aj4n(0),
|
||||||
|
CmdNvgamctrlArg12::new().with_vc239n(16),
|
||||||
|
CmdNvgamctrlArg13::new().with_vc247n(48).with_aj5n(0),
|
||||||
|
CmdNvgamctrlArg14::new().with_vc251n(47).with_aj6n(0),
|
||||||
|
CmdNvgamctrlArg15::new().with_vc255n(31).with_aj7n(0),
|
||||||
|
) },
|
||||||
|
{ command: CmdCn2bkxsel(
|
||||||
|
CmdCn2bkxselArg0::new(),
|
||||||
|
CmdCn2bkxselArg1::new(),
|
||||||
|
CmdCn2bkxselArg2::new(),
|
||||||
|
CmdCn2bkxselArg3::new(),
|
||||||
|
CmdCn2bkxselArg4::new().with_bksel(0x1).with_cn2(true),
|
||||||
|
) },
|
||||||
|
{ command: CmdVrhs(
|
||||||
|
CmdVrhsArg0::new().with_vrha(115),
|
||||||
|
) },
|
||||||
|
{ command: CmdVcoms(
|
||||||
|
CmdVcomsArg0::new().with_vcom(124),
|
||||||
|
) },
|
||||||
|
{ command: CmdVghss(
|
||||||
|
// The first bit is set to 1 in the original init code, but not here.
|
||||||
|
CmdVghssArg0::new().with_vghss(0x3), // 13 V
|
||||||
|
) },
|
||||||
|
{ command: CmdTescmd(
|
||||||
|
CmdTescmdArg0::new(),
|
||||||
|
) },
|
||||||
|
{ command: CmdVgls(
|
||||||
|
CmdVglsArg0::new().with_vgls(0x9) // -10.17 V
|
||||||
|
) },
|
||||||
|
{ command: CmdPwctrl1(
|
||||||
|
CmdPwctrl1Arg0::new()
|
||||||
|
.with_apos(0x3) // Max
|
||||||
|
.with_apis(0x1) // Min
|
||||||
|
.with_ap(0x2) // Middle
|
||||||
|
) },
|
||||||
|
{ command: CmdPwctrl2(
|
||||||
|
CmdPwctrl2Arg0::new()
|
||||||
|
.with_avcl(0x3) // -5 V
|
||||||
|
.with_avdd(0x3) // 6.8 V
|
||||||
|
) },
|
||||||
|
{ command: CmdPwctrl3(
|
||||||
|
CmdPwctrl3Arg0::new()
|
||||||
|
.with_svno_pum(0) // Cell setting 4
|
||||||
|
.with_svpo_pum(0x1) // Cell setting 5
|
||||||
|
) },
|
||||||
|
{ command: CmdPclks2(
|
||||||
|
CmdPclks2Arg0::new().with_sbstcks(0x3)
|
||||||
|
) },
|
||||||
|
{ command: CmdPdr1(
|
||||||
|
CmdPdr1Arg0::new().with_t2d(8) // 1.6 us
|
||||||
|
) },
|
||||||
|
{ command: CmdPdr2(
|
||||||
|
CmdPdr2Arg0::new().with_t3d(8) // 6.4 us
|
||||||
|
) },
|
||||||
|
{ command: CmdMipiset1(
|
||||||
|
CmdMipiset1Arg0::new().with_err_sel(0).with_eotp_en(true),
|
||||||
|
) },
|
||||||
|
{ command: CustomCommand {
|
||||||
|
address: 0xE0,
|
||||||
|
args: &[
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x02,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x0C,
|
||||||
|
]
|
||||||
|
} },
|
||||||
|
{ command: CustomCommand {
|
||||||
|
address: 0xE1,
|
||||||
|
args: &[
|
||||||
|
0x05,
|
||||||
|
0x96,
|
||||||
|
0x07,
|
||||||
|
0x96,
|
||||||
|
0x06,
|
||||||
|
0x96,
|
||||||
|
0x08,
|
||||||
|
0x96,
|
||||||
|
0x00,
|
||||||
|
0x44,
|
||||||
|
0x44,
|
||||||
|
]
|
||||||
|
} },
|
||||||
|
{ command: CustomCommand {
|
||||||
|
address: 0xE2,
|
||||||
|
args: &[
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x03,
|
||||||
|
0x03,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x02,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x02,
|
||||||
|
0x00,
|
||||||
|
]
|
||||||
|
} },
|
||||||
|
{ command: CustomCommand {
|
||||||
|
address: 0xE3,
|
||||||
|
args: &[
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x33,
|
||||||
|
0x33,
|
||||||
|
]
|
||||||
|
} },
|
||||||
|
{ command: CustomCommand {
|
||||||
|
address: 0xE4,
|
||||||
|
args: &[
|
||||||
|
0x44,
|
||||||
|
0x44,
|
||||||
|
]
|
||||||
|
} },
|
||||||
|
{ command: CustomCommand {
|
||||||
|
address: 0xE5,
|
||||||
|
args: &[
|
||||||
|
0x0D,
|
||||||
|
0xD4,
|
||||||
|
0x28,
|
||||||
|
0x8C,
|
||||||
|
0x0F,
|
||||||
|
0xD6,
|
||||||
|
0x28,
|
||||||
|
0x8C,
|
||||||
|
0x09,
|
||||||
|
0xD0,
|
||||||
|
0x28,
|
||||||
|
0x8C,
|
||||||
|
0x0B,
|
||||||
|
0xD2,
|
||||||
|
0x28,
|
||||||
|
0x8C,
|
||||||
|
]
|
||||||
|
} },
|
||||||
|
{ command: CustomCommand {
|
||||||
|
address: 0xE6,
|
||||||
|
args: &[
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x33,
|
||||||
|
0x33,
|
||||||
|
]
|
||||||
|
} },
|
||||||
|
{ command: CustomCommand {
|
||||||
|
address: 0xE7,
|
||||||
|
args: &[
|
||||||
|
0x44,
|
||||||
|
0x44,
|
||||||
|
]
|
||||||
|
} },
|
||||||
|
{ command: CustomCommand {
|
||||||
|
address: 0xE8,
|
||||||
|
args: &[
|
||||||
|
0x0E,
|
||||||
|
0xD5,
|
||||||
|
0x28,
|
||||||
|
0x8C,
|
||||||
|
0x10,
|
||||||
|
0xD7,
|
||||||
|
0x28,
|
||||||
|
0x8C,
|
||||||
|
0x0A,
|
||||||
|
0xD1,
|
||||||
|
0x28,
|
||||||
|
0x8C,
|
||||||
|
0x0C,
|
||||||
|
0xD3,
|
||||||
|
0x28,
|
||||||
|
0x8C,
|
||||||
|
]
|
||||||
|
} },
|
||||||
|
{ command: CustomCommand {
|
||||||
|
address: 0xEB,
|
||||||
|
args: &[
|
||||||
|
0x00,
|
||||||
|
0x01,
|
||||||
|
0xE4,
|
||||||
|
0xE4,
|
||||||
|
0x44,
|
||||||
|
0x00,
|
||||||
|
]
|
||||||
|
} },
|
||||||
|
{ command: CustomCommand {
|
||||||
|
address: 0xED,
|
||||||
|
args: &[
|
||||||
|
0xF3,
|
||||||
|
0xC1,
|
||||||
|
0xBA,
|
||||||
|
0x0F,
|
||||||
|
0x66,
|
||||||
|
0x77,
|
||||||
|
0x44,
|
||||||
|
0x55,
|
||||||
|
0x55,
|
||||||
|
0x44,
|
||||||
|
0x77,
|
||||||
|
0x66,
|
||||||
|
0xF0,
|
||||||
|
0xAB,
|
||||||
|
0x1C,
|
||||||
|
0x3F,
|
||||||
|
]
|
||||||
|
} },
|
||||||
|
{ command: CustomCommand {
|
||||||
|
address: 0xEF,
|
||||||
|
args: &[
|
||||||
|
0x10,
|
||||||
|
0x0D,
|
||||||
|
0x04,
|
||||||
|
0x08,
|
||||||
|
0x3F,
|
||||||
|
0x1F,
|
||||||
|
]
|
||||||
|
} },
|
||||||
|
{ command: CmdCn2bkxsel(
|
||||||
|
CmdCn2bkxselArg0::new(),
|
||||||
|
CmdCn2bkxselArg1::new(),
|
||||||
|
CmdCn2bkxselArg2::new(),
|
||||||
|
CmdCn2bkxselArg3::new(),
|
||||||
|
CmdCn2bkxselArg4::new().with_bksel(0x3).with_cn2(true),
|
||||||
|
) },
|
||||||
|
{ command: CustomCommand {
|
||||||
|
address: 0xE8,
|
||||||
|
args: &[
|
||||||
|
0x00,
|
||||||
|
0x0E,
|
||||||
|
],
|
||||||
|
} },
|
||||||
|
{ command: CmdSlpout() },
|
||||||
|
{ sleep_ms: 120 },
|
||||||
|
{ command: CustomCommand {
|
||||||
|
address: 0xE8,
|
||||||
|
args: &[
|
||||||
|
0x00,
|
||||||
|
0x0C,
|
||||||
|
],
|
||||||
|
} },
|
||||||
|
{ sleep_ms: 10 },
|
||||||
|
{ command: CustomCommand {
|
||||||
|
address: 0xE8,
|
||||||
|
args: &[
|
||||||
|
0x40,
|
||||||
|
0x00,
|
||||||
|
],
|
||||||
|
} },
|
||||||
|
{ command: CmdCn2bkxsel(
|
||||||
|
CmdCn2bkxselArg0::new(),
|
||||||
|
CmdCn2bkxselArg1::new(),
|
||||||
|
CmdCn2bkxselArg2::new(),
|
||||||
|
CmdCn2bkxselArg3::new(),
|
||||||
|
CmdCn2bkxselArg4::new().with_bksel(0).with_cn2(false),
|
||||||
|
) },
|
||||||
|
{ command: CmdMadctl(
|
||||||
|
CmdMadctlArg0::new().with_bgr(false).with_ml(false),
|
||||||
|
) },
|
||||||
|
{ command: CmdColmod(
|
||||||
|
CmdColmodArg0::new().with_vipf(6) // 18-bit pixel
|
||||||
|
) },
|
||||||
|
{ command: CmdDispon() },
|
||||||
|
{ sleep_ms: 20 },
|
||||||
|
// { command: CmdCn2bkxsel(
|
||||||
|
// CmdCn2bkxselArg0::new(),
|
||||||
|
// CmdCn2bkxselArg1::new(),
|
||||||
|
// CmdCn2bkxselArg2::new(),
|
||||||
|
// CmdCn2bkxselArg3::new(),
|
||||||
|
// CmdCn2bkxselArg4::new().with_bksel(0).with_cn2(true),
|
||||||
|
// ) },
|
||||||
|
// { command: CmdPorctrl(
|
||||||
|
// CmdPorctrlArg0::new().with_vbp(0xFF),
|
||||||
|
// CmdPorctrlArg1::new().with_vfp(0x0),
|
||||||
|
// ) },
|
||||||
|
// // { command: CmdRgbctrl(
|
||||||
|
// // CmdRgbctrlArg0::new()
|
||||||
|
// // .with_ep(false)
|
||||||
|
// // .with_dp(false)
|
||||||
|
// // .with_hsp(false)
|
||||||
|
// // .with_vsp(false)
|
||||||
|
// // .with_de_hv(true),
|
||||||
|
// // CmdRgbctrlArg1::new()
|
||||||
|
// // .with_hbp_hvrgb(16),
|
||||||
|
// // CmdRgbctrlArg2::new()
|
||||||
|
// // .with_vbp_hvrgb(8),
|
||||||
|
// // ) },
|
||||||
|
// { command: CmdCn2bkxsel(
|
||||||
|
// CmdCn2bkxselArg0::new(),
|
||||||
|
// CmdCn2bkxselArg1::new(),
|
||||||
|
// CmdCn2bkxselArg2::new(),
|
||||||
|
// CmdCn2bkxselArg3::new(),
|
||||||
|
// CmdCn2bkxselArg4::new().with_bksel(0).with_cn2(false),
|
||||||
|
// ) },
|
||||||
|
// { sleep_ms: 20 },
|
||||||
|
];
|
||||||
230
firmware/acid-firmware/src/peripherals/st7701s/mod.rs
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
use embassy_time::Timer;
|
||||||
|
use esp_hal::{
|
||||||
|
DriverMode,
|
||||||
|
gpio::{Flex, Level, Output},
|
||||||
|
lcd_cam::lcd::{
|
||||||
|
ClockMode, DelayMode, Phase, Polarity,
|
||||||
|
dpi::{Dpi, Format, FrameTiming},
|
||||||
|
},
|
||||||
|
ledc::{self, LowSpeed, channel::ChannelIFace, timer::TimerIFace},
|
||||||
|
time::Rate,
|
||||||
|
};
|
||||||
|
use log::debug;
|
||||||
|
use ouroboros::self_referencing;
|
||||||
|
|
||||||
|
use crate::peripherals::st7701s::{commands::Command, init_sequence::InitSequenceAction};
|
||||||
|
|
||||||
|
mod commands;
|
||||||
|
mod init_sequence;
|
||||||
|
mod spi;
|
||||||
|
|
||||||
|
#[self_referencing]
|
||||||
|
pub struct Backlight<'d> {
|
||||||
|
pub timer: ledc::timer::Timer<'d, LowSpeed>,
|
||||||
|
#[borrows(timer)]
|
||||||
|
#[covariant]
|
||||||
|
pub channel: ledc::channel::Channel<'this, LowSpeed>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Backlight<'a> {
|
||||||
|
pub fn set_duty(&mut self, duty_pct: u8) -> Result<(), ledc::channel::Error> {
|
||||||
|
self.borrow_channel().set_duty(duty_pct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct St7701sController<'d> {
|
||||||
|
pub sck: Output<'d>,
|
||||||
|
pub mosi: Flex<'d>,
|
||||||
|
pub cs: Output<'d>,
|
||||||
|
pub backlight: Backlight<'d>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'d> St7701sController<'d> {
|
||||||
|
pub async fn send_init_sequence(&mut self) {
|
||||||
|
debug!("Writing ST7701S init sequence.");
|
||||||
|
|
||||||
|
for action in &init_sequence::INIT_SEQUENCE_COMMANDS {
|
||||||
|
match action {
|
||||||
|
InitSequenceAction::Command(command) => {
|
||||||
|
spi::spi_write(
|
||||||
|
command.address(),
|
||||||
|
command.args_iter().copied(),
|
||||||
|
&mut self.mosi,
|
||||||
|
&mut self.sck,
|
||||||
|
&mut self.cs,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
InitSequenceAction::SleepMs(delay_ms) => {
|
||||||
|
Timer::after_millis(*delay_ms).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send(&mut self, command: impl Command) {
|
||||||
|
spi::spi_write(
|
||||||
|
command.address(),
|
||||||
|
command.args_iter().copied(),
|
||||||
|
&mut self.mosi,
|
||||||
|
&mut self.sck,
|
||||||
|
&mut self.cs,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Puts the display into sleep mode and disables the backlight.
|
||||||
|
pub async fn sleep_on(&mut self) {
|
||||||
|
self.backlight.set_duty(0).unwrap();
|
||||||
|
self.send(commands::CmdSlpin()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Brings the display out of sleep mode and enables the backlight.
|
||||||
|
pub async fn sleep_off(&mut self) {
|
||||||
|
self.send(commands::CmdSlpout()).await;
|
||||||
|
self.backlight.set_duty(100).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct St7701s<'d, Dm>
|
||||||
|
where
|
||||||
|
Dm: DriverMode,
|
||||||
|
{
|
||||||
|
pub controller: St7701sController<'d>,
|
||||||
|
pub dpi: Dpi<'d, Dm>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'d, Dm> St7701s<'d, Dm>
|
||||||
|
where
|
||||||
|
Dm: DriverMode,
|
||||||
|
{
|
||||||
|
/// Initializes an ST7701S display, and puts it to sleep.
|
||||||
|
/// To turn it on, invoke `St7701sController:sleep_off`.
|
||||||
|
pub async fn new(
|
||||||
|
mut sck: Output<'d>,
|
||||||
|
mut mosi: Flex<'d>,
|
||||||
|
mut cs: Output<'d>,
|
||||||
|
mut unconfigured_dpi: Dpi<'d, Dm>,
|
||||||
|
mut bl_timer: ledc::timer::Timer<'d, LowSpeed>,
|
||||||
|
bl_channel: ledc::channel::Channel<'d, LowSpeed>,
|
||||||
|
) -> Self {
|
||||||
|
sck.apply_config(&Default::default());
|
||||||
|
sck.set_high();
|
||||||
|
cs.apply_config(&Default::default());
|
||||||
|
cs.set_high();
|
||||||
|
mosi.apply_input_config(&Default::default());
|
||||||
|
mosi.apply_output_config(&Default::default());
|
||||||
|
mosi.set_input_enable(false);
|
||||||
|
mosi.set_output_enable(true);
|
||||||
|
|
||||||
|
let lcd_config = esp_hal::lcd_cam::lcd::dpi::Config::default()
|
||||||
|
// Internal memory can use the full 16 MHz, but when external PSRAM is used, it cannot keep up with the display.
|
||||||
|
// For that reason, we choose the highest value for which it doesn't glitch by showing black
|
||||||
|
// stripes on the screen.
|
||||||
|
//
|
||||||
|
// There are three knobs you can turn to improve the bandwidth situation.
|
||||||
|
// - increase psram frequency
|
||||||
|
// - decrease the peripheral's frequency
|
||||||
|
// - prevent flash from being used whilst your program is running. (There's a PR to make
|
||||||
|
// this easy to do)
|
||||||
|
// https://github.com/esp-rs/esp-hal/pull/3024
|
||||||
|
//
|
||||||
|
// Adafruit would use 11 MHz.
|
||||||
|
// I had lowered the frequency, so that `DmaBounce` could keep up.
|
||||||
|
.with_frequency(Rate::from_mhz(9)) // From Adafruit
|
||||||
|
.with_clock_mode(ClockMode {
|
||||||
|
polarity: Polarity::IdleLow, // From Adafruit
|
||||||
|
phase: Phase::ShiftHigh, // From Adafruit
|
||||||
|
})
|
||||||
|
.with_de_mode(DelayMode::RaisingEdge)
|
||||||
|
.with_hsync_mode(DelayMode::RaisingEdge)
|
||||||
|
.with_vsync_mode(DelayMode::RaisingEdge)
|
||||||
|
.with_output_bit_mode(DelayMode::RaisingEdge)
|
||||||
|
.with_format(Format {
|
||||||
|
enable_2byte_mode: true,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.with_timing({
|
||||||
|
// Adafruit's config for this LCD:
|
||||||
|
// https://github.com/adafruit/Adafruit_CircuitPython_Qualia/blob/742d336e05e6a4d8bdaa46e15bbf60c9f30d2eba/adafruit_qualia/displays/bar240x960.py#L81-L97
|
||||||
|
// https://github.com/adafruit/Adafruit_CircuitPython_Qualia/blob/742d336e05e6a4d8bdaa46e15bbf60c9f30d2eba/adafruit_qualia/displays/__init__.py#L59-L62
|
||||||
|
// CircuitPython code handling Adafruit's config
|
||||||
|
// https://github.com/adafruit/circuitpython/blob/97c6617817e95b1f6aa2ce458778aaa8371de39b/ports/espressif/common-hal/dotclockframebuffer/DotClockFramebuffer.c#L63
|
||||||
|
// ESP-IDF peripheral configuration code:
|
||||||
|
// https://github.com/espressif/esp-idf/blob/800f141f94c0f880c162de476512e183df671307/components/esp_lcd/rgb/esp_lcd_panel_rgb.c#L556
|
||||||
|
// Espressif's docs:
|
||||||
|
// https://docs.espressif.com/projects/esp-idf/en/v5.5.1/esp32s3/api-reference/peripherals/lcd/rgb_lcd.html#structures
|
||||||
|
|
||||||
|
// TODO: Investigate PORCTRL instruction in datasheet of ST7701
|
||||||
|
let horizontal_resolution: usize = 240;
|
||||||
|
let vertical_resolution = 960;
|
||||||
|
let overscan_left = 120;
|
||||||
|
let vsync_width = 8;
|
||||||
|
let hsync_width = 8;
|
||||||
|
let horizontal_blank_front_porch = 20;
|
||||||
|
let horizontal_blank_back_porch = 20;
|
||||||
|
let vertical_blank_front_porch = 20;
|
||||||
|
let vertical_blank_back_porch = 20;
|
||||||
|
let hsync_position = 0;
|
||||||
|
let horizontal_active_width =
|
||||||
|
(horizontal_resolution + overscan_left).div_ceil(16) * 16; // Round up to a multiple of 16.
|
||||||
|
let vertical_active_height = vertical_resolution;
|
||||||
|
FrameTiming {
|
||||||
|
horizontal_total_width: hsync_width
|
||||||
|
+ horizontal_blank_back_porch
|
||||||
|
+ horizontal_active_width
|
||||||
|
+ horizontal_blank_front_porch,
|
||||||
|
vertical_total_height: vsync_width
|
||||||
|
+ vertical_blank_back_porch
|
||||||
|
+ vertical_active_height
|
||||||
|
+ vertical_blank_front_porch,
|
||||||
|
horizontal_blank_front_porch: horizontal_blank_front_porch + hsync_width,
|
||||||
|
vertical_blank_front_porch: vertical_blank_front_porch + vsync_width,
|
||||||
|
horizontal_active_width,
|
||||||
|
vertical_active_height,
|
||||||
|
vsync_width,
|
||||||
|
hsync_width,
|
||||||
|
hsync_position,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with_hsync_idle_level(Level::High)
|
||||||
|
.with_vsync_idle_level(Level::High)
|
||||||
|
.with_de_idle_level(Level::Low);
|
||||||
|
|
||||||
|
unconfigured_dpi.apply_config(&lcd_config).unwrap();
|
||||||
|
|
||||||
|
bl_timer
|
||||||
|
.configure(ledc::timer::config::Config {
|
||||||
|
duty: ledc::timer::config::Duty::Duty5Bit,
|
||||||
|
clock_source: ledc::timer::LSClockSource::APBClk,
|
||||||
|
frequency: Rate::from_khz(24),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let backlight = Backlight::new(bl_timer, move |bl_timer| {
|
||||||
|
let mut bl_channel = bl_channel; // Forces bl_channel to be moved before it is mutated.
|
||||||
|
bl_channel
|
||||||
|
.configure(ledc::channel::config::Config {
|
||||||
|
timer: bl_timer,
|
||||||
|
drive_mode: esp_hal::gpio::DriveMode::PushPull,
|
||||||
|
duty_pct: 0,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
bl_channel
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut lcd = Self {
|
||||||
|
controller: St7701sController {
|
||||||
|
sck,
|
||||||
|
mosi,
|
||||||
|
cs,
|
||||||
|
backlight,
|
||||||
|
},
|
||||||
|
dpi: unconfigured_dpi,
|
||||||
|
};
|
||||||
|
|
||||||
|
lcd.controller.send_init_sequence().await;
|
||||||
|
lcd.controller.sleep_on().await;
|
||||||
|
|
||||||
|
lcd
|
||||||
|
}
|
||||||
|
}
|
||||||
102
firmware/acid-firmware/src/peripherals/st7701s/spi.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
use core::iter::once;
|
||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
use esp_hal::gpio::{Flex, Level, Output};
|
||||||
|
|
||||||
|
async fn spi_delay() {
|
||||||
|
Timer::after(Duration::from_micros(1)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn spi_dummy_bit(sck: &mut Output<'_>) {
|
||||||
|
sck.set_low();
|
||||||
|
spi_delay().await;
|
||||||
|
sck.set_high();
|
||||||
|
spi_delay().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn spi_write_bit(bit: bool, mosi: &mut Flex<'_>, sck: &mut Output<'_>) {
|
||||||
|
mosi.set_level(if bit { Level::High } else { Level::Low });
|
||||||
|
sck.set_low();
|
||||||
|
spi_delay().await;
|
||||||
|
sck.set_high();
|
||||||
|
spi_delay().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn spi_read_bit(mosi: &mut Flex<'_>, sck: &mut Output<'_>) -> bool {
|
||||||
|
sck.set_low();
|
||||||
|
spi_delay().await;
|
||||||
|
sck.set_high();
|
||||||
|
spi_delay().await;
|
||||||
|
mosi.is_high()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn spi_write_bits(
|
||||||
|
bits: impl Iterator<Item = bool>,
|
||||||
|
mosi: &mut Flex<'_>,
|
||||||
|
sck: &mut Output<'_>,
|
||||||
|
) {
|
||||||
|
for bit in bits {
|
||||||
|
spi_write_bit(bit, mosi, sck).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn spi_write_word(is_param: bool, data: u8, mosi: &mut Flex<'_>, sck: &mut Output<'_>) {
|
||||||
|
assert!(sck.is_set_high());
|
||||||
|
spi_write_bits(
|
||||||
|
once(is_param).chain((0..8).map(|i| (data >> i) & 1 != 0).rev()),
|
||||||
|
mosi,
|
||||||
|
sck,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn spi_write(
|
||||||
|
command: u8,
|
||||||
|
args: impl IntoIterator<Item = u8>,
|
||||||
|
mosi: &mut Flex<'_>,
|
||||||
|
sck: &mut Output<'_>,
|
||||||
|
cs: &mut Output<'_>,
|
||||||
|
) {
|
||||||
|
cs.set_low();
|
||||||
|
spi_write_word(false, command, mosi, sck).await;
|
||||||
|
|
||||||
|
for arg in args {
|
||||||
|
spi_write_word(true, arg, mosi, sck).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
cs.set_high();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[expect(unused)]
|
||||||
|
pub async fn spi_read(
|
||||||
|
command: u8,
|
||||||
|
dummy_cycle: bool,
|
||||||
|
mosi: &mut Flex<'_>,
|
||||||
|
sck: &mut Output<'_>,
|
||||||
|
cs: &mut Output<'_>,
|
||||||
|
output_buffer: &mut [u8],
|
||||||
|
) {
|
||||||
|
output_buffer.fill(0);
|
||||||
|
|
||||||
|
cs.set_low();
|
||||||
|
spi_write_word(false, command, mosi, sck).await;
|
||||||
|
|
||||||
|
mosi.set_output_enable(false);
|
||||||
|
mosi.set_input_enable(true);
|
||||||
|
|
||||||
|
if dummy_cycle {
|
||||||
|
spi_dummy_bit(sck).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
for output_byte in output_buffer {
|
||||||
|
for i in (0..8).rev() {
|
||||||
|
if spi_read_bit(mosi, sck).await {
|
||||||
|
*output_byte |= 1 << i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mosi.set_input_enable(false);
|
||||||
|
mosi.set_output_enable(true);
|
||||||
|
|
||||||
|
cs.set_high();
|
||||||
|
}
|
||||||
616
firmware/acid-firmware/src/proxy.rs
Normal file
|
|
@ -0,0 +1,616 @@
|
||||||
|
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::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::ram::PsramAllocator;
|
||||||
|
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, PsramAllocator>;
|
||||||
|
type KeysymMap = BTreeMap<Keysym, KeysymEntries, PsramAllocator>;
|
||||||
|
|
||||||
|
/// 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, PsramAllocator>, 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);
|
||||||
373
firmware/acid-firmware/src/ram.rs
Normal file
|
|
@ -0,0 +1,373 @@
|
||||||
|
use embassy_time::Timer;
|
||||||
|
use esp_alloc::{HeapRegion, MemoryCapability};
|
||||||
|
use esp_hal::ram;
|
||||||
|
use log::{info, warn};
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// RAM usage of static variables can be diagnosed with:
|
||||||
|
// ```sh
|
||||||
|
// xtensa-esp32s3-elf-nm.exe -S --size-sort -t d ../target/xtensa-esp32s3-none-elf/release/acid-firmware | grep -iE ' [dbv] ' | tail -n 10 | tac
|
||||||
|
// ```
|
||||||
|
|
||||||
|
/// Total heap size
|
||||||
|
pub const HEAP_SIZE: usize = 196 * 1024;
|
||||||
|
/// Size of the app core's stack
|
||||||
|
pub const STACK_SIZE_CORE_APP: usize = 80 * 1024;
|
||||||
|
|
||||||
|
pub static PSRAM_ALLOCATOR: esp_alloc::EspHeap = esp_alloc::EspHeap::empty();
|
||||||
|
pub type PsramAllocator = &'static esp_alloc::EspHeap;
|
||||||
|
|
||||||
|
pub fn initialize(psram_peripheral: esp_hal::peripherals::PSRAM) {
|
||||||
|
// Use the internal DRAM as the heap.
|
||||||
|
// Memory reclaimed from the esp-idf bootloader.
|
||||||
|
const HEAP_SIZE_RECLAIMED: usize = const {
|
||||||
|
let range = esp_metadata_generated::memory_range!("DRAM2_UNINIT");
|
||||||
|
range.end - range.start
|
||||||
|
};
|
||||||
|
|
||||||
|
esp_alloc::heap_allocator!(#[ram(reclaimed)] size: HEAP_SIZE_RECLAIMED);
|
||||||
|
esp_alloc::heap_allocator!(size: HEAP_SIZE - HEAP_SIZE_RECLAIMED);
|
||||||
|
|
||||||
|
#[cfg(not(feature = "no-alloc-tracing"))]
|
||||||
|
alloc_tracing::install_ram_allocator_proxy();
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Internal RAM heap initialized!\n{}",
|
||||||
|
esp_alloc::HEAP.stats()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize the PSRAM allocator.
|
||||||
|
{
|
||||||
|
let (psram_offset, psram_size) = esp_hal::psram::psram_raw_parts(&psram_peripheral);
|
||||||
|
unsafe {
|
||||||
|
PSRAM_ALLOCATOR.add_region(HeapRegion::new(
|
||||||
|
psram_offset,
|
||||||
|
psram_size,
|
||||||
|
MemoryCapability::External.into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
info!(
|
||||||
|
"External PSRAM heap initialized with capacity of {} MiB!\n{}",
|
||||||
|
psram_size / 1024 / 1024,
|
||||||
|
PSRAM_ALLOCATOR.stats(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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!(
|
||||||
|
"External PSRAM heap 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!(
|
||||||
|
"Internal RAM heap usage changed: {}{}\n{heap_stats}",
|
||||||
|
if difference < 0 { '-' } else { '+' },
|
||||||
|
difference.abs()
|
||||||
|
);
|
||||||
|
#[cfg(not(feature = "no-alloc-tracing"))]
|
||||||
|
alloc_tracing::report_stats();
|
||||||
|
}
|
||||||
|
Timer::after_secs(1).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "no-alloc-tracing"))]
|
||||||
|
mod alloc_tracing {
|
||||||
|
use core::{
|
||||||
|
alloc::GlobalAlloc,
|
||||||
|
cell::RefCell,
|
||||||
|
cmp::{Ordering, Reverse},
|
||||||
|
fmt::Display,
|
||||||
|
};
|
||||||
|
|
||||||
|
use alloc::collections::btree_map::BTreeMap;
|
||||||
|
use embassy_sync::blocking_mutex::{Mutex, raw::CriticalSectionRawMutex};
|
||||||
|
use esp_alloc::EspHeap;
|
||||||
|
use esp_backtrace::Backtrace;
|
||||||
|
use esp_sync::NonReentrantMutex;
|
||||||
|
use log::warn;
|
||||||
|
use tinyvec::ArrayVec;
|
||||||
|
|
||||||
|
use crate::ram::{PSRAM_ALLOCATOR, PsramAllocator};
|
||||||
|
|
||||||
|
#[derive(Default, Clone, PartialEq, Eq, Debug)]
|
||||||
|
struct Stats {
|
||||||
|
/// The total number of allocations.
|
||||||
|
allocations: usize,
|
||||||
|
/// The number of bytes allocated in total.
|
||||||
|
allocated_total: usize,
|
||||||
|
/// The number of bytes allocated minus the number of bytes deallocated.
|
||||||
|
allocated_current: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stats {
|
||||||
|
fn update(&mut self, allocations_delta: isize, bytes_delta: isize, bytes_new: usize) {
|
||||||
|
self.allocations = (self.allocations as isize + allocations_delta) as usize;
|
||||||
|
self.allocated_total += bytes_new;
|
||||||
|
self.allocated_current = (self.allocated_current as isize + bytes_delta) as usize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Stats {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Stats {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
self.allocated_current
|
||||||
|
.cmp(&other.allocated_current)
|
||||||
|
.then_with(|| self.allocated_total.cmp(&other.allocated_total).reverse())
|
||||||
|
.then_with(|| self.allocations.cmp(&other.allocations).reverse())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct BacktraceWrapper(Backtrace);
|
||||||
|
|
||||||
|
impl Display for BacktraceWrapper {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
let mut it = self.0.frames().iter();
|
||||||
|
if let Some(frame) = it.next() {
|
||||||
|
write!(f, "0x{:08x}", frame.program_counter())?;
|
||||||
|
}
|
||||||
|
while let Some(frame) = it.next() {
|
||||||
|
write!(f, " 0x{:08x}", frame.program_counter())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for BacktraceWrapper {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
if self.0.frames().len() != other.0.frames().len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (lhs_frame, rhs_frame) in self.0.frames().iter().zip(other.0.frames()) {
|
||||||
|
if lhs_frame.program_counter() != rhs_frame.program_counter() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for BacktraceWrapper {}
|
||||||
|
|
||||||
|
impl PartialOrd for BacktraceWrapper {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for BacktraceWrapper {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
let mut lhs_it = self.0.frames().iter().rev();
|
||||||
|
let mut rhs_it = other.0.frames().iter().rev();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match (lhs_it.next(), rhs_it.next()) {
|
||||||
|
(Some(lhs_frame), Some(rhs_frame)) => {
|
||||||
|
let ordering = lhs_frame
|
||||||
|
.program_counter()
|
||||||
|
.cmp(&rhs_frame.program_counter());
|
||||||
|
|
||||||
|
if ordering != Ordering::Equal {
|
||||||
|
return ordering;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some(_), None) => return Ordering::Greater,
|
||||||
|
(None, Some(_)) => return Ordering::Less,
|
||||||
|
(None, None) => return Ordering::Equal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct AllocationTracer {
|
||||||
|
bt_to_stats: BTreeMap<BacktraceWrapper, Stats, PsramAllocator>,
|
||||||
|
ptr_to_bt: BTreeMap<*mut u8, BacktraceWrapper, PsramAllocator>,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for AllocationTracer {}
|
||||||
|
|
||||||
|
impl AllocationTracer {
|
||||||
|
const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
bt_to_stats: BTreeMap::new_in(&PSRAM_ALLOCATOR),
|
||||||
|
ptr_to_bt: BTreeMap::new_in(&PSRAM_ALLOCATOR),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn alloc(&mut self, bt: Backtrace, ptr: *mut u8, bytes: usize) {
|
||||||
|
let bt = BacktraceWrapper(bt.clone());
|
||||||
|
self.ptr_to_bt.insert(ptr, bt.clone());
|
||||||
|
let stats = self
|
||||||
|
.bt_to_stats
|
||||||
|
.entry(bt)
|
||||||
|
.or_insert_with(|| Default::default());
|
||||||
|
stats.update(1, bytes as isize, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn realloc(
|
||||||
|
&mut self,
|
||||||
|
ptr_old: *mut u8,
|
||||||
|
ptr_new: *mut u8,
|
||||||
|
bytes_old: usize,
|
||||||
|
bytes_new: usize,
|
||||||
|
) {
|
||||||
|
let bt = self.ptr_to_bt.remove(&ptr_old).unwrap();
|
||||||
|
self.ptr_to_bt.insert(ptr_new, bt.clone());
|
||||||
|
let stats = self.bt_to_stats.get_mut(&bt).unwrap();
|
||||||
|
stats.update(0, bytes_new as isize - bytes_old as isize, bytes_new);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dealloc(&mut self, ptr: *mut u8, bytes: usize) {
|
||||||
|
let bt = self.ptr_to_bt.remove(&ptr).unwrap();
|
||||||
|
let stats = self.bt_to_stats.get_mut(&bt).unwrap();
|
||||||
|
stats.update(-1, -(bytes as isize), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TracingAllocator<T: GlobalAlloc + 'static> {
|
||||||
|
inner: NonReentrantMutex<Option<&'static T>>,
|
||||||
|
tracer: Mutex<CriticalSectionRawMutex, RefCell<AllocationTracer>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> TracingAllocator<T>
|
||||||
|
where
|
||||||
|
T: GlobalAlloc,
|
||||||
|
{
|
||||||
|
const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: NonReentrantMutex::new(None),
|
||||||
|
tracer: Mutex::new(RefCell::new(AllocationTracer::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_inner<R>(&self, callback: impl FnOnce(&T) -> R) -> R {
|
||||||
|
self.inner.with(|inner| {
|
||||||
|
(callback)(
|
||||||
|
inner
|
||||||
|
.as_ref()
|
||||||
|
.expect("an allocator must be installed in the global allocator proxy"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_with<R>(&self, callback: impl FnOnce(&mut AllocationTracer) -> R) -> R {
|
||||||
|
self.tracer
|
||||||
|
.lock(|tracer| (callback)(&mut tracer.borrow_mut()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl<T> GlobalAlloc for TracingAllocator<T>
|
||||||
|
where
|
||||||
|
T: GlobalAlloc,
|
||||||
|
{
|
||||||
|
unsafe fn alloc(&self, layout: core::alloc::Layout) -> *mut u8 {
|
||||||
|
let bt = Backtrace::capture();
|
||||||
|
let ptr = self.with_inner(|inner| unsafe { inner.alloc(layout) });
|
||||||
|
self.update_with(|tracer| tracer.alloc(bt, ptr, layout.size()));
|
||||||
|
ptr
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn alloc_zeroed(&self, layout: core::alloc::Layout) -> *mut u8 {
|
||||||
|
let bt = Backtrace::capture();
|
||||||
|
let ptr = self.with_inner(|inner| unsafe { inner.alloc_zeroed(layout) });
|
||||||
|
self.update_with(|tracer| tracer.alloc(bt, ptr, layout.size()));
|
||||||
|
ptr
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn realloc(
|
||||||
|
&self,
|
||||||
|
ptr_old: *mut u8,
|
||||||
|
layout: core::alloc::Layout,
|
||||||
|
new_size: usize,
|
||||||
|
) -> *mut u8 {
|
||||||
|
let ptr_new =
|
||||||
|
self.with_inner(|inner| unsafe { inner.realloc(ptr_old, layout, new_size) });
|
||||||
|
self.update_with(|tracer| tracer.realloc(ptr_old, ptr_new, layout.size(), new_size));
|
||||||
|
ptr_new
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn dealloc(&self, ptr: *mut u8, layout: core::alloc::Layout) {
|
||||||
|
self.update_with(|tracer| tracer.dealloc(ptr, layout.size()));
|
||||||
|
self.with_inner(|inner| unsafe { inner.dealloc(ptr, layout) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[global_allocator]
|
||||||
|
static PROXY: TracingAllocator<EspHeap> = TracingAllocator::new();
|
||||||
|
|
||||||
|
pub fn install_ram_allocator_proxy() {
|
||||||
|
PROXY.inner.with(|inner| {
|
||||||
|
*inner = Some(&esp_alloc::HEAP);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn report_stats() {
|
||||||
|
let AllocationTracer {
|
||||||
|
bt_to_stats,
|
||||||
|
ptr_to_bt,
|
||||||
|
} = PROXY.tracer.lock(|tracer| tracer.borrow().clone());
|
||||||
|
// Wrapped in `Option` because of the `Default` requirement.
|
||||||
|
let mut sorted = ArrayVec::<[Option<(BacktraceWrapper, Reverse<Stats>)>; 5]>::new();
|
||||||
|
let bt_to_stats_len = bt_to_stats.len();
|
||||||
|
|
||||||
|
for (key, value) in bt_to_stats {
|
||||||
|
// Reverse ordering of stats because we are interested in the largest.
|
||||||
|
let value_rev = Reverse(value);
|
||||||
|
if let Some((_, value_last)) = sorted.last().and_then(|last| last.as_ref())
|
||||||
|
&& &value_rev >= value_last
|
||||||
|
&& sorted.len() >= sorted.capacity()
|
||||||
|
{
|
||||||
|
// This stat is not large enough to be inserted.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let index = match sorted.binary_search_by_key(&Some(&value_rev), |item| {
|
||||||
|
item.as_ref().map(|(_, current_value)| current_value)
|
||||||
|
}) {
|
||||||
|
Ok(index) => index,
|
||||||
|
Err(index) => index,
|
||||||
|
};
|
||||||
|
if sorted.len() >= sorted.capacity() {
|
||||||
|
assert!(
|
||||||
|
index < sorted.len(),
|
||||||
|
"the stat should be large enough to be inserted not at the end of the list"
|
||||||
|
);
|
||||||
|
let _ = sorted.pop().unwrap();
|
||||||
|
}
|
||||||
|
sorted.insert(index, Some((key, value_rev)));
|
||||||
|
}
|
||||||
|
|
||||||
|
warn!("Largest allocations in global allocator:");
|
||||||
|
|
||||||
|
for (index, (key, value)) in sorted.into_iter().map(Option::unwrap).enumerate() {
|
||||||
|
warn!("{}. {}\n{:#?}", index + 1, key, value.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn!("bt_to_stats.len() = {}", bt_to_stats_len);
|
||||||
|
warn!("ptr_to_bt.len() = {}", ptr_to_bt.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
167
firmware/acid-firmware/src/ui/backend.rs
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
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 embassy_sync::blocking_mutex::{self, raw::CriticalSectionRawMutex};
|
||||||
|
use esp_hal::time::Instant;
|
||||||
|
use esp_hal_bounce_buffers::SwapchainWriter;
|
||||||
|
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 SlintBackend {
|
||||||
|
pub window_size: [u32; 2],
|
||||||
|
pub window: RefCell<Option<Rc<SoftwareWindowAdapter>>>,
|
||||||
|
pub swapchain: RefCell<SwapchainWriter>,
|
||||||
|
pub quit_event_loop: Arc<AtomicBool>,
|
||||||
|
pub events: Arc<
|
||||||
|
blocking_mutex::Mutex<CriticalSectionRawMutex, 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::SwappedBuffers);
|
||||||
|
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 = self
|
||||||
|
.events
|
||||||
|
.lock(|events| events.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: VSYNC support.
|
||||||
|
let mut swapchain = self.swapchain.borrow_mut();
|
||||||
|
let mut write_guard = swapchain.write();
|
||||||
|
let framebuffer = bytemuck::cast_slice_mut::<u8, Rgb565Pixel>(&mut write_guard);
|
||||||
|
renderer.render(framebuffer, self.window_size[1] as usize);
|
||||||
|
info!("UI rendered.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AcidEventLoopProxy {
|
||||||
|
pub quit_event_loop: Arc<AtomicBool>,
|
||||||
|
pub events: Arc<
|
||||||
|
blocking_mutex::Mutex<CriticalSectionRawMutex, 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> {
|
||||||
|
self.events.lock(|events| {
|
||||||
|
events.borrow_mut().push_back(event);
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
50
firmware/acid-firmware/src/ui/messages.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
use alloc::rc::Rc;
|
||||||
|
use slint::{SharedString, VecModel};
|
||||||
|
use spectre_api_sys::SpectreUserKey;
|
||||||
|
|
||||||
|
use crate::ui::storage::SpectreSite;
|
||||||
|
|
||||||
|
pub enum CallbackMessage {
|
||||||
|
/// The escape key was pressed.
|
||||||
|
Escape,
|
||||||
|
Login(CallbackMessageLogin),
|
||||||
|
Users(CallbackMessageUsers),
|
||||||
|
UserEdit(CallbackMessageUserEdit),
|
||||||
|
UserSites(CallbackMessageUserSites),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum LoginResult {
|
||||||
|
Failure,
|
||||||
|
Success {
|
||||||
|
user_key: SpectreUserKey,
|
||||||
|
sites: Rc<VecModel<SpectreSite>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum CallbackMessageLogin {
|
||||||
|
PwAccepted {
|
||||||
|
user_index: i32,
|
||||||
|
username: SharedString,
|
||||||
|
password: SharedString,
|
||||||
|
},
|
||||||
|
LoginResult {
|
||||||
|
username: SharedString,
|
||||||
|
result: LoginResult,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum CallbackMessageUsers {
|
||||||
|
EditUser { username: SharedString, new: bool },
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum CallbackMessageUserEdit {
|
||||||
|
ComputeIdenticon { password: SharedString },
|
||||||
|
ComputeKeyId { key: SharedString },
|
||||||
|
ConfirmRequest,
|
||||||
|
ConfirmProcessed,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum CallbackMessageUserSites {
|
||||||
|
SiteNameEdited { query: SharedString },
|
||||||
|
SiteNameAccepted { site_list_index: i32 },
|
||||||
|
}
|
||||||
917
firmware/acid-firmware/src/ui/mod.rs
Normal file
|
|
@ -0,0 +1,917 @@
|
||||||
|
// #![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,
|
||||||
|
db::{
|
||||||
|
AcidDatabase, DbKey, DbPathSpectreUserSite, DbPathSpectreUserSites, DbPathSpectreUsers,
|
||||||
|
PartitionAcid, ReadTransactionExt,
|
||||||
|
},
|
||||||
|
ffi::{alloc::__spre_free, crypto::ACTIVE_ENCRYPTED_USER_KEY},
|
||||||
|
proxy::OUTPUT_STRING_CHANNEL,
|
||||||
|
ram::PsramAllocator,
|
||||||
|
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 {
|
||||||
|
ACTIVE_ENCRYPTED_USER_KEY.lock(|user_key| {
|
||||||
|
user_key.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, PsramAllocator>,
|
||||||
|
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>, PsramAllocator> {
|
||||||
|
let users = Self::load_users(&db).await;
|
||||||
|
let usernames = users.users.clone().map(|user| user.username);
|
||||||
|
|
||||||
|
let state = Rc::new_in(
|
||||||
|
RefCell::new(State {
|
||||||
|
window: main.clone_strong(),
|
||||||
|
users,
|
||||||
|
db: Rc::new_in(db, &PSRAM_ALLOCATOR),
|
||||||
|
view: AppState::Login,
|
||||||
|
state_login: Default::default(),
|
||||||
|
state_users: Default::default(),
|
||||||
|
state_user_edit: Default::default(),
|
||||||
|
state_user_sites: Default::default(),
|
||||||
|
}),
|
||||||
|
&PSRAM_ALLOCATOR,
|
||||||
|
);
|
||||||
|
|
||||||
|
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>, PsramAllocator>,
|
||||||
|
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();
|
||||||
|
#[cfg(feature = "limit-fps")]
|
||||||
|
embassy_time::Timer::after(FRAME_DURATION_MIN).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[expect(unreachable_code)]
|
||||||
|
window.hide().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait AppViewTrait {
|
||||||
|
fn process_callback_message(
|
||||||
|
_state_rc: &Rc<RefCell<State>, PsramAllocator>,
|
||||||
|
_message: CallbackMessage,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct StateLogin {}
|
||||||
|
|
||||||
|
impl AppViewTrait for StateLogin {
|
||||||
|
fn process_callback_message(
|
||||||
|
state_rc: &Rc<RefCell<State>, PsramAllocator>,
|
||||||
|
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>, PsramAllocator>,
|
||||||
|
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>, PsramAllocator>,
|
||||||
|
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>, PsramAllocator>,
|
||||||
|
message: CallbackMessage,
|
||||||
|
) {
|
||||||
|
let state = state_rc.clone();
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
match message {
|
||||||
|
CallbackMessage::Escape => {
|
||||||
|
state.set_view(AppState::Login, true, false);
|
||||||
|
}
|
||||||
|
CallbackMessage::UserSites(CallbackMessageUserSites::SiteNameEdited { query }) => {
|
||||||
|
if let Some(user_sites) = state.state_user_sites.as_mut() {
|
||||||
|
user_sites.query = query;
|
||||||
|
state.update_site_list();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CallbackMessage::UserSites(CallbackMessageUserSites::SiteNameAccepted {
|
||||||
|
site_list_index,
|
||||||
|
}) => {
|
||||||
|
let Some(user_sites) = state.state_user_sites.as_mut() else {
|
||||||
|
error!("User sites uninitialized.");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(site_list_entry) = user_sites.site_list.row_data(site_list_index as usize)
|
||||||
|
else {
|
||||||
|
error!("Invalid site list entry index: {site_list_index}");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
warn!("Site name accepted: {site_list_entry:?}");
|
||||||
|
let site_name = match site_list_entry {
|
||||||
|
SiteListEntry::New(site_name) => site_name,
|
||||||
|
SiteListEntry::Existing(site_name) => site_name,
|
||||||
|
};
|
||||||
|
let site_name_c = CString::new(&*site_name).unwrap();
|
||||||
|
let site_password =
|
||||||
|
spectre_derive_site_password(&user_sites.user_key, &site_name_c);
|
||||||
|
|
||||||
|
warn!("Site password: {site_password:?}");
|
||||||
|
|
||||||
|
slint::spawn_local({
|
||||||
|
let username = user_sites.username.clone();
|
||||||
|
let db = state.db.clone();
|
||||||
|
async move {
|
||||||
|
// Send password to the host.
|
||||||
|
OUTPUT_STRING_CHANNEL.send(site_password).await;
|
||||||
|
|
||||||
|
// Update the stored site.
|
||||||
|
let mut write = db.write_transaction().await;
|
||||||
|
let key = DbKey::new(DbPathSpectreUserSite {
|
||||||
|
user_sites: DbPathSpectreUserSites {
|
||||||
|
username: Cow::Borrowed(&username),
|
||||||
|
},
|
||||||
|
site: Cow::Borrowed(&site_name),
|
||||||
|
});
|
||||||
|
let site = SpectreSiteConfig::default();
|
||||||
|
let site_bytes =
|
||||||
|
postcard::to_extend(&site, Vec::<u8, _>::new_in(&PSRAM_ALLOCATOR))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
write.write(&key, &site_bytes).await.unwrap();
|
||||||
|
write.commit().await.unwrap();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SitesModel<M>(Pin<Box<ModelChangeListenerContainer<SitesModelInner<M>>>>)
|
||||||
|
where
|
||||||
|
M: Model + 'static;
|
||||||
|
|
||||||
|
struct SitesModelInner<M>
|
||||||
|
where
|
||||||
|
M: Model + 'static,
|
||||||
|
{
|
||||||
|
wrapped_model: M,
|
||||||
|
notify: ModelNotify,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M> ModelChangeListener for SitesModelInner<M>
|
||||||
|
where
|
||||||
|
M: Model + 'static,
|
||||||
|
{
|
||||||
|
fn row_changed(self: Pin<&Self>, row: usize) {
|
||||||
|
self.notify
|
||||||
|
.row_changed(self.wrapped_model.row_count() - 1 - row);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn row_added(self: Pin<&Self>, index: usize, count: usize) {
|
||||||
|
let row_count = self.wrapped_model.row_count();
|
||||||
|
let old_row_count = row_count - count;
|
||||||
|
let index = old_row_count - index;
|
||||||
|
self.notify.row_added(index, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn row_removed(self: Pin<&Self>, index: usize, count: usize) {
|
||||||
|
let row_count = self.wrapped_model.row_count();
|
||||||
|
self.notify.row_removed(row_count - index, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(self: Pin<&Self>) {
|
||||||
|
self.notify.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M> SitesModel<M>
|
||||||
|
where
|
||||||
|
M: Model + 'static,
|
||||||
|
{
|
||||||
|
pub fn new(wrapped_model: M) -> Self {
|
||||||
|
let inner = SitesModelInner {
|
||||||
|
wrapped_model,
|
||||||
|
notify: Default::default(),
|
||||||
|
};
|
||||||
|
let container = Box::pin(ModelChangeListenerContainer::new(inner));
|
||||||
|
container
|
||||||
|
.wrapped_model
|
||||||
|
.model_tracker()
|
||||||
|
.attach_peer(container.as_ref().model_peer());
|
||||||
|
Self(container)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the inner model
|
||||||
|
pub fn source_model(&self) -> &M {
|
||||||
|
&self.0.as_ref().get().get_ref().wrapped_model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M> Model for SitesModel<M>
|
||||||
|
where
|
||||||
|
M: Model + 'static,
|
||||||
|
{
|
||||||
|
type Data = M::Data;
|
||||||
|
|
||||||
|
fn row_count(&self) -> usize {
|
||||||
|
self.0.wrapped_model.row_count()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn row_data(&self, row: usize) -> Option<Self::Data> {
|
||||||
|
let count = self.0.wrapped_model.row_count();
|
||||||
|
self.0.wrapped_model.row_data(count.checked_sub(row + 1)?)
|
||||||
|
}
|
||||||
|
fn set_row_data(&self, row: usize, data: Self::Data) {
|
||||||
|
let count = self.0.as_ref().wrapped_model.row_count();
|
||||||
|
self.0.wrapped_model.set_row_data(count - row - 1, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn model_tracker(&self) -> &dyn ModelTracker {
|
||||||
|
&self.0.notify
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn core::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum SiteListEntry {
|
||||||
|
New(SharedString),
|
||||||
|
Existing(SharedString),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SiteListEntry {
|
||||||
|
pub fn to_string(&self) -> SharedString {
|
||||||
|
match self {
|
||||||
|
SiteListEntry::New(site) => slint::format!("{site} <Add new site>"),
|
||||||
|
SiteListEntry::Existing(site) => site.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
160
firmware/acid-firmware/src/ui/storage.rs
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
use core::fmt::{Debug, Formatter};
|
||||||
|
|
||||||
|
use alloc::rc::Rc;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use password_hash::Key;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use slint::{Model, SharedString, VecModel};
|
||||||
|
use spectre_api_sys::{SpectreAlgorithm, SpectreCounter, SpectreResultType};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Default)]
|
||||||
|
pub struct SpectreUsersConfig {
|
||||||
|
#[serde(with = "vec_model")]
|
||||||
|
pub users: Rc<VecModel<SpectreUserConfig>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for SpectreUsersConfig {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
|
||||||
|
f.debug_struct("SpectreUsersConfig")
|
||||||
|
.field_with("users", |f| {
|
||||||
|
f.debug_list().entries(self.users.iter()).finish()
|
||||||
|
})
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for SpectreUsersConfig {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
users: Rc::new(self.users.iter().collect()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct SpectreUserConfig {
|
||||||
|
pub username: SharedString,
|
||||||
|
#[serde(with = "serde_bytes")]
|
||||||
|
pub encrypted_key: Key,
|
||||||
|
#[serde(with = "serde_bytes")]
|
||||||
|
pub key_id: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct SpectreSiteConfig {
|
||||||
|
#[serde(with = "with_repr")]
|
||||||
|
pub algorithm: SpectreAlgorithm,
|
||||||
|
pub counter: SpectreCounter::Type,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
#[serde(with = "with_repr")]
|
||||||
|
pub result_type: SpectreResultType,
|
||||||
|
pub password: Option<SharedString>,
|
||||||
|
#[serde(with = "with_repr")]
|
||||||
|
pub login_type: SpectreResultType,
|
||||||
|
pub login_name: Option<SharedString>,
|
||||||
|
pub uses: u32,
|
||||||
|
pub last_used: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SpectreSiteConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
algorithm: SpectreAlgorithm::Current,
|
||||||
|
counter: SpectreCounter::Default,
|
||||||
|
result_type: SpectreResultType::SpectreResultDefaultResult,
|
||||||
|
password: None,
|
||||||
|
login_type: SpectreResultType::SpectreResultDefaultLogin,
|
||||||
|
login_name: None,
|
||||||
|
uses: 0,
|
||||||
|
last_used: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct SpectreSite {
|
||||||
|
pub username: SharedString,
|
||||||
|
pub site_name: SharedString,
|
||||||
|
pub config: SpectreSiteConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
mod vec_model {
|
||||||
|
use alloc::{rc::Rc, vec::Vec};
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
use slint::{Model, VecModel};
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn serialize<T, S>(value: &Rc<VecModel<T>>, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
T: Serialize,
|
||||||
|
VecModel<T>: Model<Data = T>,
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let vec: Vec<T> = value.iter().collect();
|
||||||
|
vec.serialize(serializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Rc<VecModel<T>>, D::Error>
|
||||||
|
where
|
||||||
|
T: Deserialize<'de>,
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let vec = Vec::<T>::deserialize(deserializer)?;
|
||||||
|
Ok(Rc::new(VecModel::from(vec)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod with_repr {
|
||||||
|
use serde::{Deserialize, Deserializer, Serializer};
|
||||||
|
use spectre_api_sys::{SpectreAlgorithm, SpectreResultType};
|
||||||
|
|
||||||
|
pub trait ReprConvert: Copy {
|
||||||
|
type Repr: Copy;
|
||||||
|
|
||||||
|
fn into_repr(self) -> Self::Repr;
|
||||||
|
fn from_repr(repr: Self::Repr) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReprConvert for SpectreAlgorithm {
|
||||||
|
type Repr = u32;
|
||||||
|
|
||||||
|
fn from_repr(repr: Self::Repr) -> Self {
|
||||||
|
unsafe { core::mem::transmute::<Self::Repr, Self>(repr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_repr(self) -> Self::Repr {
|
||||||
|
self as Self::Repr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReprConvert for SpectreResultType {
|
||||||
|
type Repr = u32;
|
||||||
|
|
||||||
|
fn from_repr(repr: Self::Repr) -> Self {
|
||||||
|
unsafe { core::mem::transmute::<Self::Repr, Self>(repr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_repr(self) -> Self::Repr {
|
||||||
|
self as Self::Repr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
T: ReprConvert<Repr = u32>,
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_u32(value.into_repr())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||||
|
where
|
||||||
|
T: ReprConvert<Repr = u32>,
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
<u32 as Deserialize>::deserialize(deserializer).map(T::from_repr)
|
||||||
|
}
|
||||||
|
}
|
||||||
86
firmware/acid-firmware/src/util.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
use core::fmt::Display;
|
||||||
|
|
||||||
|
use embassy_sync::{
|
||||||
|
blocking_mutex::raw::RawMutex,
|
||||||
|
mutex::{Mutex, MutexGuard},
|
||||||
|
};
|
||||||
|
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."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub trait MutexExt<M, T> {
|
||||||
|
type Guard<'a>
|
||||||
|
where
|
||||||
|
M: 'a,
|
||||||
|
T: 'a,
|
||||||
|
Self: 'a;
|
||||||
|
|
||||||
|
/// Locks the mutex, while entering a critical section only during locking and unlocking.
|
||||||
|
///
|
||||||
|
/// # Safety:
|
||||||
|
/// * The guard must not be kept across async `yield` points.
|
||||||
|
/// * This must not be called from within interrupt handlers.
|
||||||
|
/// Otherwise, a deadlock might occur.
|
||||||
|
unsafe fn lock_blocking(&self) -> Self::Guard<'_>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M: RawMutex, T> MutexExt<M, T> for Mutex<M, T> {
|
||||||
|
type Guard<'a>
|
||||||
|
= MutexGuard<'a, M, T>
|
||||||
|
where
|
||||||
|
M: 'a,
|
||||||
|
T: 'a,
|
||||||
|
Self: 'a;
|
||||||
|
|
||||||
|
unsafe fn lock_blocking(&self) -> Self::Guard<'_> {
|
||||||
|
loop {
|
||||||
|
if let Ok(guard) = self.try_lock() {
|
||||||
|
return guard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
firmware/acid-firmware/ui/globals.slint
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export global Style {
|
||||||
|
in property <length> spacing: 8px;
|
||||||
|
}
|
||||||
1
firmware/acid-firmware/ui/images/help-circle.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
||||||
|
After Width: | Height: | Size: 365 B |
1
firmware/acid-firmware/ui/images/key.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-key"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"></path></svg>
|
||||||
|
After Width: | Height: | Size: 352 B |
1
firmware/acid-firmware/ui/images/log-in.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-log-in"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path><polyline points="10 17 15 12 10 7"></polyline><line x1="15" y1="12" x2="3" y2="12"></line></svg>
|
||||||
|
After Width: | Height: | Size: 368 B |
1
firmware/acid-firmware/ui/images/log-out.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-log-out"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>
|
||||||
|
After Width: | Height: | Size: 367 B |
1
firmware/acid-firmware/ui/images/sliders.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sliders"><line x1="4" y1="21" x2="4" y2="14"></line><line x1="4" y1="10" x2="4" y2="3"></line><line x1="12" y1="21" x2="12" y2="12"></line><line x1="12" y1="8" x2="12" y2="3"></line><line x1="20" y1="21" x2="20" y2="16"></line><line x1="20" y1="12" x2="20" y2="3"></line><line x1="1" y1="14" x2="7" y2="14"></line><line x1="9" y1="8" x2="15" y2="8"></line><line x1="17" y1="16" x2="23" y2="16"></line></svg>
|
||||||
|
After Width: | Height: | Size: 611 B |
1
firmware/acid-firmware/ui/images/trash-2.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-trash-2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
|
||||||
|
After Width: | Height: | Size: 448 B |
1
firmware/acid-firmware/ui/images/users.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
|
||||||
|
After Width: | Height: | Size: 400 B |
48
firmware/acid-firmware/ui/login-view.slint
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { ComboBox, LineEdit } from "std-widgets.slint";
|
||||||
|
import { Style } from "globals.slint";
|
||||||
|
import { IconButton } from "widgets/icon-button.slint";
|
||||||
|
|
||||||
|
export component LoginView inherits VerticalLayout {
|
||||||
|
padding: Style.spacing;
|
||||||
|
spacing: Style.spacing;
|
||||||
|
in property <[string]> usernames <=> combo_box_username.model;
|
||||||
|
callback users_clicked();
|
||||||
|
callback pw_accepted(int, string, string);
|
||||||
|
callback test_string_accepted <=> line_edit_test.accepted;
|
||||||
|
Rectangle { }
|
||||||
|
|
||||||
|
HorizontalLayout {
|
||||||
|
spacing: Style.spacing;
|
||||||
|
IconButton {
|
||||||
|
icon: @image-url("images/users.svg");
|
||||||
|
clicked => {
|
||||||
|
users_clicked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
combo_box_username := ComboBox { }
|
||||||
|
|
||||||
|
line_edit_user_pw := LineEdit {
|
||||||
|
input-type: InputType.password;
|
||||||
|
placeholder-text: "Password";
|
||||||
|
accepted(text) => {
|
||||||
|
root.pw_accepted(combo_box_username.current-index, combo_box_username.current-value, text);
|
||||||
|
line_edit_user_pw.text = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
icon: @image-url("images/log-in.svg");
|
||||||
|
clicked => {
|
||||||
|
root.pw_accepted(combo_box_username.current-index, combo_box_username.current-value, line_edit_user_pw.text);
|
||||||
|
line_edit_user_pw.text = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
line_edit_test := LineEdit {
|
||||||
|
placeholder-text: "Text to send to host.";
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle { }
|
||||||
|
}
|
||||||
106
firmware/acid-firmware/ui/main.slint
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
VerticalBox,
|
||||||
|
LineEdit,
|
||||||
|
GridBox,
|
||||||
|
StandardListView,
|
||||||
|
ListView,
|
||||||
|
ComboBox,
|
||||||
|
} from "std-widgets.slint";
|
||||||
|
|
||||||
|
// Implementations of standard widgets can be found at https://github.com/slint-ui/slint/tree/master/internal/compiler/widgets
|
||||||
|
|
||||||
|
// See https://github.com/slint-ui/slint/issues/4956 for issues with fonts.
|
||||||
|
import "../fonts/IBM_Plex_Mono/IBMPlexMono-Regular.ttf";
|
||||||
|
import { UserSitesView } from "user-sites-view.slint";
|
||||||
|
import { Style } from "globals.slint";
|
||||||
|
import { UsersView } from "users-view.slint";
|
||||||
|
import { LoginView } from "login-view.slint";
|
||||||
|
import { UserEditView } from "user-edit-view.slint";
|
||||||
|
|
||||||
|
export enum AppState {
|
||||||
|
login,
|
||||||
|
users,
|
||||||
|
user-edit,
|
||||||
|
user-sites,
|
||||||
|
}
|
||||||
|
|
||||||
|
export component AppWindow inherits Window {
|
||||||
|
// Special characters to generate pre-render glyphs for.
|
||||||
|
in property <string> dummy: "ÄÖÜÁÉÍÓÚÝŔŚĹŹĆŃĚĽŽŠČŘĎŤŇŮÅäöüáéíóúýŕśĺźćńěľžščřďťňůåß„“”‘’—–@&$%+=¡¿¢£$¥€²³¼½¬¤¦§©®™°´ˇ¨●";
|
||||||
|
in property <string> dummy_identicon_symbols: "╔╚╰═█░▒▓☺☻╗╝╯═◈◎◐◑◒◓☀☁☂☃☄★☆☎☏⎈⌂☘☢☣☕⌚⌛⏰⚡⛄⛅☔♔♕♖♗♘♙♚♛♜♝♞♟♨♩♪♫⚐⚑⚔⚖⚙⚠⌘⏎✄✆✈✉✌";
|
||||||
|
default-font-family: "IBM Plex Mono";
|
||||||
|
default-font-size: 16pt;
|
||||||
|
height: 8px + 240px + 8px;
|
||||||
|
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: 8px + 240px + 8px;
|
||||||
|
padding: 0px;
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
Rectangle {
|
||||||
|
height: 240px;
|
||||||
|
background: #00141d;
|
||||||
|
// For debugging bounds.
|
||||||
|
// border-color: #ffcf00;
|
||||||
|
// border-width: 1px;
|
||||||
|
login_view := LoginView {
|
||||||
|
visible: app-state == AppState.login;
|
||||||
|
users_clicked => {
|
||||||
|
enter-view(AppState.users);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
users_view := UsersView {
|
||||||
|
visible: app-state == AppState.users;
|
||||||
|
}
|
||||||
|
|
||||||
|
user_edit_view := UserEditView {
|
||||||
|
visible: app-state == AppState.user-edit;
|
||||||
|
cancel => {
|
||||||
|
root.escape();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user_sites_view := UserSitesView {
|
||||||
|
visible: app-state == AppState.user-sites;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
firmware/acid-firmware/ui/user-edit-view.slint
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { LineEdit, StandardListView, Button } from "std-widgets.slint";
|
||||||
|
import { Style } from "globals.slint";
|
||||||
|
import { IconButton } from "widgets/icon-button.slint";
|
||||||
|
|
||||||
|
export component UserEditView inherits HorizontalLayout {
|
||||||
|
padding: Style.spacing;
|
||||||
|
spacing: Style.spacing;
|
||||||
|
in property <string> username;
|
||||||
|
in property <string> key_error;
|
||||||
|
in-out property <string> identicon;
|
||||||
|
in-out property <string> key_id;
|
||||||
|
callback compute_identicon(password: string);
|
||||||
|
callback compute_key_id(encrypted_key: string);
|
||||||
|
callback confirm(encrypted_key: string);
|
||||||
|
callback cancel <=> button_cancel.clicked;
|
||||||
|
VerticalLayout {
|
||||||
|
spacing: Style.spacing;
|
||||||
|
Rectangle { }
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Enter " + username + "'s encrypted key and press Enter:";
|
||||||
|
}
|
||||||
|
|
||||||
|
line_edit_password := LineEdit {
|
||||||
|
input-type: InputType.password;
|
||||||
|
placeholder-text: "Password";
|
||||||
|
edited(text) => {
|
||||||
|
root.identicon = "";
|
||||||
|
root.key_id = "";
|
||||||
|
}
|
||||||
|
accepted(text) => {
|
||||||
|
compute_identicon(text);
|
||||||
|
line_edit_password.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: identicon.is-empty ? "" : ("Check the identicon: " + identicon);
|
||||||
|
}
|
||||||
|
|
||||||
|
line_edit_encrypted_key := LineEdit {
|
||||||
|
input-type: InputType.text;
|
||||||
|
placeholder-text: "Encrypted key";
|
||||||
|
enabled: !identicon.is-empty;
|
||||||
|
edited(text) => {
|
||||||
|
root.key_id = "";
|
||||||
|
}
|
||||||
|
accepted(text) => {
|
||||||
|
compute_key_id(text);
|
||||||
|
button_confirm.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: !key_error.is-empty ? key_error : (!key_id.is-empty ? ("Key ID: " + key_id) : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalLayout {
|
||||||
|
spacing: Style.spacing;
|
||||||
|
button_cancel := Button {
|
||||||
|
text: "Cancel";
|
||||||
|
}
|
||||||
|
|
||||||
|
button_confirm := Button {
|
||||||
|
text: "Confirm";
|
||||||
|
enabled: !identicon.is-empty && !key_id.is-empty;
|
||||||
|
clicked => {
|
||||||
|
confirm(line_edit_encrypted_key.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle { }
|
||||||
|
}
|
||||||
|
}
|
||||||
61
firmware/acid-firmware/ui/user-sites-view.slint
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { LineEdit, StandardListView, Button } from "std-widgets.slint";
|
||||||
|
import { Style } from "globals.slint";
|
||||||
|
import { IconButton } from "widgets/icon-button.slint";
|
||||||
|
|
||||||
|
export component UserSitesView inherits HorizontalLayout {
|
||||||
|
padding: Style.spacing;
|
||||||
|
spacing: Style.spacing;
|
||||||
|
in property <[StandardListViewItem]> model <=> list_view_sites.model;
|
||||||
|
in-out property <int> current-item <=> list_view_sites.current-item;
|
||||||
|
callback site_name_edited <=> line_edit_site_name.edited;
|
||||||
|
callback site_name_accepted(site_list_index: int);
|
||||||
|
public function site_name_clear() {
|
||||||
|
line_edit_site_name.text = "";
|
||||||
|
}
|
||||||
|
FocusScope {
|
||||||
|
key-pressed(event) => {
|
||||||
|
if event.text == "\n" {
|
||||||
|
site_name_accepted(list_view_sites.current-item);
|
||||||
|
EventResult.accept
|
||||||
|
} else {
|
||||||
|
EventResult.reject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VerticalLayout {
|
||||||
|
spacing: Style.spacing;
|
||||||
|
Text {
|
||||||
|
text: "Send password for:";
|
||||||
|
}
|
||||||
|
|
||||||
|
line_edit_site_name := LineEdit {
|
||||||
|
input-type: InputType.text;
|
||||||
|
placeholder-text: "example.org";
|
||||||
|
}
|
||||||
|
|
||||||
|
list_view_sites := StandardListView { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
spacing: Style.spacing;
|
||||||
|
// IconButton {
|
||||||
|
// icon: @image-url("images/log-out.svg");
|
||||||
|
// }
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
icon: @image-url("images/sliders.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
icon: @image-url("images/help-circle.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
icon: @image-url("images/key.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
icon: @image-url("images/trash-2.svg");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
firmware/acid-firmware/ui/users-view.slint
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { LineEdit, StandardListView, Button } from "std-widgets.slint";
|
||||||
|
import { Style } from "globals.slint";
|
||||||
|
import { IconButton } from "widgets/icon-button.slint";
|
||||||
|
|
||||||
|
export component UsersView inherits HorizontalLayout {
|
||||||
|
padding: Style.spacing;
|
||||||
|
spacing: Style.spacing;
|
||||||
|
callback edit_user(username: string, new: bool);
|
||||||
|
VerticalLayout {
|
||||||
|
spacing: Style.spacing * 2;
|
||||||
|
VerticalLayout {
|
||||||
|
spacing: Style.spacing;
|
||||||
|
Text {
|
||||||
|
text: "Add new user:";
|
||||||
|
}
|
||||||
|
|
||||||
|
line_edit_site_pw := LineEdit {
|
||||||
|
input-type: InputType.text;
|
||||||
|
placeholder-text: "Full Name";
|
||||||
|
accepted(text) => {
|
||||||
|
edit_user(text, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
spacing: Style.spacing;
|
||||||
|
Text {
|
||||||
|
text: "Edit existing user:";
|
||||||
|
}
|
||||||
|
|
||||||
|
FocusScope {
|
||||||
|
forward-focus: list_view_sites;
|
||||||
|
key-pressed(event) => {
|
||||||
|
if event.text == "\u{0D}" /* enter */ || event.text == " " /* space */ {
|
||||||
|
edit_user(list_view_sites.current-item, false);
|
||||||
|
EventResult.accept
|
||||||
|
} else {
|
||||||
|
EventResult.reject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list_view_sites := StandardListView { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
spacing: Style.spacing;
|
||||||
|
IconButton {
|
||||||
|
icon: @image-url("images/key.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
icon: @image-url("images/trash-2.svg");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
firmware/acid-firmware/ui/widgets/icon-button.slint
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { Button } from "std-widgets.slint";
|
||||||
|
|
||||||
|
export component IconButton inherits Button {
|
||||||
|
colorize-icon: true;
|
||||||
|
icon-size: 24px;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
@ -47,6 +47,36 @@
|
||||||
"name": "FOCUS_LCD",
|
"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": {
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
fn main() {
|
|
||||||
linker_be_nice();
|
|
||||||
// make sure linkall.x is the last linker script (otherwise might cause problems with flip-link)
|
|
||||||
println!("cargo:rustc-link-arg=-Tlinkall.x");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn linker_be_nice() {
|
|
||||||
let args: Vec<String> = std::env::args().collect();
|
|
||||||
if args.len() > 1 {
|
|
||||||
let kind = &args[1];
|
|
||||||
let what = &args[2];
|
|
||||||
|
|
||||||
match kind.as_str() {
|
|
||||||
"undefined-symbol" => match what.as_str() {
|
|
||||||
"_defmt_timestamp" => {
|
|
||||||
eprintln!();
|
|
||||||
eprintln!("💡 `defmt` not found - make sure `defmt.x` is added as a linker script and you have included `use defmt_rtt as _;`");
|
|
||||||
eprintln!();
|
|
||||||
}
|
|
||||||
"_stack_start" => {
|
|
||||||
eprintln!();
|
|
||||||
eprintln!("💡 Is the linker script `linkall.x` missing?");
|
|
||||||
eprintln!();
|
|
||||||
}
|
|
||||||
"esp_wifi_preempt_enable"
|
|
||||||
| "esp_wifi_preempt_yield_task"
|
|
||||||
| "esp_wifi_preempt_task_create" => {
|
|
||||||
eprintln!();
|
|
||||||
eprintln!("💡 `esp-wifi` has no scheduler enabled. Make sure you have the `builtin-scheduler` feature enabled, or that you provide an external scheduler.");
|
|
||||||
eprintln!();
|
|
||||||
}
|
|
||||||
"embedded_test_linker_file_not_added_to_rustflags" => {
|
|
||||||
eprintln!();
|
|
||||||
eprintln!("💡 `embedded-test` not found - make sure `embedded-test.x` is added as a linker script for tests");
|
|
||||||
eprintln!();
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
},
|
|
||||||
// we don't have anything helpful for "missing-lib" yet
|
|
||||||
_ => {
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"cargo:rustc-link-arg=-Wl,--error-handling-script={}",
|
|
||||||
std::env::current_exe().unwrap().display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
1
firmware/libsodium
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit d24faf56214469b354b01c8ba36257e04737101e
|
||||||
22
firmware/libsodium-compile.sh
Executable file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
if [ "$#" -lt 1 ]; then
|
||||||
|
echo "Usage: $0 <installation-directory-name>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||||
|
REPO_DIR="$SCRIPT_DIR/libsodium"
|
||||||
|
INSTALL_DIR_NAME="$REPO_DIR/$1"
|
||||||
|
|
||||||
|
pushd "$REPO_DIR" >/dev/null
|
||||||
|
env CC=xtensa-esp32s3-elf-gcc \
|
||||||
|
CFLAGS="-ffreestanding -fno-builtin -mlongcalls" \
|
||||||
|
LDFLAGS="-nostdlib -static" \
|
||||||
|
./configure \
|
||||||
|
--host xtensa-esp32s3 \
|
||||||
|
--disable-shared \
|
||||||
|
--enable-static \
|
||||||
|
--prefix="$INSTALL_DIR_NAME"
|
||||||
|
make -j
|
||||||
|
make install
|
||||||
|
popd >/dev/null
|
||||||
6
firmware2/libxkbcommon-compile.sh → firmware/libxkbcommon-compile.sh
Normal file → Executable file
|
|
@ -10,10 +10,8 @@ BUILD_DIR_NAME="$LIBXKBCOMMON_DIR/$1"
|
||||||
STATIC_LIB_PATH="$BUILD_DIR_NAME/libxkbcommon.a"
|
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'
|
||||||
2
firmware/password-hash/.cargo/config.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
[env] # These must be kept in sync with /.zed/settings.json
|
||||||
|
LIBSODIUM_INSTALL_DIR = "../libsodium/install-host"
|
||||||
27
firmware/password-hash/Cargo.toml
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
[package]
|
||||||
|
name = "password-hash"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "password_hash"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "password_hash"
|
||||||
|
path = "src/bin.rs"
|
||||||
|
required-features = ["cmd"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["cmd"]
|
||||||
|
std = []
|
||||||
|
cmd = ["std", "dep:scrypt", "dep:itertools", "dep:spectre-api-sys", "dep:byteorder"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
argon2 = { version = "0.5.3", default-features = false, features = ["alloc"] }
|
||||||
|
itertools = { version = "0.14.0", optional = true }
|
||||||
|
scrypt = { version = "0.11.0", default-features = false, optional = true }
|
||||||
|
spectre-api-sys = { workspace = true, optional = true }
|
||||||
|
byteorder = { version = "1.5.0", optional = true }
|
||||||
|
# TODO: Why is this unable to provide the symbols for spectre-api-sys?
|
||||||
|
# libsodium-sys-stable = { version = "1.23.2", optional = true }
|
||||||
17
firmware/password-hash/README.md
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Compiling
|
||||||
|
|
||||||
|
Compile on Linux or in WSL.
|
||||||
|
|
||||||
|
Compile libsodium for the host:
|
||||||
|
```bash
|
||||||
|
cd ../libsodium
|
||||||
|
./configure --disable-shared --enable-static --prefix="$PWD/install-host"
|
||||||
|
make -j
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
|
Then compile and run password-hash:
|
||||||
|
```bash
|
||||||
|
cd ../password-hash
|
||||||
|
cargo run --release [--target x86_64-unknown-linux-gnu]
|
||||||
|
```
|
||||||
35
firmware/password-hash/build.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
fn main() {
|
||||||
|
#[cfg(feature = "cmd")]
|
||||||
|
{
|
||||||
|
use std::{env, path::PathBuf};
|
||||||
|
|
||||||
|
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||||
|
println!(
|
||||||
|
"cargo:rustc-link-search=native={}",
|
||||||
|
manifest_dir.join("../spectre-api-c/build-host").display()
|
||||||
|
);
|
||||||
|
println!("cargo:rustc-link-lib=static=spectre");
|
||||||
|
println!(
|
||||||
|
"cargo:rerun-if-changed={}",
|
||||||
|
manifest_dir
|
||||||
|
.join("../spectre-api-c/build-host/libspectre.a")
|
||||||
|
.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Ok(libsodium_install_dir) = env::var("LIBSODIUM_INSTALL_DIR") {
|
||||||
|
let libsodium_install_dir =
|
||||||
|
PathBuf::from(libsodium_install_dir).canonicalize().unwrap();
|
||||||
|
println!(
|
||||||
|
"cargo:rustc-link-search=native={}",
|
||||||
|
libsodium_install_dir.join("lib").display()
|
||||||
|
);
|
||||||
|
println!("cargo:rustc-link-lib=static=sodium");
|
||||||
|
println!(
|
||||||
|
"cargo:rerun-if-changed={}",
|
||||||
|
libsodium_install_dir.join("lib/libsodium.a").display()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!("Environment variable `LIBSODIUM_INSTALL_DIR` missing!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
88
firmware/password-hash/src/bin.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
#![feature(allocator_api)]
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
alloc::Global,
|
||||||
|
ffi::{CStr, CString},
|
||||||
|
io::{Cursor, Write, stdin},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
use byteorder::{BigEndian, WriteBytesExt};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use password_hash::{Key, derive_encryption_key, encrypt_with};
|
||||||
|
use spectre_api_sys::SpectreAlgorithm;
|
||||||
|
|
||||||
|
macro_rules! read_line {
|
||||||
|
($lines:expr, $label:expr) => {{
|
||||||
|
print!("{}: ", $label);
|
||||||
|
std::io::stdout().flush().unwrap();
|
||||||
|
CString::from_str(
|
||||||
|
&$lines
|
||||||
|
.next()
|
||||||
|
.transpose()
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_else(|| panic!("Expected {}.", $label)),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let print_secrets = std::env::args().contains("--print-secrets");
|
||||||
|
|
||||||
|
let mut lines = stdin().lines();
|
||||||
|
let username = read_line!(lines, "username");
|
||||||
|
let secret = read_line!(lines, "secret");
|
||||||
|
|
||||||
|
let user_key: Key = unsafe {
|
||||||
|
&*spectre_api_sys::spectre_user_key(
|
||||||
|
username.as_ptr(),
|
||||||
|
secret.as_ptr(),
|
||||||
|
SpectreAlgorithm::Current,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.bytes;
|
||||||
|
let user_key_id = unsafe { spectre_api_sys::spectre_id_buf(user_key.as_ptr(), user_key.len()) };
|
||||||
|
let salt = derive_user_key_salt(&username);
|
||||||
|
let encryption_key = derive_encryption_key(&salt, secret.as_bytes(), Global);
|
||||||
|
|
||||||
|
if print_secrets {
|
||||||
|
println!("\nUser Key (SECRET):\n{:02x}", user_key.iter().format(""));
|
||||||
|
println!(
|
||||||
|
"\nEncryption Key (SECRET):\n{:02x}",
|
||||||
|
encryption_key.iter().format("")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut encrypted_user_key = user_key;
|
||||||
|
|
||||||
|
encrypt_with(&mut encrypted_user_key, &encryption_key);
|
||||||
|
println!(
|
||||||
|
"\nUser Key ID:\n{:02x}",
|
||||||
|
user_key_id.bytes.iter().format("")
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"\nEncrypted User Key:\n{:02x}",
|
||||||
|
encrypted_user_key.iter().format("")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copied from the body of the `spectre_user_key_v3` function.
|
||||||
|
fn derive_user_key_salt(username: &CStr) -> Vec<u8> {
|
||||||
|
use spectre_api_sys::*;
|
||||||
|
|
||||||
|
let mut salt = Cursor::new(Vec::new());
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let key_scope: &'static CStr =
|
||||||
|
CStr::from_ptr(spectre_purpose_scope(SpectreKeyPurpose::Authentication));
|
||||||
|
|
||||||
|
salt.write_all(key_scope.to_bytes()).unwrap();
|
||||||
|
salt.write_u32::<BigEndian>(username.count_bytes() as u32)
|
||||||
|
.unwrap();
|
||||||
|
salt.write_all(username.to_bytes()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
salt.into_inner()
|
||||||
|
}
|
||||||
51
firmware/password-hash/src/blocks.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
use core::slice;
|
||||||
|
|
||||||
|
use argon2::Block;
|
||||||
|
|
||||||
|
pub(crate) struct Argon2Blocks<A: alloc::alloc::Allocator> {
|
||||||
|
p: core::ptr::NonNull<Block>,
|
||||||
|
len: usize,
|
||||||
|
alloc: A,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: alloc::alloc::Allocator> Argon2Blocks<A> {
|
||||||
|
/// Each block is 1 KiB.
|
||||||
|
/// Total size is `len` * 1 KiB
|
||||||
|
pub fn new_in(len: usize, alloc: A) -> Option<Self> {
|
||||||
|
use alloc::alloc::Layout;
|
||||||
|
|
||||||
|
if len == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout = Layout::array::<Block>(len).ok()?;
|
||||||
|
// SAFETY: `alloc_zeroed` is used correctly with non-zero layout
|
||||||
|
let p = alloc.allocate_zeroed(layout).ok()?.cast();
|
||||||
|
|
||||||
|
Some(Self { p, len, alloc })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: alloc::alloc::Allocator> AsMut<[Block]> for Argon2Blocks<A> {
|
||||||
|
fn as_mut(&mut self) -> &mut [Block] {
|
||||||
|
unsafe { slice::from_raw_parts_mut(self.p.as_ptr(), self.len) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: alloc::alloc::Allocator> AsRef<[Block]> for Argon2Blocks<A> {
|
||||||
|
fn as_ref(&self) -> &[Block] {
|
||||||
|
unsafe { slice::from_raw_parts(self.p.as_ptr(), self.len) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: alloc::alloc::Allocator> Drop for Argon2Blocks<A> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
use alloc::alloc::Layout;
|
||||||
|
// SAFETY: layout was checked during construction
|
||||||
|
let layout = unsafe { Layout::array::<Block>(self.len).unwrap_unchecked() };
|
||||||
|
// SAFETY: we use `dealloc` correctly with the previously allocated pointer
|
||||||
|
unsafe {
|
||||||
|
self.alloc.deallocate(self.p.cast(), layout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
firmware/password-hash/src/lib.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
//! scrypt memory requirements scale linearly with parameters `N` and `r`.
|
||||||
|
//! This makes it unsuitable for embedded environments with the parameters
|
||||||
|
//! used in Spectre.
|
||||||
|
//! Our work-around is to derive the Spectre _user key_ using scrypt on the
|
||||||
|
//! host, encrypt it with XOR using a key derived using argon2, which
|
||||||
|
//! has parameters for specifying the memory and time requirements separately.
|
||||||
|
//! This encrypted key is then stored on the keyboard, to be decrypted again.
|
||||||
|
#![cfg_attr(not(feature = "std"), no_std)]
|
||||||
|
#![feature(allocator_api)]
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
use alloc::alloc::Allocator;
|
||||||
|
use alloc::vec::Vec;
|
||||||
|
use argon2::{Algorithm, Argon2, ParamsBuilder, Version};
|
||||||
|
|
||||||
|
use crate::blocks::Argon2Blocks;
|
||||||
|
|
||||||
|
pub mod blocks;
|
||||||
|
|
||||||
|
pub type Key = [u8; 64];
|
||||||
|
|
||||||
|
/// KiB used by the argon2 algorithm.
|
||||||
|
/// Lower than the default, to fit in the constrained memory of embedded devices.
|
||||||
|
pub const ARGON2_M_COST: u32 = 1024;
|
||||||
|
/// Compensate the difficulty by increasing the iterations proportionally.
|
||||||
|
pub const ARGON2_T_COST: u32 =
|
||||||
|
argon2::Params::DEFAULT_T_COST * argon2::Params::DEFAULT_M_COST / ARGON2_M_COST;
|
||||||
|
pub const ARGON2_P_COST: u32 = argon2::Params::DEFAULT_P_COST;
|
||||||
|
pub const ARGON2_SALT_PREFIX: &[u8] = b"acid-firmware\0";
|
||||||
|
|
||||||
|
pub fn derive_encryption_key(
|
||||||
|
unprefixed_salt: &[u8],
|
||||||
|
secret: &[u8],
|
||||||
|
allocator: impl Allocator,
|
||||||
|
) -> Key {
|
||||||
|
let argon2 = Argon2::new(
|
||||||
|
Algorithm::Argon2id,
|
||||||
|
Version::default(),
|
||||||
|
ParamsBuilder::default()
|
||||||
|
.m_cost(ARGON2_M_COST)
|
||||||
|
.t_cost(ARGON2_T_COST)
|
||||||
|
.p_cost(ARGON2_P_COST)
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
let mut blocks = Argon2Blocks::new_in(ARGON2_M_COST as usize, &allocator).unwrap();
|
||||||
|
let mut key: Key = [0u8; _];
|
||||||
|
// Salt is prefixed to form a salt that is long enough for Argon2.
|
||||||
|
let mut salt =
|
||||||
|
Vec::with_capacity_in(unprefixed_salt.len() + ARGON2_SALT_PREFIX.len(), &allocator);
|
||||||
|
salt.extend_from_slice(ARGON2_SALT_PREFIX);
|
||||||
|
salt.extend_from_slice(unprefixed_salt);
|
||||||
|
argon2
|
||||||
|
.hash_password_into_with_memory(secret, &salt, &mut key, &mut blocks)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
key
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt_with(data: &mut Key, key: &Key) {
|
||||||
|
for (dst_byte, user_byte) in core::iter::zip(data.iter_mut(), key.iter()) {
|
||||||
|
*dst_byte ^= *user_byte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt_with(data: &mut Key, key: &Key) {
|
||||||
|
decrypt_with(data, key);
|
||||||
|
}
|
||||||
10
firmware2/libxkbcommon-redefine-syms.sh → firmware/redefine-syms.sh
Normal file → Executable file
|
|
@ -1,10 +1,10 @@
|
||||||
#!/usr/bin/env bash
|
#!/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
|
||||||
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
[toolchain]
|
|
||||||
channel = "esp"
|
|
||||||
1
firmware/spectre-api-c
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 733e6d6d73bc9462d8ec9528dd96eee23846a3e3
|
||||||