This commit is contained in:
Pat Nakajima 2026-07-08 16:47:24 -07:00
parent 6c3da38fb9
commit 37ba2e0e6f
25 changed files with 1043 additions and 1982 deletions

View File

@ -1,6 +1,7 @@
[target.riscv32imac-unknown-none-elf] [target.riscv32imac-unknown-none-elf]
# runner = "probe-rs run --chip=esp32c6 --always-print-stacktrace --no-location --catch-hardfault" # runner = "probe-rs run --chip=esp32c6 --always-print-stacktrace --no-location --catch-hardfault"
runner = "espflash flash --monitor -L defmt --no-skip --chip esp32c6 --partition-table partitions.csv" # runner = "espflash flash --monitor -L defmt --no-skip --chip esp32c6 --partition-table partitions.csv"
runner = "espflash flash --monitor -L defmt --no-skip --chip esp32c6 --partition-table partitions.csv --partition-table-offset 0x8000"
rustflags = ["-C", "force-frame-pointers"] rustflags = ["-C", "force-frame-pointers"]
[env] [env]

494
Cargo.lock generated
View File

@ -2,15 +2,6 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "aead"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.4" version = "1.1.4"
@ -85,36 +76,6 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "bt-hci"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "211713d2e9fb4793ce4360a712c0764264aff6be48932ccf02ca2a331c0436a9"
dependencies = [
"btuuid",
"defmt 1.0.1",
"embassy-sync 0.8.0",
"embedded-io 0.7.1",
"embedded-io-async 0.7.0",
"futures-intrusive",
"heapless 0.9.3",
]
[[package]]
name = "btuuid"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5f48f1e9b0aad0a4f05d17bdeae0fa20ff798e272a03a6940ca27ad9c5a6ae7"
dependencies = [
"defmt 0.3.100",
]
[[package]]
name = "byte"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21c7ab3e4ae80853c7f8dcdcd904dfa25c02cc373534b8d165194325a088a7cc"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.25.0" version = "1.25.0"
@ -137,33 +98,12 @@ dependencies = [
"shlex", "shlex",
] ]
[[package]]
name = "ccm"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a9cf981c7e62b6fb02225592ee7ebf221e0b0b5317984a57a1e9d21af20e317"
dependencies = [
"aead",
"cipher",
"ctr",
"subtle",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cipher"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "const-default" name = "const-default"
version = "1.0.0" version = "1.0.0"
@ -196,15 +136,6 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "ctr"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea"
dependencies = [
"cipher",
]
[[package]] [[package]]
name = "darling" name = "darling"
version = "0.20.11" version = "0.20.11"
@ -370,26 +301,6 @@ dependencies = [
"crypto-common", "crypto-common",
] ]
[[package]]
name = "docsplay"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8547ea80db62c5bb9d7796fcce5e6e07d1136bdc1a02269095061e806758fab4"
dependencies = [
"docsplay-macros",
]
[[package]]
name = "docsplay-macros"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11772ed3eb3db124d826f3abeadf5a791a557f62c19b123e3f07288158a71fdd"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]] [[package]]
name = "document-features" name = "document-features"
version = "0.2.11" version = "0.2.11"
@ -466,29 +377,6 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "embassy-net"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed041cc19a603d657124fddefdcbe5ef8bd60e77d972793ebb57de93394f5949"
dependencies = [
"document-features",
"embassy-net-driver",
"embassy-sync 0.6.2",
"embassy-time 0.4.0",
"embedded-io-async 0.6.1",
"embedded-nal-async",
"heapless 0.8.0",
"managed",
"smoltcp",
]
[[package]]
name = "embassy-net-driver"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524eb3c489760508f71360112bca70f6e53173e6fe48fc5f0efd0f5ab217751d"
[[package]] [[package]]
name = "embassy-sync" name = "embassy-sync"
version = "0.6.2" version = "0.6.2"
@ -532,22 +420,6 @@ dependencies = [
"heapless 0.9.3", "heapless 0.9.3",
] ]
[[package]]
name = "embassy-time"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f820157f198ada183ad62e0a66f554c610cdcd1a9f27d4b316358103ced7a1f8"
dependencies = [
"cfg-if",
"critical-section",
"document-features",
"embassy-time-driver",
"embedded-hal 0.2.7",
"embedded-hal 1.0.0",
"embedded-hal-async",
"futures-util",
]
[[package]] [[package]]
name = "embassy-time" name = "embassy-time"
version = "0.5.1" version = "0.5.1"
@ -684,25 +556,6 @@ dependencies = [
"embedded-io 0.7.1", "embedded-io 0.7.1",
] ]
[[package]]
name = "embedded-nal"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c56a28be191a992f28f178ec338a0bf02f63d7803244add736d026a471e6ed77"
dependencies = [
"nb 1.1.0",
]
[[package]]
name = "embedded-nal-async"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76959917cd2b86f40a98c28dd5624eddd1fa69d746241c8257eac428d83cb211"
dependencies = [
"embedded-io-async 0.6.1",
"embedded-nal",
]
[[package]] [[package]]
name = "embedded-storage" name = "embedded-storage"
version = "0.3.1" version = "0.3.1"
@ -757,7 +610,7 @@ dependencies = [
"document-features", "document-features",
"enumset", "enumset",
"esp-config", "esp-config",
"esp-sync 0.2.1", "esp-sync",
"linked_list_allocator", "linked_list_allocator",
"rlsf", "rlsf",
] ]
@ -772,7 +625,7 @@ dependencies = [
"defmt 1.0.1", "defmt 1.0.1",
"document-features", "document-features",
"esp-config", "esp-config",
"esp-metadata-generated 0.4.0", "esp-metadata-generated",
"esp-println", "esp-println",
"heapless 0.9.3", "heapless 0.9.3",
"riscv", "riscv",
@ -789,8 +642,8 @@ dependencies = [
"document-features", "document-features",
"embedded-storage", "embedded-storage",
"esp-config", "esp-config",
"esp-hal-procmacros 0.22.0", "esp-hal-procmacros",
"esp-metadata-generated 0.4.0", "esp-metadata-generated",
"esp-rom-sys", "esp-rom-sys",
"jiff", "jiff",
"strum", "strum",
@ -803,7 +656,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d9b92fd9cfb0b4f8f1b6219b9763269a335571e307b014903b8201619374b80" checksum = "4d9b92fd9cfb0b4f8f1b6219b9763269a335571e307b014903b8201619374b80"
dependencies = [ dependencies = [
"document-features", "document-features",
"esp-metadata-generated 0.4.0", "esp-metadata-generated",
"serde", "serde",
"serde_yaml", "serde_yaml",
"somni-expr", "somni-expr",
@ -836,11 +689,11 @@ dependencies = [
"embedded-io-async 0.7.0", "embedded-io-async 0.7.0",
"enumset", "enumset",
"esp-config", "esp-config",
"esp-hal-procmacros 0.22.0", "esp-hal-procmacros",
"esp-metadata-generated 0.4.0", "esp-metadata-generated",
"esp-riscv-rt", "esp-riscv-rt",
"esp-rom-sys", "esp-rom-sys",
"esp-sync 0.2.1", "esp-sync",
"esp32", "esp32",
"esp32c2", "esp32c2",
"esp32c3", "esp32c3",
@ -865,20 +718,6 @@ dependencies = [
"xtensa-lx-rt", "xtensa-lx-rt",
] ]
[[package]]
name = "esp-hal-procmacros"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e025a7a7a0affdb4ff913b5c4494aef96ee03d085bf83c27453ae3a71d50da6"
dependencies = [
"document-features",
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.101",
"termcolor",
]
[[package]] [[package]]
name = "esp-hal-procmacros" name = "esp-hal-procmacros"
version = "0.22.0" version = "0.22.0"
@ -894,36 +733,12 @@ dependencies = [
"termcolor", "termcolor",
] ]
[[package]]
name = "esp-metadata-generated"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a93e39c8ad8d390d248dc7b9f4b59a873f313bf535218b8e2351356972399e3"
[[package]] [[package]]
name = "esp-metadata-generated" name = "esp-metadata-generated"
version = "0.4.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42c2ee95b945a4780796e4359e72c033aed3b45073880e8029458f538532db8a" checksum = "42c2ee95b945a4780796e4359e72c033aed3b45073880e8029458f538532db8a"
[[package]]
name = "esp-phy"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c0a29815cd105ae1a02f3d0c6e7aafda9504a41effae17fac4c3f827719228"
dependencies = [
"cfg-if",
"defmt 1.0.1",
"document-features",
"embassy-sync 0.8.0",
"esp-config",
"esp-hal",
"esp-metadata-generated 0.4.0",
"esp-sync 0.2.1",
"esp-wifi-sys-esp32c6",
"esp32c6",
]
[[package]] [[package]]
name = "esp-println" name = "esp-println"
version = "0.17.0" version = "0.17.0"
@ -932,64 +747,12 @@ checksum = "42dee1e9ac7c3539bf6464db1707b0edd7557168f98278cf3c84fe70e63c6ce6"
dependencies = [ dependencies = [
"defmt 1.0.1", "defmt 1.0.1",
"document-features", "document-features",
"esp-metadata-generated 0.4.0", "esp-metadata-generated",
"esp-sync 0.2.1", "esp-sync",
"log", "log",
"portable-atomic", "portable-atomic",
] ]
[[package]]
name = "esp-radio"
version = "1.0.0-beta.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f25cc4e3ce27476b42c4a68943f10a92f9dec3c24bb001269958f0318fef02c"
dependencies = [
"allocator-api2",
"bt-hci",
"cfg-if",
"defmt 1.0.1",
"docsplay",
"document-features",
"embassy-net-driver",
"embassy-sync 0.8.0",
"enumset",
"esp-alloc",
"esp-config",
"esp-hal",
"esp-hal-procmacros 0.22.0",
"esp-metadata-generated 0.4.0",
"esp-phy",
"esp-radio-rtos-driver",
"esp-rom-sys",
"esp-sync 0.2.1",
"esp-wifi-sys-esp32",
"esp-wifi-sys-esp32c2",
"esp-wifi-sys-esp32c3",
"esp-wifi-sys-esp32c6",
"esp-wifi-sys-esp32h2",
"esp-wifi-sys-esp32s2",
"esp-wifi-sys-esp32s3",
"esp32c6",
"heapless 0.9.3",
"ieee802154",
"instability",
"num-derive",
"num-traits",
"portable-atomic",
"portable_atomic_enum",
]
[[package]]
name = "esp-radio-rtos-driver"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bd75cd9073a90ffaa53db0bf17df7dc14164f2407a6ff36c725d2d1f78ff494"
dependencies = [
"cfg-if",
"esp-sync 0.2.1",
"portable-atomic",
]
[[package]] [[package]]
name = "esp-riscv-rt" name = "esp-riscv-rt"
version = "0.14.0" version = "0.14.0"
@ -1010,7 +773,7 @@ checksum = "eae852ccb08971155023d1371c96d5490cbc26860f06aee2d629ef73f1a890c3"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"document-features", "document-features",
"esp-metadata-generated 0.4.0", "esp-metadata-generated",
"esp32c6", "esp32c6",
] ]
@ -1020,7 +783,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "551f90766e1527edaa0c91e8d559e9e2a60397b545e93357ac61fb31845e5712" checksum = "551f90766e1527edaa0c91e8d559e9e2a60397b545e93357ac61fb31845e5712"
dependencies = [ dependencies = [
"allocator-api2",
"cfg-if", "cfg-if",
"defmt 1.0.1", "defmt 1.0.1",
"document-features", "document-features",
@ -1030,47 +792,15 @@ dependencies = [
"embassy-time-queue-utils", "embassy-time-queue-utils",
"esp-config", "esp-config",
"esp-hal", "esp-hal",
"esp-hal-procmacros 0.22.0", "esp-hal-procmacros",
"esp-metadata-generated 0.4.0", "esp-metadata-generated",
"esp-radio-rtos-driver",
"esp-rom-sys", "esp-rom-sys",
"esp-sync 0.2.1", "esp-sync",
"portable-atomic", "portable-atomic",
"riscv", "riscv",
"xtensa-lx", "xtensa-lx",
] ]
[[package]]
name = "esp-storage"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1495fc1f5549bdd840b52d9ceb201746200e1620d2636f46958c11e765623b80"
dependencies = [
"defmt 1.0.1",
"document-features",
"embedded-storage",
"esp-hal",
"esp-hal-procmacros 0.21.0",
"esp-metadata-generated 0.3.0",
"esp-rom-sys",
"esp-sync 0.1.1",
]
[[package]]
name = "esp-sync"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d44974639b4e88914f83fe60d2832c00276657d7d857628fdfc966cc7302e8a8"
dependencies = [
"cfg-if",
"document-features",
"embassy-sync 0.6.2",
"embassy-sync 0.7.2",
"esp-metadata-generated 0.3.0",
"riscv",
"xtensa-lx",
]
[[package]] [[package]]
name = "esp-sync" name = "esp-sync"
version = "0.2.1" version = "0.2.1"
@ -1083,74 +813,11 @@ dependencies = [
"embassy-sync 0.6.2", "embassy-sync 0.6.2",
"embassy-sync 0.7.2", "embassy-sync 0.7.2",
"embassy-sync 0.8.0", "embassy-sync 0.8.0",
"esp-metadata-generated 0.4.0", "esp-metadata-generated",
"riscv", "riscv",
"xtensa-lx", "xtensa-lx",
] ]
[[package]]
name = "esp-wifi-sys-esp32"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2556f38f5292d9735d4e156e276815fc001c9a0a2be0544a575c5fb867129d24"
dependencies = [
"defmt 1.0.1",
]
[[package]]
name = "esp-wifi-sys-esp32c2"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4b3eb3435dae84de611d4384639e61eed862818223e93fa70c525cb6a70127f"
dependencies = [
"defmt 1.0.1",
]
[[package]]
name = "esp-wifi-sys-esp32c3"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b290846b53db9a3965866964260220b67f2c41cc2dbc0b377e7136239fe168a7"
dependencies = [
"defmt 1.0.1",
]
[[package]]
name = "esp-wifi-sys-esp32c6"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57649401fc2f906a16e2268de88693724a125adcd0eba89b594a157affcee2d5"
dependencies = [
"defmt 1.0.1",
]
[[package]]
name = "esp-wifi-sys-esp32h2"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cf1be2e311f4b4e75b5507c6b007ffc3fcb8c6cb57f83cc6a9569ecdcf58484"
dependencies = [
"defmt 1.0.1",
]
[[package]]
name = "esp-wifi-sys-esp32s2"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a9a7f1bc51e7026c3012cda4f11d8799e5e0c0bb3be85797462219def6c26e"
dependencies = [
"defmt 1.0.1",
]
[[package]]
name = "esp-wifi-sys-esp32s3"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d8321f44b57d8112cbd8607cc83b85783b9195289acb45532728e3e229e7786"
dependencies = [
"defmt 1.0.1",
]
[[package]] [[package]]
name = "esp32" name = "esp32"
version = "0.40.2" version = "0.40.2"
@ -1278,16 +945,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-intrusive"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
dependencies = [
"futures-core",
"lock_api",
]
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.31" version = "0.3.31"
@ -1343,15 +1000,6 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "hash32"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
dependencies = [
"byteorder",
]
[[package]] [[package]]
name = "hash32" name = "hash32"
version = "0.3.1" version = "0.3.1"
@ -1361,17 +1009,6 @@ dependencies = [
"byteorder", "byteorder",
] ]
[[package]]
name = "hash32-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59d2aba832b60be25c1b169146b27c64115470981b128ed84c8db18c1b03c6ff"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.17.1" version = "0.17.1"
@ -1384,7 +1021,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
dependencies = [ dependencies = [
"hash32 0.3.1", "hash32",
"stable_deref_trait", "stable_deref_trait",
] ]
@ -1394,8 +1031,7 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4"
dependencies = [ dependencies = [
"defmt 1.0.1", "hash32",
"hash32 0.3.1",
"stable_deref_trait", "stable_deref_trait",
] ]
@ -1411,20 +1047,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "ieee802154"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fcb6de62f20180795db19ae2ab338852a66f8576581554fa8a730e437b450a5"
dependencies = [
"byte",
"ccm",
"cipher",
"defmt 0.3.100",
"hash32 0.2.1",
"hash32-derive",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.14.0" version = "2.14.0"
@ -1491,25 +1113,17 @@ dependencies = [
"critical-section", "critical-section",
"defmt 1.0.1", "defmt 1.0.1",
"embassy-executor", "embassy-executor",
"embassy-futures", "embassy-time",
"embassy-net",
"embassy-time 0.5.1",
"embedded-graphics", "embedded-graphics",
"embedded-hal 1.0.0", "embedded-hal 1.0.0",
"embedded-io 0.6.1",
"embedded-io-async 0.6.1",
"embedded-storage",
"esp-alloc", "esp-alloc",
"esp-backtrace", "esp-backtrace",
"esp-bootloader-esp-idf", "esp-bootloader-esp-idf",
"esp-hal", "esp-hal",
"esp-println", "esp-println",
"esp-radio",
"esp-rtos", "esp-rtos",
"esp-storage",
"heapless 0.8.0", "heapless 0.8.0",
"lm3630a", "lm3630a",
"smoltcp",
"ssd1677", "ssd1677",
"static_cell", "static_cell",
] ]
@ -1548,15 +1162,6 @@ dependencies = [
"embedded-hal-async", "embedded-hal-async",
] ]
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.27" version = "0.4.27"
@ -1576,12 +1181,6 @@ dependencies = [
"tracing-subscriber", "tracing-subscriber",
] ]
[[package]]
name = "managed"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d"
[[package]] [[package]]
name = "matchers" name = "matchers"
version = "0.2.0" version = "0.2.0"
@ -1627,17 +1226,6 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "num-derive"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@ -1695,27 +1283,6 @@ dependencies = [
"portable-atomic", "portable-atomic",
] ]
[[package]]
name = "portable_atomic_enum"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d48f60c43e0120bb2bb48589a16d4bed2f4b911be41e299f2d0fc0e0e20885"
dependencies = [
"portable-atomic",
"portable_atomic_enum_macros",
]
[[package]]
name = "portable_atomic_enum_macros"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a33fa6ec7f2047f572d49317cca19c87195de99c6e5b6ee492da701cfe02b053"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]] [[package]]
name = "proc-macro-crate" name = "proc-macro-crate"
version = "3.5.0" version = "3.5.0"
@ -1891,12 +1458,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.228"
@ -1961,19 +1522,6 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "smoltcp"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dad095989c1533c1c266d9b1e8d70a1329dd3723c3edac6d03bbd67e7bf6f4bb"
dependencies = [
"bitflags 1.3.2",
"byteorder",
"cfg-if",
"heapless 0.8.0",
"managed",
]
[[package]] [[package]]
name = "somni-expr" name = "somni-expr"
version = "0.2.0" version = "0.2.0"
@ -2041,12 +1589,6 @@ dependencies = [
"syn 2.0.101", "syn 2.0.101",
] ]
[[package]]
name = "subtle"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]] [[package]]
name = "svgbobdoc" name = "svgbobdoc"
version = "0.3.0" version = "0.3.0"

View File

@ -25,45 +25,18 @@ heapless = { version = "0.8.0", default-features = false }
embedded-graphics = { version = "0.8.1", features = ["defmt"] } embedded-graphics = { version = "0.8.1", features = ["defmt"] }
ssd1677 = { path = "../ssd1677" } ssd1677 = { path = "../ssd1677" }
defmt = "1.0.1" defmt = "1.0.1"
embassy-net = { version = "0.6.0", features = [
"dhcpv4",
"medium-ethernet",
"tcp",
"udp",
] }
embedded-io = "0.6.1"
embedded-io-async = "0.6.1"
esp-alloc = "0.10.0" esp-alloc = "0.10.0"
esp-hal = { version = "1.1.1", features = ["defmt", "esp32c6", "unstable"] } esp-hal = { version = "1.1.1", features = ["defmt", "esp32c6", "unstable"] }
smoltcp = { version = "0.12.0", default-features = false, features = [
"proto-dhcpv4",
"proto-dns",
"proto-ipv4",
"socket-dns",
"socket-icmp",
"socket-raw",
"socket-tcp",
"socket-udp",
] }
# for more networking protocol support see https://crates.io/crates/edge-net
critical-section = "1.2.0" critical-section = "1.2.0"
embassy-executor = { version = "0.10.0", features = ["defmt", "nightly"] } embassy-executor = { version = "0.10.0", features = ["defmt", "nightly"] }
embassy-time = { version = "0.5.0", features = ["generic-queue-8"] } embassy-time = { version = "0.5.0", features = ["generic-queue-8"] }
esp-rtos = { version = "0.3.0", features = [ esp-rtos = { version = "0.3.0", features = [
"embassy", "embassy",
"defmt", "defmt",
"esp-radio",
"esp32c6", "esp32c6",
] } ] }
esp-radio = { version = "1.0.0-beta.0", features = [
"defmt",
"esp32c6",
"wifi",
] }
static_cell = { version = "2.1.0", features = ["nightly"] } static_cell = { version = "2.1.0", features = ["nightly"] }
embedded-hal = { version = "1.0.0", features = ["defmt-03"] } embedded-hal = { version = "1.0.0", features = ["defmt-03"] }
embedded-storage = "0.3.1"
esp-storage = { version = "0.8.1", features = ["defmt", "esp32c6"] }
esp-println = { version = "0.17.0", features = [ esp-println = { version = "0.17.0", features = [
"defmt-espflash", "defmt-espflash",
"esp32c6", "esp32c6",
@ -75,7 +48,6 @@ esp-backtrace = { version = "0.19.0", features = [
"defmt", "defmt",
] } ] }
esp-bootloader-esp-idf = { version = "0.5.0", features = ["esp32c6"] } esp-bootloader-esp-idf = { version = "0.5.0", features = ["esp32c6"] }
embassy-futures = { version = "0.1.1", features = ["defmt"] }
lm3630a = { version = "0.1.1", features = ["async"] } lm3630a = { version = "0.1.1", features = ["async"] }
[profile.dev] [profile.dev]
@ -83,9 +55,6 @@ lm3630a = { version = "0.1.1", features = ["async"] }
# For debug builds always builds with some optimization # For debug builds always builds with some optimization
opt-level = "s" opt-level = "s"
[profile.dev.package.esp-radio]
opt-level = 3
[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

175
docs/power.md Normal file
View File

@ -0,0 +1,175 @@
# Power Usage and Power Saving Plan
## Status
Power has not been measured on hardware yet. This document describes expected power behavior based on the current firmware and the display/touch datasheets.
## Current power behavior
The current app is optimized for bring-up and UI demo behavior, not battery life.
Current firmware behavior:
- CPU runs at max clock (`CpuClock::max()`, 160 MHz on ESP32-C6).
- Main loop wakes every 25 ms.
- FT6336U touch is polled over I2C every loop.
- Front light starts enabled.
- Front light off currently toggles GPIO14, but the LM3630A driver is not put into sleep mode.
- Display GPIO diagnostics run on every boot.
- I2C bus scan runs on every boot.
- SSD1677 display controller is not explicitly put into low power mode after UI updates.
Expected display-side numbers from the GDEQ0426T82-FT01C datasheet:
- Panel operation: about 8 mA / 26.4 mW typical.
- Sleep mode: 40-70 uA.
- Deep sleep: 2-6 uA.
The front light is likely the largest power draw whenever enabled. Exact current depends on LM3630A full-scale current, brightness setting, LED string configuration, and GPIO14 enable behavior.
## Highest impact changes
### 1. Start with front light off
The demo currently initializes the front light as on. For battery use, default it off.
Implementation targets:
- `src/ui/screens/demo.rs`: initialize `light_on` to `false`.
- `src/bin/main.rs`: initialize GPIO14 low.
- Only enable LM3630A outputs when the UI light button turns the light on.
Expected impact: high when the device is idle or used in ambient light.
### 2. Put LM3630A into sleep when light is off
GPIO14 low may reduce power, but the LM3630A should also be commanded into sleep.
When light is off:
- Set brightness to 0.
- Disable bank A and bank B.
- Set `ControlConfig { sleep: true, ... }`.
When light is on:
- Clear sleep.
- Enable the needed banks.
- Restore brightness.
Expected impact: high if the backlight IC otherwise remains active.
### 3. Lower CPU clock
The UI demo does not need 160 MHz.
Change:
- Replace `CpuClock::max()` with `CpuClock::_80MHz` in `src/bin/main.rs`.
Expected impact: moderate for active and idle runtime.
### 4. Stop polling touch every 25 ms
The FT6336U has an `INT` pin according to the datasheet. If it is wired to the MCU, use it.
Preferred approach:
- Configure touch `INT` as a GPIO input interrupt.
- Only read FT6336U coordinates after an interrupt.
- Use the same interrupt as a wake source for sleep modes if supported.
Fallback if `INT` is not wired:
- Slow touch polling to 100-200 ms while idle.
- Poll faster only briefly after a touch or button input.
Expected impact: moderate. It cuts frequent I2C transactions and wakeups.
### 5. Power off the SSD1677 after updates
The SSD1677 driver already exposes:
- `power_off(...)`
- `deep_sleep(...)`
After each display update, explicitly power down the display controller when no further update is pending.
Suggested behavior:
- After full or partial refresh: `display.power_off(&mut Delay)`.
- After a longer idle timeout: `display.deep_sleep(&mut Delay)`.
- Before the next update after deep sleep: reinitialize the display.
Expected impact: high for display-controller idle current.
### 6. Remove boot diagnostics from normal builds
Current boot does extra diagnostics:
- Display GPIO diagnostics.
- I2C bus scan.
These are useful during bring-up but should be behind a debug feature or removed from production builds.
Expected impact: improves boot time and boot energy.
### 7. Add inactivity sleep
After a period with no touch or button input:
1. Turn front light off.
2. Put LM3630A into sleep.
3. Power off or deep sleep SSD1677.
4. Put ESP32-C6 into light sleep or deep sleep.
5. Wake from button GPIO or FT6336U INT.
Suggested timeouts:
- 10-30 seconds: front light off.
- 30-120 seconds: display controller deep sleep.
- Longer idle: MCU sleep.
Expected impact: highest for real battery use.
## Suggested implementation order
1. Measure current draw in current firmware.
2. Default front light off.
3. Add LM3630A sleep/on command handling.
4. Switch CPU to 80 MHz.
5. Disable boot diagnostics outside debug builds.
6. Power off SSD1677 after updates.
7. Use touch INT instead of polling, or slow polling if INT is unavailable.
8. Add inactivity sleep and wake sources.
9. Measure again after each change.
## Measurement plan
Measure at least these states:
1. Boot until first screen shown.
2. Idle with front light on.
3. Idle with front light off.
4. Touch interaction and partial refresh.
5. Full refresh.
6. SSD1677 powered off after refresh.
7. SSD1677 deep sleep.
8. MCU light sleep or deep sleep.
Record:
- Battery/input voltage.
- Average current.
- Peak current during refresh/front-light startup.
- Firmware commit.
- Front light brightness and current limit.
- Whether USB serial/debugger is connected.
## Open hardware questions
- Is FT6336U `INT` connected to an ESP32-C6 GPIO?
- Is FT6336U `RST` connected to an ESP32-C6 GPIO?
- Does GPIO14 fully cut LM3630A power/enable, or only gate an enable pin?
- Are both LM3630A banks needed for the light, or can one bank be disabled?
- What is the target battery capacity and desired runtime?

View File

@ -1,3 +1,2 @@
# Name, Type, SubType, Offset, Size, Flags # Name, Type, SubType, Offset, Size, Flags
wifi,data,nvs,0x9000,0x7000,
factory,app,factory,0x10000,0x3F0000, factory,app,factory,0x10000,0x3F0000,

1 # Name Type SubType Offset Size Flags
wifi data nvs 0x9000 0x7000
2 factory app factory 0x10000 0x3F0000

View File

@ -3,7 +3,7 @@ set -euo pipefail
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
export RUSTFLAGS="${RUSTFLAGS:-} -C link-arg=--no-entry -C link-arg=--export=kinda_sim_init -C link-arg=--export=kinda_sim_input -C link-arg=--export=kinda_sim_width -C link-arg=--export=kinda_sim_height -C link-arg=--export=kinda_sim_pixels -C link-arg=--export=kinda_sim_pixel_len -C link-arg=--export=memory" export RUSTFLAGS="${RUSTFLAGS:-} -C link-arg=--no-entry -C link-arg=--export=kinda_sim_init -C link-arg=--export=kinda_sim_input -C link-arg=--export=kinda_sim_touch -C link-arg=--export=kinda_sim_width -C link-arg=--export=kinda_sim_height -C link-arg=--export=kinda_sim_pixels -C link-arg=--export=kinda_sim_pixel_len -C link-arg=--export=memory"
cargo build --bin kinda-web-sim --features web-sim --target wasm32-unknown-unknown --release cargo build --bin kinda-web-sim --features web-sim --target wasm32-unknown-unknown --release
mkdir -p web/pkg mkdir -p web/pkg

131
scripts/flash-font.sh Executable file
View File

@ -0,0 +1,131 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
MODE="flash"
FONT=""
usage() {
cat <<'EOF'
Usage:
scripts/flash-font.sh [--build-only|--generate-only] <font>
scripts/flash-font.sh --list
Fonts:
verdana /System/Library/Fonts/Supplemental/Verdana.ttf
tahoma /System/Library/Fonts/Supplemental/Tahoma.ttf
georgia /System/Library/Fonts/Supplemental/Georgia.ttf
arial /System/Library/Fonts/Supplemental/Arial.ttf
<font> may also be an explicit .ttf/.otf path.
Modes:
default Generate font tables, then cargo run/flash using .cargo runner.
--build-only Generate font tables, then build embedded target without flashing.
--generate-only Generate font tables only.
EOF
}
list_fonts() {
cat <<'EOF'
verdana=/System/Library/Fonts/Supplemental/Verdana.ttf
tahoma=/System/Library/Fonts/Supplemental/Tahoma.ttf
georgia=/System/Library/Fonts/Supplemental/Georgia.ttf
arial=/System/Library/Fonts/Supplemental/Arial.ttf
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--build-only)
MODE="build"
shift
;;
--generate-only)
MODE="generate"
shift
;;
--list)
list_fonts
exit 0
;;
-h|--help)
usage
exit 0
;;
*)
if [[ -n "$FONT" ]]; then
echo "error: multiple fonts specified: '$FONT' and '$1'" >&2
usage >&2
exit 2
fi
FONT="$1"
shift
;;
esac
done
if [[ -z "$FONT" ]]; then
usage >&2
exit 2
fi
font_path_for() {
case "$1" in
verdana) echo "/System/Library/Fonts/Supplemental/Verdana.ttf" ;;
tahoma) echo "/System/Library/Fonts/Supplemental/Tahoma.ttf" ;;
georgia) echo "/System/Library/Fonts/Supplemental/Georgia.ttf" ;;
arial) echo "/System/Library/Fonts/Supplemental/Arial.ttf" ;;
*) echo "$1" ;;
esac
}
FONT_PATH="$(font_path_for "$FONT")"
if [[ ! -f "$FONT_PATH" ]]; then
echo "error: font not found: $FONT_PATH" >&2
exit 1
fi
if ! command -v cc >/dev/null 2>&1; then
echo "error: cc not found" >&2
exit 1
fi
if ! command -v freetype-config >/dev/null 2>&1; then
echo "error: freetype-config not found" >&2
exit 1
fi
GENERATOR="/tmp/kinda-fontgen-ftmono"
GENERATOR_SOURCE="scripts/generate-fonts-ftmono.c"
if [[ ! -x "$GENERATOR" || "$GENERATOR_SOURCE" -nt "$GENERATOR" ]]; then
echo "Building FreeType mono font generator"
cc "$GENERATOR_SOURCE" $(freetype-config --cflags --libs) -o "$GENERATOR"
fi
echo "Generating font tables from: $FONT_PATH"
"$GENERATOR" "$FONT_PATH" src/ui/generated_fonts.rs
if command -v rg >/dev/null 2>&1; then
rg "Source font|Rasterizer|px; cell height" src/ui/generated_fonts.rs || true
else
grep -E "Source font|Rasterizer|px; cell height" src/ui/generated_fonts.rs || true
fi
case "$MODE" in
generate)
echo "Generated src/ui/generated_fonts.rs"
;;
build)
cargo build --bin kinda --target riscv32imac-unknown-none-elf
;;
flash)
cargo run --bin kinda --target riscv32imac-unknown-none-elf
;;
*)
echo "error: unknown mode: $MODE" >&2
exit 2
;;
esac

View File

@ -2,7 +2,6 @@
#![no_main] #![no_main]
#![feature(impl_trait_in_assoc_type)] #![feature(impl_trait_in_assoc_type)]
use alloc::string::String as AllocString;
use core::convert::Infallible; use core::convert::Infallible;
use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
@ -14,10 +13,9 @@ use embedded_graphics::{
primitives::Rectangle, primitives::Rectangle,
}; };
use embedded_hal::delay::DelayNs; use embedded_hal::delay::DelayNs;
use embedded_hal::i2c::I2c as EmbeddedI2c;
use embedded_hal::spi::{ErrorType, Operation, SpiBus, SpiDevice}; use embedded_hal::spi::{ErrorType, Operation, SpiBus, SpiDevice};
use embedded_storage::{ReadStorage, Storage};
use esp_backtrace as _; use esp_backtrace as _;
use esp_bootloader_esp_idf::partitions::{read_partition_table, PARTITION_TABLE_MAX_LEN};
use esp_hal::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull}; use esp_hal::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull};
use esp_hal::i2c::master::{Config as I2cConfig, I2c}; use esp_hal::i2c::master::{Config as I2cConfig, I2c};
use esp_hal::interrupt::software::SoftwareInterruptControl; use esp_hal::interrupt::software::SoftwareInterruptControl;
@ -25,16 +23,7 @@ use esp_hal::timer::timg::TimerGroup;
use esp_hal::{clock::CpuClock, ram, spi::master::Spi, time::Rate}; use esp_hal::{clock::CpuClock, ram, spi::master::Spi, time::Rate};
use esp_hal::{spi, Blocking}; use esp_hal::{spi, Blocking};
use esp_println as _; use esp_println as _;
use esp_radio::wifi::{ use kinda::{App, AppCommand, InputEvent, RefreshHint};
scan::ScanConfig, sta::StationConfig, AuthenticationMethod, Config as WifiConfig,
ControllerConfig, WifiController, WifiError as RadioWifiError,
};
use esp_storage::FlashStorage;
use kinda::{
App, AppCommand, InputEvent, RefreshHint, WifiAccessPoint, WifiCredentials, WifiEvent,
WifiFailure, WifiNetworkList, WifiPassword, WifiSecurity, WifiSsid, MAX_NETWORKS,
WIFI_CREDENTIAL_RECORD_BYTES,
};
use lm3630a::{ use lm3630a::{
Address, Bank, Brightness, Configuration, ControlConfig, FullScaleCurrent, Led2Bank, Lm3630a, Address, Bank, Brightness, Configuration, ControlConfig, FullScaleCurrent, Led2Bank, Lm3630a,
Mapping, Mapping,
@ -554,18 +543,24 @@ const LONG_PRESS_TICKS: u16 = 32;
const MAX_FAST_DISPLAY_UPDATES: u8 = 12; const MAX_FAST_DISPLAY_UPDATES: u8 = 12;
const PARTIAL_UPDATE_X_PADDING: u16 = 16; const PARTIAL_UPDATE_X_PADDING: u16 = 16;
const PARTIAL_UPDATE_Y_PADDING: u16 = 12; const PARTIAL_UPDATE_Y_PADDING: u16 = 12;
const LOGICAL_WIDTH: u16 = HEIGHT;
const LOGICAL_HEIGHT: u16 = WIDTH;
const TOUCH_RAW_LEFT: u16 = 453;
const TOUCH_RAW_RIGHT: u16 = 32;
const TOUCH_RAW_TOP: u16 = 733;
const TOUCH_RAW_BOTTOM: u16 = 69;
struct Rotated180<'a, D> { struct RotatedMinus90<'a, D> {
target: &'a mut D, target: &'a mut D,
} }
impl<'a, D> Rotated180<'a, D> { impl<'a, D> RotatedMinus90<'a, D> {
fn new(target: &'a mut D) -> Self { fn new(target: &'a mut D) -> Self {
Self { target } Self { target }
} }
} }
impl<D> DrawTarget for Rotated180<'_, D> impl<D> DrawTarget for RotatedMinus90<'_, D>
where where
D: DrawTarget + OriginDimensions, D: DrawTarget + OriginDimensions,
{ {
@ -576,26 +571,30 @@ where
where where
I: IntoIterator<Item = Pixel<Self::Color>>, I: IntoIterator<Item = Pixel<Self::Color>>,
{ {
let size = self.target.size(); let logical_size = self.size();
let width = size.width as i32; let logical_width = logical_size.width as i32;
let height = size.height as i32; let logical_height = logical_size.height as i32;
self.target self.target
.draw_iter(pixels.into_iter().filter_map(|Pixel(point, color)| { .draw_iter(pixels.into_iter().filter_map(|Pixel(point, color)| {
if point.x < 0 || point.y < 0 || point.x >= width || point.y >= height { if point.x < 0
|| point.y < 0
|| point.x >= logical_width
|| point.y >= logical_height
{
return None; return None;
} }
Some(Pixel( Some(Pixel(
Point::new(width - 1 - point.x, height - 1 - point.y), Point::new(logical_height - 1 - point.y, point.x),
color, color,
)) ))
})) }))
} }
fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> { fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> {
let size = self.target.size(); let logical_size = self.size();
let bounds = Rectangle::new(Point::new(0, 0), size); let bounds = Rectangle::new(Point::new(0, 0), logical_size);
let drawable_area = area.intersection(&bounds); let drawable_area = area.intersection(&bounds);
if drawable_area.size.width == 0 || drawable_area.size.height == 0 { if drawable_area.size.width == 0 || drawable_area.size.height == 0 {
@ -604,10 +603,12 @@ where
let rotated_area = Rectangle::new( let rotated_area = Rectangle::new(
Point::new( Point::new(
size.width as i32 - drawable_area.top_left.x - drawable_area.size.width as i32, logical_size.height as i32
size.height as i32 - drawable_area.top_left.y - drawable_area.size.height as i32, - drawable_area.top_left.y
- drawable_area.size.height as i32,
drawable_area.top_left.x,
), ),
drawable_area.size, Size::new(drawable_area.size.height, drawable_area.size.width),
); );
self.target.fill_solid(&rotated_area, color) self.target.fill_solid(&rotated_area, color)
} }
@ -617,12 +618,177 @@ where
} }
} }
impl<D> OriginDimensions for Rotated180<'_, D> impl<D> OriginDimensions for RotatedMinus90<'_, D>
where where
D: OriginDimensions, D: OriginDimensions,
{ {
fn size(&self) -> Size { fn size(&self) -> Size {
self.target.size() let native_size = self.target.size();
Size::new(native_size.height, native_size.width)
}
}
struct TouchController<I2C> {
i2c: I2C,
was_touching: bool,
read_failed: bool,
}
impl<I2C> TouchController<I2C>
where
I2C: EmbeddedI2c,
{
const ADDRESS: u8 = 0x38;
const CHIP_ID: u8 = 0x64;
const REG_ID_G_CIPHER: u8 = 0xA3;
const REG_TOUCH1_XH: u8 = 0x03;
const REG_TD_STATUS: u8 = 0x02;
const TOUCH_EVENT_MASK: u8 = 0xC0;
const TOUCH_EVENT_DOWN: u8 = 0x00;
const TOUCH_EVENT_CONTACT: u8 = 0x80;
const LM3630A_ADDRESS: u8 = 0x36;
const LM3630A_CONTROL: u8 = 0x00;
const LM3630A_CONFIGURATION: u8 = 0x01;
const LM3630A_BRIGHTNESS_A: u8 = 0x03;
const LM3630A_BRIGHTNESS_B: u8 = 0x04;
const LM3630A_CURRENT_A: u8 = 0x05;
const LM3630A_CURRENT_B: u8 = 0x06;
const LM3630A_CONFIGURATION_DEFAULT: u8 = 0x18;
const LM3630A_CONTROL_ON: u8 = 0x1E;
const LM3630A_CONTROL_SLEEP: u8 = 0x80;
const LM3630A_CURRENT_CODE: u8 = 6;
const LM3630A_BRIGHTNESS_ON: u8 = 100;
fn new(i2c: I2C) -> Self {
Self {
i2c,
was_touching: false,
read_failed: false,
}
}
fn init(&mut self) {
let mut chip_id = [0u8; 1];
match self
.i2c
.write_read(Self::ADDRESS, &[Self::REG_ID_G_CIPHER], &mut chip_id)
{
Ok(()) => {
if chip_id[0] == Self::CHIP_ID {
info!("FT6336U touch detected at I2C 0x38");
} else {
defmt::warn!(
"Touch controller at I2C 0x38 returned chip id {}; expected {}",
chip_id[0],
Self::CHIP_ID
);
}
}
Err(_) => {
self.read_failed = true;
defmt::warn!("FT6336U touch not detected at I2C 0x38");
}
}
}
fn poll(&mut self) -> Option<InputEvent> {
let mut points = [0u8; 1];
if self
.i2c
.write_read(Self::ADDRESS, &[Self::REG_TD_STATUS], &mut points)
.is_err()
{
if !self.read_failed {
defmt::warn!("FT6336U touch read failed");
}
self.read_failed = true;
self.was_touching = false;
return None;
}
self.read_failed = false;
let points = points[0] & 0x0F;
if points == 0 {
self.was_touching = false;
return None;
}
let mut data = [0u8; 4];
if self
.i2c
.write_read(Self::ADDRESS, &[Self::REG_TOUCH1_XH], &mut data)
.is_err()
{
if !self.read_failed {
defmt::warn!("FT6336U touch coordinate read failed");
}
self.read_failed = true;
self.was_touching = false;
return None;
}
let event = data[0] & Self::TOUCH_EVENT_MASK;
if event != Self::TOUCH_EVENT_DOWN && event != Self::TOUCH_EVENT_CONTACT {
self.was_touching = false;
return None;
}
if self.was_touching {
return None;
}
self.was_touching = true;
let raw_x = (u16::from(data[0] & 0x0F) << 8) | u16::from(data[1]);
let raw_y = (u16::from(data[2] & 0x0F) << 8) | u16::from(data[3]);
let (x, y) = Self::logical_touch(raw_x, raw_y);
info!(
"FT6336U touch raw=({}, {}) logical=({}, {})",
raw_x, raw_y, x, y
);
Some(InputEvent::Touch { x, y })
}
fn set_front_light(&mut self, on: bool) -> Result<(), I2C::Error> {
if on {
self.write_lm3630a(
Self::LM3630A_CONFIGURATION,
Self::LM3630A_CONFIGURATION_DEFAULT,
)?;
self.write_lm3630a(Self::LM3630A_CURRENT_A, Self::LM3630A_CURRENT_CODE)?;
self.write_lm3630a(Self::LM3630A_CURRENT_B, Self::LM3630A_CURRENT_CODE)?;
self.write_lm3630a(Self::LM3630A_BRIGHTNESS_A, Self::LM3630A_BRIGHTNESS_ON)?;
self.write_lm3630a(Self::LM3630A_BRIGHTNESS_B, Self::LM3630A_BRIGHTNESS_ON)?;
self.write_lm3630a(Self::LM3630A_CONTROL, Self::LM3630A_CONTROL_ON)
} else {
self.write_lm3630a(Self::LM3630A_BRIGHTNESS_A, 0)?;
self.write_lm3630a(Self::LM3630A_BRIGHTNESS_B, 0)?;
self.write_lm3630a(Self::LM3630A_CONTROL, Self::LM3630A_CONTROL_SLEEP)
}
}
fn write_lm3630a(&mut self, register: u8, value: u8) -> Result<(), I2C::Error> {
self.i2c.write(Self::LM3630A_ADDRESS, &[register, value])
}
fn logical_touch(raw_x: u16, raw_y: u16) -> (u16, u16) {
let x = Self::map_inverted_axis(raw_x, TOUCH_RAW_LEFT, TOUCH_RAW_RIGHT, LOGICAL_WIDTH - 1);
let y = Self::map_inverted_axis(raw_y, TOUCH_RAW_TOP, TOUCH_RAW_BOTTOM, LOGICAL_HEIGHT - 1);
(x, y)
}
fn map_inverted_axis(raw: u16, raw_at_zero: u16, raw_at_max: u16, logical_max: u16) -> u16 {
if raw >= raw_at_zero {
return 0;
}
if raw <= raw_at_max {
return logical_max;
}
let span = u32::from(raw_at_zero - raw_at_max);
let offset = u32::from(raw_at_zero - raw);
((offset * u32::from(logical_max) + (span / 2)) / span) as u16
} }
} }
@ -742,204 +908,6 @@ impl ButtonState {
} }
} }
struct WifiCredentialStore {
flash: FlashStorage<'static>,
}
impl WifiCredentialStore {
const PARTITION_LABEL: &'static str = "wifi";
fn new(flash: esp_hal::peripherals::FLASH<'static>) -> Self {
Self {
flash: FlashStorage::new(flash),
}
}
fn load(&mut self) -> Option<WifiCredentials> {
let mut table_storage = [0u8; PARTITION_TABLE_MAX_LEN];
let table = read_partition_table(&mut self.flash, &mut table_storage).ok()?;
let partition = table
.iter()
.find(|partition| partition.label_as_str() == Self::PARTITION_LABEL)?;
let mut region = partition.as_embedded_storage(&mut self.flash);
let mut record = [0u8; WIFI_CREDENTIAL_RECORD_BYTES];
region.read(0, &mut record).ok()?;
WifiCredentials::decode(&record)
}
fn save(&mut self, credentials: &WifiCredentials) -> Result<(), ()> {
let mut table_storage = [0u8; PARTITION_TABLE_MAX_LEN];
let table = read_partition_table(&mut self.flash, &mut table_storage).map_err(|_| ())?;
let partition = table
.iter()
.find(|partition| partition.label_as_str() == Self::PARTITION_LABEL)
.ok_or(())?;
let mut region = partition.as_embedded_storage(&mut self.flash);
let record = credentials.encode();
region.write(0, &record).map_err(|_| ())
}
}
struct WifiService {
controller: WifiController<'static>,
}
impl WifiService {
fn new(device: esp_hal::peripherals::WIFI<'static>) -> Result<Self, RadioWifiError> {
let controller = WifiController::new(device, ControllerConfig::default())?;
Ok(Self { controller })
}
async fn handle(&mut self, command: AppCommand) -> WifiEvent {
match command {
AppCommand::ScanWifi => self.scan().await,
AppCommand::JoinWifi {
access_point,
password,
} => self.join_access_point(access_point, password).await,
AppCommand::JoinSavedWifi { credentials } => self.join_saved(credentials).await,
}
}
fn unavailable(command: AppCommand) -> WifiEvent {
match command {
AppCommand::ScanWifi => WifiEvent::ScanFinished(Err(WifiFailure::Unsupported)),
AppCommand::JoinWifi { .. } | AppCommand::JoinSavedWifi { .. } => {
WifiEvent::JoinFinished(Err(WifiFailure::Unsupported))
}
}
}
async fn scan(&mut self) -> WifiEvent {
let scan_config = ScanConfig::default().with_max(MAX_NETWORKS);
match self.controller.scan_async(&scan_config).await {
Ok(access_points) => {
let mut networks = WifiNetworkList::new();
for access_point in access_points {
if access_point.ssid.is_empty() {
continue;
}
let mut ssid = WifiSsid::new();
if ssid.push_str(access_point.ssid.as_str()).is_err() {
continue;
}
let network = WifiAccessPoint {
ssid,
bssid: access_point.bssid,
channel: access_point.channel,
signal_strength: access_point.signal_strength,
security: Self::from_auth_method(access_point.auth_method),
};
if networks.push(network).is_err() {
break;
}
}
WifiEvent::ScanFinished(Ok(networks))
}
Err(error) => {
defmt::error!("WiFi scan failed: {:?}", error);
WifiEvent::ScanFinished(Err(Self::scan_failure_from_error(error)))
}
}
}
async fn join_access_point(
&mut self,
access_point: WifiAccessPoint,
password: WifiPassword,
) -> WifiEvent {
let ssid = access_point.ssid.clone();
let station_config = StationConfig::default()
.with_ssid(access_point.ssid.as_str())
.with_bssid(access_point.bssid)
.with_channel(access_point.channel)
.with_auth_method(Self::to_auth_method(access_point.security))
.with_password(AllocString::from(password.as_str()));
self.connect(ssid, station_config).await
}
async fn join_saved(&mut self, credentials: WifiCredentials) -> WifiEvent {
let ssid = credentials.ssid.clone();
let station_config = StationConfig::default()
.with_ssid(credentials.ssid.as_str())
.with_auth_method(Self::to_auth_method(credentials.security))
.with_password(AllocString::from(credentials.password.as_str()));
self.connect(ssid, station_config).await
}
async fn connect(&mut self, ssid: WifiSsid, station_config: StationConfig) -> WifiEvent {
let _ = self.controller.disconnect_async().await;
let station_config = WifiConfig::Station(station_config);
if let Err(error) = self.controller.set_config(&station_config) {
defmt::error!("WiFi config failed: {:?}", error);
return WifiEvent::JoinFinished(Err(Self::join_failure_from_error(error)));
}
match self.controller.connect_async().await {
Ok(_) => WifiEvent::JoinFinished(Ok(ssid)),
Err(error) => {
defmt::error!("WiFi join failed: {:?}", error);
WifiEvent::JoinFinished(Err(Self::join_failure_from_error(error)))
}
}
}
fn from_auth_method(auth_method: Option<AuthenticationMethod>) -> WifiSecurity {
match auth_method {
None | Some(AuthenticationMethod::None) => WifiSecurity::Open,
Some(AuthenticationMethod::Wep) => WifiSecurity::Wep,
Some(AuthenticationMethod::Wpa) => WifiSecurity::Wpa,
Some(AuthenticationMethod::Wpa2Personal) => WifiSecurity::Wpa2Personal,
Some(AuthenticationMethod::WpaWpa2Personal) => WifiSecurity::WpaWpa2Personal,
Some(AuthenticationMethod::Wpa2Wpa3Personal) => WifiSecurity::Wpa2Wpa3Personal,
Some(AuthenticationMethod::Wpa3Personal) => WifiSecurity::Wpa3Personal,
_ => WifiSecurity::Other,
}
}
fn to_auth_method(security: WifiSecurity) -> AuthenticationMethod {
match security {
WifiSecurity::Open => AuthenticationMethod::None,
WifiSecurity::Wep => AuthenticationMethod::Wep,
WifiSecurity::Wpa => AuthenticationMethod::Wpa,
WifiSecurity::Wpa2Personal => AuthenticationMethod::Wpa2Personal,
WifiSecurity::WpaWpa2Personal => AuthenticationMethod::WpaWpa2Personal,
WifiSecurity::Wpa2Wpa3Personal => AuthenticationMethod::Wpa2Wpa3Personal,
WifiSecurity::Wpa3Personal => AuthenticationMethod::Wpa3Personal,
WifiSecurity::Other => AuthenticationMethod::Wpa2Personal,
}
}
fn scan_failure_from_error(error: RadioWifiError) -> WifiFailure {
match error {
RadioWifiError::OutOfMemory => WifiFailure::OutOfMemory,
RadioWifiError::Unsupported => WifiFailure::Unsupported,
_ => WifiFailure::ScanFailed,
}
}
fn join_failure_from_error(error: RadioWifiError) -> WifiFailure {
match error {
RadioWifiError::InvalidSsid
| RadioWifiError::InvalidPassword
| RadioWifiError::InvalidArguments => WifiFailure::InvalidCredentials,
RadioWifiError::OutOfMemory => WifiFailure::OutOfMemory,
RadioWifiError::Unsupported => WifiFailure::Unsupported,
RadioWifiError::NotConnected => WifiFailure::NotConnected,
_ => WifiFailure::JoinFailed,
}
}
}
fn present_display_update<SPI, DC, RST, BUSY, PinError>( fn present_display_update<SPI, DC, RST, BUSY, PinError>(
display: &mut Ssd1677<SPI, DC, RST, BUSY>, display: &mut Ssd1677<SPI, DC, RST, BUSY>,
screen: &Framebuffer<'_>, screen: &Framebuffer<'_>,
@ -1070,46 +1038,32 @@ async fn main(spawner: Spawner) {
info!("Embassy initialized!"); info!("Embassy initialized!");
let mut wifi = match WifiService::new(peripherals.WIFI) {
Ok(service) => Some(service),
Err(error) => {
defmt::error!("WiFi init failed: {:?}", error);
None
}
};
let mut credential_store = WifiCredentialStore::new(peripherals.FLASH);
let saved_credentials = credential_store.load();
if saved_credentials.is_some() {
info!("Loaded saved WiFi credentials");
}
let _ = spawner; let _ = spawner;
let button_bottom = Input::new( let button_bottom = Input::new(
peripherals.GPIO14, peripherals.GPIO4,
InputConfig::default().with_pull(Pull::Up), InputConfig::default().with_pull(Pull::Up),
); );
let button_left = Input::new( let button_left = Input::new(
peripherals.GPIO19, peripherals.GPIO5,
InputConfig::default().with_pull(Pull::Up), InputConfig::default().with_pull(Pull::Up),
); );
let button_right_up = Input::new( let button_right_up = Input::new(
peripherals.GPIO20, peripherals.GPIO6,
InputConfig::default().with_pull(Pull::Up), InputConfig::default().with_pull(Pull::Up),
); );
let button_right_down = Input::new( let button_right_down = Input::new(
peripherals.GPIO21, peripherals.GPIO7,
InputConfig::default().with_pull(Pull::Up), InputConfig::default().with_pull(Pull::Up),
); );
let mut display_pins = DisplayPins { let mut display_pins = DisplayPins {
cs: Flex::new(peripherals.GPIO2), cs: Flex::new(peripherals.GPIO20),
dc: Flex::new(peripherals.GPIO3), dc: Flex::new(peripherals.GPIO21),
rst: Flex::new(peripherals.GPIO4), rst: Flex::new(peripherals.GPIO22),
busy: Flex::new(peripherals.GPIO5), busy: Flex::new(peripherals.GPIO23),
sck: Flex::new(peripherals.GPIO6), sck: Flex::new(peripherals.GPIO19),
mosi: Flex::new(peripherals.GPIO7), mosi: Flex::new(peripherals.GPIO18),
}; };
display_pins.run_diagnostics().await; display_pins.run_diagnostics().await;
@ -1148,7 +1102,7 @@ async fn main(spawner: Spawner) {
let previous_buf = PREVIOUS_DISPLAY_BUFFER.init([0; FRAME_BYTES]); let previous_buf = PREVIOUS_DISPLAY_BUFFER.init([0; FRAME_BYTES]);
let mut screen = Framebuffer::new(buf).expect("invalid framebuffer"); let mut screen = Framebuffer::new(buf).expect("invalid framebuffer");
let mut backlight_enable = Output::new(peripherals.GPIO18, Level::High, Default::default()); let mut backlight_enable = Output::new(peripherals.GPIO14, Level::High, Default::default());
backlight_enable.set_high(); backlight_enable.set_high();
Timer::after_millis(10).await; Timer::after_millis(10).await;
@ -1157,8 +1111,8 @@ async fn main(spawner: Spawner) {
I2cConfig::default().with_frequency(Rate::from_khz(10)), I2cConfig::default().with_frequency(Rate::from_khz(10)),
) )
.expect("did not get I2C") .expect("did not get I2C")
.with_sda(peripherals.GPIO22) .with_sda(peripherals.GPIO3)
.with_scl(peripherals.GPIO23); .with_scl(peripherals.GPIO2);
info!("Scanning I2C bus"); info!("Scanning I2C bus");
let mut devices_found = 0u8; let mut devices_found = 0u8;
@ -1237,17 +1191,21 @@ async fn main(spawner: Spawner) {
defmt::error!("Skipping LM3630A brightness update because init failed"); defmt::error!("Skipping LM3630A brightness update because init failed");
} }
let i2c = backlight.release();
let mut touch = TouchController::new(i2c);
touch.init();
let mut buttons = Buttons::new( let mut buttons = Buttons::new(
button_bottom, button_bottom,
button_left, button_left,
button_right_up, button_right_up,
button_right_down, button_right_down,
); );
let mut app = App::new_with_saved_wifi(saved_credentials); let mut app = App::new();
{ {
let mut threshold_screen = ThresholdFramebuffer::new(&mut screen); let mut threshold_screen = ThresholdFramebuffer::new(&mut screen);
let mut display_target = Rotated180::new(&mut threshold_screen); let mut display_target = RotatedMinus90::new(&mut threshold_screen);
app.render(&mut display_target).expect("did not render app"); app.render(&mut display_target).expect("did not render app");
} }
DISPLAY_BUSY_STATS.reset(); DISPLAY_BUSY_STATS.reset();
@ -1263,58 +1221,37 @@ async fn main(spawner: Spawner) {
.copy_to(previous_buf) .copy_to(previous_buf)
.expect("did not sync framebuffer"); .expect("did not sync framebuffer");
let mut fast_update_count = 0u8; let mut fast_update_count = 0u8;
let mut pending_command = app.take_pending_command();
loop { loop {
if pending_command.is_none() { if let Some(event) = buttons.poll().or_else(|| touch.poll()) {
if let Some(event) = buttons.poll() { let update = app.update(event);
let update = app.update(event); if let Some(command) = update.command {
if update.dirty { match command {
{ AppCommand::SetFrontLight { on } => {
let mut threshold_screen = ThresholdFramebuffer::new(&mut screen); if on {
let mut display_target = Rotated180::new(&mut threshold_screen); backlight_enable.set_high();
app.render(&mut display_target).expect("did not render app"); Timer::after_millis(10).await;
} if touch.set_front_light(true).is_ok() {
present_display_update( info!("Front light on");
&mut display, } else {
&screen, defmt::error!("Failed to turn front light on");
previous_buf, }
update.refresh, } else {
&mut fast_update_count, if touch.set_front_light(false).is_ok() {
); info!("Front light off");
} } else {
pending_command = update.command; defmt::error!("Failed to turn front light off");
} }
} backlight_enable.set_low();
}
if let Some(command) = pending_command.take() {
let command_for_storage = command.clone();
let wifi_event = match wifi.as_mut() {
Some(wifi) => wifi.handle(command).await,
None => WifiService::unavailable(command),
};
if let AppCommand::JoinWifi {
access_point,
password,
} = &command_for_storage
{
if matches!(&wifi_event, WifiEvent::JoinFinished(Ok(_))) {
let credentials =
WifiCredentials::from_access_point(access_point, password.clone());
if credential_store.save(&credentials).is_ok() {
info!("Saved WiFi credentials");
} else {
defmt::error!("Failed to save WiFi credentials");
} }
} }
} }
let update = app.handle_wifi_event(wifi_event);
if update.dirty { if update.dirty {
{ {
let mut threshold_screen = ThresholdFramebuffer::new(&mut screen); let mut threshold_screen = ThresholdFramebuffer::new(&mut screen);
let mut display_target = Rotated180::new(&mut threshold_screen); let mut display_target = RotatedMinus90::new(&mut threshold_screen);
app.render(&mut display_target).expect("did not render app"); app.render(&mut display_target).expect("did not render app");
} }
present_display_update( present_display_update(
@ -1325,7 +1262,6 @@ async fn main(spawner: Spawner) {
&mut fast_update_count, &mut fast_update_count,
); );
} }
pending_command = update.command;
} }
Timer::after_millis(25).await; Timer::after_millis(25).await;

View File

@ -10,13 +10,10 @@ use embedded_graphics::{
Pixel, Pixel,
}; };
use kinda::{ use kinda::{App, AppUpdate, InputEvent};
App, AppCommand, AppUpdate, InputEvent, WifiAccessPoint, WifiCredentials, WifiEvent,
WifiNetworkList, WifiSecurity, WifiSsid,
};
const WIDTH: u32 = 800; const WIDTH: u32 = 480;
const HEIGHT: u32 = 480; const HEIGHT: u32 = 800;
const PIXEL_COUNT: usize = (WIDTH * HEIGHT) as usize; const PIXEL_COUNT: usize = (WIDTH * HEIGHT) as usize;
const EVENT_UP: u32 = 0; const EVENT_UP: u32 = 0;
@ -68,88 +65,11 @@ impl WebSimulator {
} }
fn apply_update(&mut self, update: AppUpdate) { fn apply_update(&mut self, update: AppUpdate) {
let mut dirty = update.dirty; if update.dirty {
let mut command = update.command;
while let Some(next_command) = command.take() {
let wifi_event = self.handle_command(next_command);
let next_update = self.app().handle_wifi_event(wifi_event);
dirty |= next_update.dirty;
command = next_update.command;
}
if dirty {
self.render(); self.render();
} }
} }
fn handle_command(&self, command: AppCommand) -> WifiEvent {
match command {
AppCommand::ScanWifi => WifiEvent::ScanFinished(Ok(self.mock_networks())),
AppCommand::JoinWifi {
access_point,
password: _,
} => WifiEvent::JoinFinished(Ok(access_point.ssid)),
AppCommand::JoinSavedWifi { credentials } => self.join_saved(credentials),
}
}
fn join_saved(&self, credentials: WifiCredentials) -> WifiEvent {
WifiEvent::JoinFinished(Ok(credentials.ssid))
}
fn mock_networks(&self) -> WifiNetworkList {
let mut networks = WifiNetworkList::new();
self.push_mock_network(
&mut networks,
"kinda-lab",
[0x02, 0x00, 0x00, 0x00, 0x00, 0x01],
6,
-42,
WifiSecurity::Wpa2Personal,
);
self.push_mock_network(
&mut networks,
"open-books",
[0x02, 0x00, 0x00, 0x00, 0x00, 0x02],
11,
-64,
WifiSecurity::Open,
);
self.push_mock_network(
&mut networks,
"reader-test",
[0x02, 0x00, 0x00, 0x00, 0x00, 0x03],
1,
-76,
WifiSecurity::Wpa2Wpa3Personal,
);
networks
}
fn push_mock_network(
&self,
networks: &mut WifiNetworkList,
name: &str,
bssid: [u8; 6],
channel: u8,
signal_strength: i8,
security: WifiSecurity,
) {
let mut ssid = WifiSsid::new();
if ssid.push_str(name).is_err() {
return;
}
let _ = networks.push(WifiAccessPoint {
ssid,
bssid,
channel,
signal_strength,
security,
});
}
fn render(&mut self) { fn render(&mut self) {
let app = self.app(); let app = self.app();
let mut display = PixelDisplay::new(PIXELS.get()); let mut display = PixelDisplay::new(PIXELS.get());
@ -251,6 +171,13 @@ pub extern "C" fn kinda_sim_input(event: u32) {
SIMULATOR.get().input(event); SIMULATOR.get().input(event);
} }
#[no_mangle]
pub extern "C" fn kinda_sim_touch(x: u32, y: u32) {
let x = x.min(u32::from(u16::MAX)) as u16;
let y = y.min(u32::from(u16::MAX)) as u16;
SIMULATOR.get().input(InputEvent::Touch { x, y });
}
#[no_mangle] #[no_mangle]
pub extern "C" fn kinda_sim_width() -> u32 { pub extern "C" fn kinda_sim_width() -> u32 {
WIDTH WIDTH

View File

@ -6,4 +6,5 @@ pub enum InputEvent {
Back, Back,
SelectLong, SelectLong,
BackLong, BackLong,
Touch { x: u16, y: u16 },
} }

View File

@ -2,11 +2,6 @@
pub mod input; pub mod input;
pub mod ui; pub mod ui;
pub mod wifi;
pub use input::InputEvent; pub use input::InputEvent;
pub use ui::{App, AppCommand, AppUpdate, RefreshHint}; pub use ui::{App, AppCommand, AppUpdate, RefreshHint};
pub use wifi::{
WifiAccessPoint, WifiCredentials, WifiEvent, WifiFailure, WifiNetworkList, WifiPassword,
WifiSecurity, WifiSsid, MAX_NETWORKS, WIFI_CREDENTIAL_RECORD_BYTES,
};

View File

@ -3,27 +3,12 @@ use embedded_graphics::{
prelude::{DrawTarget, OriginDimensions}, prelude::{DrawTarget, OriginDimensions},
}; };
use crate::{input::InputEvent, wifi::WifiEvent, WifiAccessPoint, WifiCredentials, WifiPassword}; use crate::input::InputEvent;
use super::{ use super::{frame::Frame, screens::demo::DemoScreen, RefreshHint};
frame::Frame,
screens::{
library::{LibraryAction, LibraryScreen},
onboarding::{OnboardingAction, OnboardingScreen},
reader::{ReaderAction, ReaderScreen},
},
RefreshHint,
};
pub struct App { pub struct App {
route: Route, screen: DemoScreen,
pending_command: Option<AppCommand>,
}
enum Route {
Onboarding(OnboardingScreen),
Library(LibraryScreen),
Reader(ReaderScreen),
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@ -33,104 +18,21 @@ pub struct AppUpdate {
pub command: Option<AppCommand>, pub command: Option<AppCommand>,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AppCommand { pub enum AppCommand {
ScanWifi, SetFrontLight { on: bool },
JoinWifi {
access_point: WifiAccessPoint,
password: WifiPassword,
},
JoinSavedWifi {
credentials: WifiCredentials,
},
} }
impl App { impl App {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
route: Route::Onboarding(OnboardingScreen::new()), screen: DemoScreen::new(),
pending_command: None,
} }
} }
pub fn new_with_saved_wifi(credentials: Option<WifiCredentials>) -> Self {
let Some(credentials) = credentials else {
return Self::new();
};
let ssid = credentials.ssid.clone();
Self {
route: Route::Onboarding(OnboardingScreen::new_connecting_saved(ssid)),
pending_command: Some(AppCommand::JoinSavedWifi { credentials }),
}
}
pub fn take_pending_command(&mut self) -> Option<AppCommand> {
self.pending_command.take()
}
pub fn update(&mut self, event: InputEvent) -> AppUpdate { pub fn update(&mut self, event: InputEvent) -> AppUpdate {
match &mut self.route { let update = self.screen.update(event);
Route::Onboarding(screen) => { AppUpdate::new(update.dirty, update.refresh, update.command)
let update = screen.update(event);
match update.action {
OnboardingAction::None => {
AppUpdate::new(update.dirty, update.refresh, update.command)
}
OnboardingAction::OpenLibrary => {
self.route = Route::Library(LibraryScreen::new());
AppUpdate::new(true, RefreshHint::Full, None)
}
}
}
Route::Library(screen) => {
let update = screen.update(event);
match update.action {
LibraryAction::None => {
AppUpdate::new(update.dirty, update.refresh, update.command)
}
LibraryAction::OpenOnboarding => {
self.route = Route::Onboarding(OnboardingScreen::new());
AppUpdate::new(true, RefreshHint::Full, None)
}
LibraryAction::OpenReader { book_index } => {
self.route = Route::Reader(ReaderScreen::new(book_index));
AppUpdate::new(true, RefreshHint::Full, None)
}
}
}
Route::Reader(screen) => {
let update = screen.update(event);
match update.action {
ReaderAction::None => {
AppUpdate::new(update.dirty, update.refresh, update.command)
}
ReaderAction::OpenLibrary { selected_book } => {
self.route =
Route::Library(LibraryScreen::new_with_selected(selected_book));
AppUpdate::new(true, RefreshHint::Full, None)
}
}
}
}
}
pub fn handle_wifi_event(&mut self, event: WifiEvent) -> AppUpdate {
match &mut self.route {
Route::Onboarding(screen) => {
let update = screen.handle_wifi_event(event);
match update.action {
OnboardingAction::None => {
AppUpdate::new(update.dirty, update.refresh, update.command)
}
OnboardingAction::OpenLibrary => {
self.route = Route::Library(LibraryScreen::new());
AppUpdate::new(true, RefreshHint::Full, None)
}
}
}
_ => AppUpdate::new(false, RefreshHint::None, None),
}
} }
pub fn render<D>(&self, target: &mut D) -> Result<(), D::Error> pub fn render<D>(&self, target: &mut D) -> Result<(), D::Error>
@ -139,12 +41,7 @@ impl App {
{ {
let mut frame = Frame::new(target); let mut frame = Frame::new(target);
frame.clear()?; frame.clear()?;
self.screen.render(&mut frame)
match &self.route {
Route::Onboarding(screen) => screen.render(&mut frame),
Route::Library(screen) => screen.render(&mut frame),
Route::Reader(screen) => screen.render(&mut frame),
}
} }
} }

View File

@ -1,39 +0,0 @@
pub(crate) struct Book {
pub title: &'static str,
pub author: &'static str,
pub pages: &'static [&'static str],
}
const BOOK_ONE_PAGES: [&str; 3] = [
"The first screen of a reader should make the page feel stable.\n\nButtons can move slowly, but the page should not jump around.\n\nDown turns the page. Up goes back.",
"A first draft can use prepared page text.\n\nLater the book service can replace this with real layout output.\n\nThe reader screen should not care where the page came from.",
"This is the last mock page.\n\nBack returns to the library. Select can become the reader menu later.",
];
const BOOK_TWO_PAGES: [&str; 2] = [
"WiFi setup is part of the device, not a separate utility.\n\nThe network list and password editor are real UI screens.",
"Password entry uses a single active character.\n\nUp and down change it, select appends it, and back deletes it.",
];
const BOOK_THREE_PAGES: [&str; 2] = [
"E-paper refresh is a UI concern.\n\nFast updates are useful while moving around. Full updates are useful when changing screens.",
"Fonts can wait until the flow is right.\n\nOnce the screens feel correct, custom bitmap fonts can replace the built-in ones.",
];
pub(crate) const BOOKS: [Book; 3] = [
Book {
title: "Reader Notes",
author: "Kinda",
pages: &BOOK_ONE_PAGES,
},
Book {
title: "Onboarding Plan",
author: "Kinda",
pages: &BOOK_TWO_PAGES,
},
Book {
title: "Display Notes",
author: "Kinda",
pages: &BOOK_THREE_PAGES,
},
];

View File

@ -1,8 +1,9 @@
use embedded_graphics::{ use embedded_graphics::{
geometry::{Point, Size}, geometry::{Point, Size},
pixelcolor::Gray8, pixelcolor::Gray8,
prelude::{DrawTarget, OriginDimensions}, prelude::{DrawTarget, OriginDimensions, Primitive},
primitives::Rectangle, primitives::{Circle, Line, PrimitiveStyle, Rectangle},
Drawable,
}; };
use crate::ui::{ use crate::ui::{
@ -34,11 +35,19 @@ where
} }
pub fn body(&mut self, y: i32, text: &str) -> Result<(), D::Error> { pub fn body(&mut self, y: i32, text: &str) -> Result<(), D::Error> {
self.text(text, Point::new(48, y), Self::body_style()) self.body_at(Point::new(48, y), text)
}
pub fn body_at(&mut self, position: Point, text: &str) -> Result<(), D::Error> {
self.text(text, position, Self::body_style())
} }
pub fn small(&mut self, y: i32, text: &str) -> Result<(), D::Error> { pub fn small(&mut self, y: i32, text: &str) -> Result<(), D::Error> {
self.text(text, Point::new(48, y), Self::small_style()) self.small_at(Point::new(48, y), text)
}
pub fn small_at(&mut self, position: Point, text: &str) -> Result<(), D::Error> {
self.text(text, position, Self::small_style())
} }
pub fn footer(&mut self, text: &str) -> Result<(), D::Error> { pub fn footer(&mut self, text: &str) -> Result<(), D::Error> {
@ -64,6 +73,7 @@ where
self.wrapped_text(text, bounds, small_style) self.wrapped_text(text, bounds, small_style)
} }
#[allow(dead_code)]
pub fn paragraph(&mut self, y: i32, text: &str) -> Result<(), D::Error> { pub fn paragraph(&mut self, y: i32, text: &str) -> Result<(), D::Error> {
let size = self.target.size(); let size = self.target.size();
let x = 72; let x = 72;
@ -75,6 +85,41 @@ where
self.wrapped_text(text, bounds, Self::reader_style()) self.wrapped_text(text, bounds, Self::reader_style())
} }
pub fn outline_rect(
&mut self,
top_left: Point,
size: Size,
color: Gray8,
stroke_width: u32,
) -> Result<(), D::Error> {
Rectangle::new(top_left, size)
.into_styled(PrimitiveStyle::with_stroke(color, stroke_width))
.draw(self.target)
}
pub fn filled_rect(
&mut self,
top_left: Point,
size: Size,
color: Gray8,
) -> Result<(), D::Error> {
Rectangle::new(top_left, size)
.into_styled(PrimitiveStyle::with_fill(color))
.draw(self.target)
}
pub fn line(&mut self, start: Point, end: Point, color: Gray8) -> Result<(), D::Error> {
Line::new(start, end)
.into_styled(PrimitiveStyle::with_stroke(color, 2))
.draw(self.target)
}
pub fn circle(&mut self, top_left: Point, diameter: u32, color: Gray8) -> Result<(), D::Error> {
Circle::new(top_left, diameter)
.into_styled(PrimitiveStyle::with_stroke(color, 2))
.draw(self.target)
}
fn text(&mut self, text: &str, position: Point, style: TextStyle) -> Result<(), D::Error> { fn text(&mut self, text: &str, position: Point, style: TextStyle) -> Result<(), D::Error> {
TextRenderer::new(self.target).draw_text(text, position, style) TextRenderer::new(self.target).draw_text(text, position, style)
} }
@ -96,6 +141,7 @@ where
TextStyle::new(&generated_fonts::QUATTRO_BODY, Gray8::new(0), 38) TextStyle::new(&generated_fonts::QUATTRO_BODY, Gray8::new(0), 38)
} }
#[allow(dead_code)]
fn reader_style() -> TextStyle { fn reader_style() -> TextStyle {
TextStyle::new(&generated_fonts::QUATTRO_READER, Gray8::new(0), 43) TextStyle::new(&generated_fonts::QUATTRO_READER, Gray8::new(0), 43)
} }

View File

@ -1,6 +1,6 @@
mod app; mod app;
mod books;
mod frame; mod frame;
#[allow(dead_code)]
mod generated_fonts; mod generated_fonts;
mod screens; mod screens;
mod typography; mod typography;

333
src/ui/screens/demo.rs Normal file
View File

@ -0,0 +1,333 @@
use core::fmt::Write;
use embedded_graphics::{
geometry::{Point, Size},
pixelcolor::Gray8,
prelude::{DrawTarget, OriginDimensions},
};
use heapless::String;
use crate::input::InputEvent;
use crate::ui::{app::AppCommand, frame::Frame, RefreshHint};
use super::ScreenUpdate;
const BUTTON_COUNT: usize = 6;
const BUTTONS: [ButtonKind; BUTTON_COUNT] = [
ButtonKind::Up,
ButtonKind::Down,
ButtonKind::Select,
ButtonKind::Back,
ButtonKind::SelectLong,
ButtonKind::BackLong,
];
const LIGHT_BUTTON_LEFT: u16 = 264;
const LIGHT_BUTTON_TOP: u16 = 146;
const LIGHT_BUTTON_RIGHT: u16 = 432;
const LIGHT_BUTTON_BOTTOM: u16 = 242;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct DemoScreen {
button_counts: [u16; BUTTON_COUNT],
touch_count: u16,
last_touch: Option<TouchPoint>,
last_button: Option<ButtonKind>,
last_event: LastEvent,
light_on: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct TouchPoint {
x: u16,
y: u16,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum LastEvent {
Startup,
Button(ButtonKind),
Touch(TouchPoint),
LightToggle { on: bool },
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ButtonKind {
Up,
Down,
Select,
Back,
SelectLong,
BackLong,
}
impl DemoScreen {
pub const fn new() -> Self {
Self {
button_counts: [0; BUTTON_COUNT],
touch_count: 0,
last_touch: None,
last_button: None,
last_event: LastEvent::Startup,
light_on: true,
}
}
pub fn update(&mut self, event: InputEvent) -> ScreenUpdate {
match event {
InputEvent::Up => self.record_button(ButtonKind::Up),
InputEvent::Down => self.record_button(ButtonKind::Down),
InputEvent::Select => self.record_button(ButtonKind::Select),
InputEvent::Back => self.record_button(ButtonKind::Back),
InputEvent::SelectLong => self.record_button(ButtonKind::SelectLong),
InputEvent::BackLong => self.record_button(ButtonKind::BackLong),
InputEvent::Touch { x, y } => {
let touch = TouchPoint { x, y };
self.record_touch(touch);
if Self::light_button_contains(touch) {
self.toggle_light();
return ScreenUpdate::with_command(
true,
RefreshHint::Fast,
AppCommand::SetFrontLight { on: self.light_on },
);
}
}
}
ScreenUpdate::new(true, RefreshHint::Fast)
}
pub fn render<D>(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error>
where
D: DrawTarget<Color = Gray8> + OriginDimensions,
{
frame.title("Kinda UI demo")?;
frame.small(64, "Button and touch input demo.")?;
self.render_buttons(frame)?;
self.render_touch_panel(frame)?;
self.render_light_button(frame)?;
self.render_last_event(frame)?;
self.render_event_meter(frame)?;
frame.footer("Press buttons or tap the display")
}
fn record_button(&mut self, button: ButtonKind) {
let index = button.index();
self.button_counts[index] = self.button_counts[index].saturating_add(1);
self.last_button = Some(button);
self.last_event = LastEvent::Button(button);
}
fn record_touch(&mut self, touch: TouchPoint) {
self.touch_count = self.touch_count.saturating_add(1);
self.last_touch = Some(touch);
self.last_button = None;
self.last_event = LastEvent::Touch(touch);
}
fn toggle_light(&mut self) {
self.light_on = !self.light_on;
self.last_event = LastEvent::LightToggle { on: self.light_on };
}
fn light_button_contains(touch: TouchPoint) -> bool {
touch.x >= LIGHT_BUTTON_LEFT
&& touch.x <= LIGHT_BUTTON_RIGHT
&& touch.y >= LIGHT_BUTTON_TOP
&& touch.y <= LIGHT_BUTTON_BOTTOM
}
fn render_buttons<D>(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error>
where
D: DrawTarget<Color = Gray8> + OriginDimensions,
{
frame.body(106, "Buttons")?;
let mut y = 146;
for button in BUTTONS {
self.render_button_row(frame, y, button)?;
y += 34;
}
Ok(())
}
fn render_button_row<D>(
&self,
frame: &mut Frame<'_, D>,
y: i32,
button: ButtonKind,
) -> Result<(), D::Error>
where
D: DrawTarget<Color = Gray8> + OriginDimensions,
{
let marker = if self.last_button == Some(button) {
">"
} else {
" "
};
let mut row = String::<48>::new();
let _ = write!(
row,
"{} {:<12} {:>3}",
marker,
button.label(),
self.button_counts[button.index()]
);
frame.small(y, row.as_str())
}
fn render_light_button<D>(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error>
where
D: DrawTarget<Color = Gray8> + OriginDimensions,
{
let black = Gray8::new(0);
let white = Gray8::new(255);
frame.filled_rect(
Point::new(i32::from(LIGHT_BUTTON_LEFT), i32::from(LIGHT_BUTTON_TOP)),
Size::new(
u32::from(LIGHT_BUTTON_RIGHT - LIGHT_BUTTON_LEFT),
u32::from(LIGHT_BUTTON_BOTTOM - LIGHT_BUTTON_TOP),
),
white,
)?;
frame.outline_rect(
Point::new(i32::from(LIGHT_BUTTON_LEFT), i32::from(LIGHT_BUTTON_TOP)),
Size::new(
u32::from(LIGHT_BUTTON_RIGHT - LIGHT_BUTTON_LEFT),
u32::from(LIGHT_BUTTON_BOTTOM - LIGHT_BUTTON_TOP),
),
black,
2,
)?;
frame.small_at(Point::new(284, 164), "Front light")?;
let label = if self.light_on { "On" } else { "Off" };
frame.body_at(Point::new(284, 198), label)
}
fn render_touch_panel<D>(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error>
where
D: DrawTarget<Color = Gray8> + OriginDimensions,
{
let black = Gray8::new(0);
frame.body(378, "Touch")?;
frame.outline_rect(Point::new(48, 420), Size::new(384, 228), black, 2)?;
frame.line(Point::new(48, 534), Point::new(432, 534), black)?;
frame.line(Point::new(240, 420), Point::new(240, 648), black)?;
frame.small_at(Point::new(70, 456), "Tap/click")?;
frame.small_at(Point::new(70, 486), "anywhere")?;
frame.small_at(Point::new(262, 456), "Last touch")?;
let mut touch_line = String::<64>::new();
if let Some(touch) = self.last_touch {
let _ = write!(touch_line, "{}, {}", touch.x, touch.y);
self.render_crosshair(frame, touch)?;
} else {
let _ = touch_line.push_str("none yet");
}
frame.small_at(Point::new(262, 490), touch_line.as_str())?;
let mut count_line = String::<48>::new();
let _ = write!(count_line, "Touches: {}", self.touch_count);
frame.small_at(Point::new(70, 576), count_line.as_str())
}
fn render_crosshair<D>(
&self,
frame: &mut Frame<'_, D>,
touch: TouchPoint,
) -> Result<(), D::Error>
where
D: DrawTarget<Color = Gray8> + OriginDimensions,
{
let black = Gray8::new(0);
let x = i32::from(touch.x);
let y = i32::from(touch.y);
frame.line(Point::new(x - 18, y), Point::new(x - 6, y), black)?;
frame.line(Point::new(x + 6, y), Point::new(x + 18, y), black)?;
frame.line(Point::new(x, y - 18), Point::new(x, y - 6), black)?;
frame.line(Point::new(x, y + 6), Point::new(x, y + 18), black)?;
frame.circle(Point::new(x - 5, y - 5), 10, black)
}
fn render_last_event<D>(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error>
where
D: DrawTarget<Color = Gray8> + OriginDimensions,
{
let mut line = String::<96>::new();
match self.last_event {
LastEvent::Startup => {
let _ = line.push_str("Last event: waiting for input");
}
LastEvent::Button(button) => {
let _ = write!(
line,
"Last event: {} button, count {}",
button.label(),
self.button_counts[button.index()]
);
}
LastEvent::Touch(touch) => {
let _ = write!(line, "Last event: touch at {}, {}", touch.x, touch.y);
}
LastEvent::LightToggle { on } => {
let state = if on { "on" } else { "off" };
let _ = write!(line, "Last event: front light {}", state);
}
}
frame.small(684, line.as_str())
}
fn render_event_meter<D>(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error>
where
D: DrawTarget<Color = Gray8> + OriginDimensions,
{
let black = Gray8::new(0);
let origin = Point::new(48, 724);
let size = Size::new(384, 18);
frame.outline_rect(origin, size, black, 2)?;
let filled = ((self.total_events() % 41) * 380) / 40;
if filled > 0 {
frame.filled_rect(Point::new(50, 726), Size::new(filled, 14), black)?;
}
Ok(())
}
fn total_events(&self) -> u32 {
let mut total = u32::from(self.touch_count);
for count in self.button_counts {
total = total.saturating_add(u32::from(count));
}
total
}
}
impl ButtonKind {
const fn index(self) -> usize {
match self {
Self::Up => 0,
Self::Down => 1,
Self::Select => 2,
Self::Back => 3,
Self::SelectLong => 4,
Self::BackLong => 5,
}
}
const fn label(self) -> &'static str {
match self {
Self::Up => "Up",
Self::Down => "Down",
Self::Select => "Select",
Self::Back => "Back",
Self::SelectLong => "Hold select",
Self::BackLong => "Hold back",
}
}
}

View File

@ -1,86 +0,0 @@
use core::fmt::Write;
use embedded_graphics::{
pixelcolor::Gray8,
prelude::{DrawTarget, OriginDimensions},
};
use heapless::String;
use crate::input::InputEvent;
use super::ScreenUpdate;
use crate::ui::{books::BOOKS, frame::Frame, RefreshHint};
pub(crate) struct LibraryScreen {
selected: usize,
}
#[derive(Clone, Copy)]
pub(crate) enum LibraryAction {
None,
OpenOnboarding,
OpenReader { book_index: usize },
}
impl LibraryScreen {
pub fn new() -> Self {
Self { selected: 0 }
}
pub fn new_with_selected(selected: usize) -> Self {
Self {
selected: selected.min(BOOKS.len() - 1),
}
}
pub fn update(&mut self, event: InputEvent) -> ScreenUpdate<LibraryAction> {
match event {
InputEvent::Up => {
self.selected = if self.selected == 0 {
BOOKS.len() - 1
} else {
self.selected - 1
};
ScreenUpdate::new(true, RefreshHint::Fast, LibraryAction::None)
}
InputEvent::Down => {
self.selected = (self.selected + 1) % BOOKS.len();
ScreenUpdate::new(true, RefreshHint::Fast, LibraryAction::None)
}
InputEvent::Select => ScreenUpdate::new(
true,
RefreshHint::Full,
LibraryAction::OpenReader {
book_index: self.selected,
},
),
InputEvent::Back => {
ScreenUpdate::new(true, RefreshHint::Full, LibraryAction::OpenOnboarding)
}
_ => ScreenUpdate::new(false, RefreshHint::None, LibraryAction::None),
}
}
pub fn render<D>(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error>
where
D: DrawTarget<Color = Gray8> + OriginDimensions,
{
frame.title("Library")?;
frame.small(58, "Mock book list")?;
let mut y = 100;
for (index, book) in BOOKS.iter().enumerate() {
let marker = if index == self.selected { ">" } else { " " };
let mut row = String::<96>::new();
let _ = write!(row, "{} {} - {}", marker, book.title, book.author);
frame.body(y, row.as_str())?;
y += 38;
}
frame.small(
274,
"Book sync and storage will plug in behind this screen.",
)?;
frame.footer("Up/Down move Select open Back WiFi")
}
}

View File

@ -1,31 +1,26 @@
pub(crate) mod library; pub(crate) mod demo;
pub(crate) mod onboarding;
pub(crate) mod reader;
use super::{app::AppCommand, RefreshHint}; use super::{app::AppCommand, RefreshHint};
pub(crate) struct ScreenUpdate<A> { pub(crate) struct ScreenUpdate {
pub dirty: bool, pub dirty: bool,
pub refresh: RefreshHint, pub refresh: RefreshHint,
pub action: A,
pub command: Option<AppCommand>, pub command: Option<AppCommand>,
} }
impl<A> ScreenUpdate<A> { impl ScreenUpdate {
pub fn new(dirty: bool, refresh: RefreshHint, action: A) -> Self { pub const fn new(dirty: bool, refresh: RefreshHint) -> Self {
Self { Self {
dirty, dirty,
refresh, refresh,
action,
command: None, command: None,
} }
} }
pub fn with_command(dirty: bool, refresh: RefreshHint, action: A, command: AppCommand) -> Self { pub const fn with_command(dirty: bool, refresh: RefreshHint, command: AppCommand) -> Self {
Self { Self {
dirty, dirty,
refresh, refresh,
action,
command: Some(command), command: Some(command),
} }
} }

View File

@ -1,495 +0,0 @@
use core::fmt::Write;
use embedded_graphics::{
pixelcolor::Gray8,
prelude::{DrawTarget, OriginDimensions},
};
use heapless::String;
use crate::{
input::InputEvent,
ui::app::AppCommand,
wifi::{WifiAccessPoint, WifiEvent, WifiNetworkList, WifiPassword, WifiSsid},
};
use super::ScreenUpdate;
use crate::ui::{frame::Frame, RefreshHint};
const PASSWORD_CHARS: &[u8] =
b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 .-_!?@#$%&*+/=:";
pub(crate) struct OnboardingScreen {
state: OnboardingState,
}
enum OnboardingState {
ScanIntro { notice: &'static str },
Scanning,
NetworkList(NetworkListState),
PasswordEntry(PasswordEntryState),
Joining(JoiningState),
SavedWifiConnecting { ssid: WifiSsid },
Joined { ssid: WifiSsid },
}
struct NetworkListState {
networks: WifiNetworkList,
selected: usize,
notice: &'static str,
}
struct PasswordEntryState {
networks: WifiNetworkList,
selected: usize,
password: WifiPassword,
char_index: usize,
notice: &'static str,
}
struct JoiningState {
networks: WifiNetworkList,
selected: usize,
password: WifiPassword,
}
#[derive(Clone, Copy)]
pub(crate) enum OnboardingAction {
None,
OpenLibrary,
}
impl OnboardingScreen {
pub fn new() -> Self {
Self {
state: OnboardingState::ScanIntro { notice: "" },
}
}
pub fn new_connecting_saved(ssid: WifiSsid) -> Self {
Self {
state: OnboardingState::SavedWifiConnecting { ssid },
}
}
pub fn update(&mut self, event: InputEvent) -> ScreenUpdate<OnboardingAction> {
match &mut self.state {
OnboardingState::ScanIntro { .. } => match event {
InputEvent::Select => {
self.state = OnboardingState::Scanning;
ScreenUpdate::with_command(
true,
RefreshHint::Full,
OnboardingAction::None,
AppCommand::ScanWifi,
)
}
_ => ScreenUpdate::new(false, RefreshHint::None, OnboardingAction::None),
},
OnboardingState::Scanning => {
ScreenUpdate::new(false, RefreshHint::None, OnboardingAction::None)
}
OnboardingState::NetworkList(state) => match event {
InputEvent::Up => {
state.selected = if state.selected == 0 {
state.networks.len() - 1
} else {
state.selected - 1
};
state.notice = "";
ScreenUpdate::new(true, RefreshHint::Fast, OnboardingAction::None)
}
InputEvent::Down => {
state.selected = (state.selected + 1) % state.networks.len();
state.notice = "";
ScreenUpdate::new(true, RefreshHint::Fast, OnboardingAction::None)
}
InputEvent::Select => {
let access_point = state.networks[state.selected].clone();
if access_point.is_locked() {
self.state = OnboardingState::PasswordEntry(PasswordEntryState::new(
state.networks.clone(),
state.selected,
));
ScreenUpdate::new(true, RefreshHint::Full, OnboardingAction::None)
} else {
let password = WifiPassword::new();
self.state = OnboardingState::Joining(JoiningState {
networks: state.networks.clone(),
selected: state.selected,
password: password.clone(),
});
ScreenUpdate::with_command(
true,
RefreshHint::Full,
OnboardingAction::None,
AppCommand::JoinWifi {
access_point,
password,
},
)
}
}
InputEvent::Back | InputEvent::SelectLong => {
self.state = OnboardingState::Scanning;
ScreenUpdate::with_command(
true,
RefreshHint::Full,
OnboardingAction::None,
AppCommand::ScanWifi,
)
}
_ => ScreenUpdate::new(false, RefreshHint::None, OnboardingAction::None),
},
OnboardingState::PasswordEntry(state) => match event {
InputEvent::Up => {
state.previous_char();
ScreenUpdate::new(true, RefreshHint::Fast, OnboardingAction::None)
}
InputEvent::Down => {
state.next_char();
ScreenUpdate::new(true, RefreshHint::Fast, OnboardingAction::None)
}
InputEvent::Select => {
state.append_char();
ScreenUpdate::new(true, RefreshHint::Fast, OnboardingAction::None)
}
InputEvent::Back => {
state.delete_char();
ScreenUpdate::new(true, RefreshHint::Fast, OnboardingAction::None)
}
InputEvent::SelectLong => {
if state.can_join() {
let access_point = state.access_point().clone();
let password = state.password.clone();
self.state = OnboardingState::Joining(JoiningState {
networks: state.networks.clone(),
selected: state.selected,
password: password.clone(),
});
ScreenUpdate::with_command(
true,
RefreshHint::Full,
OnboardingAction::None,
AppCommand::JoinWifi {
access_point,
password,
},
)
} else {
state.notice = "Password must be 8-63 characters";
ScreenUpdate::new(true, RefreshHint::Fast, OnboardingAction::None)
}
}
InputEvent::BackLong => {
self.state = OnboardingState::NetworkList(NetworkListState {
networks: state.networks.clone(),
selected: state.selected,
notice: "",
});
ScreenUpdate::new(true, RefreshHint::Full, OnboardingAction::None)
}
},
OnboardingState::Joining(_) | OnboardingState::SavedWifiConnecting { .. } => {
ScreenUpdate::new(false, RefreshHint::None, OnboardingAction::None)
}
OnboardingState::Joined { .. } => match event {
InputEvent::Select => {
ScreenUpdate::new(true, RefreshHint::Full, OnboardingAction::OpenLibrary)
}
InputEvent::Back => {
self.state = OnboardingState::ScanIntro { notice: "" };
ScreenUpdate::new(true, RefreshHint::Full, OnboardingAction::None)
}
_ => ScreenUpdate::new(false, RefreshHint::None, OnboardingAction::None),
},
}
}
pub fn handle_wifi_event(&mut self, event: WifiEvent) -> ScreenUpdate<OnboardingAction> {
match event {
WifiEvent::ScanFinished(Ok(networks)) => {
if networks.is_empty() {
self.state = OnboardingState::ScanIntro {
notice: "No networks found",
};
} else {
self.state = OnboardingState::NetworkList(NetworkListState {
networks,
selected: 0,
notice: "",
});
}
ScreenUpdate::new(true, RefreshHint::Full, OnboardingAction::None)
}
WifiEvent::ScanFinished(Err(error)) => {
self.state = OnboardingState::ScanIntro {
notice: error.message(),
};
ScreenUpdate::new(true, RefreshHint::Full, OnboardingAction::None)
}
WifiEvent::JoinFinished(Ok(ssid)) => {
let action = if matches!(self.state, OnboardingState::SavedWifiConnecting { .. }) {
OnboardingAction::OpenLibrary
} else {
OnboardingAction::None
};
self.state = OnboardingState::Joined { ssid };
ScreenUpdate::new(true, RefreshHint::Full, action)
}
WifiEvent::JoinFinished(Err(error)) => {
let previous =
core::mem::replace(&mut self.state, OnboardingState::ScanIntro { notice: "" });
self.state = match previous {
OnboardingState::Joining(state) => {
OnboardingState::PasswordEntry(PasswordEntryState {
networks: state.networks,
selected: state.selected,
password: state.password,
char_index: 0,
notice: error.message(),
})
}
_ => OnboardingState::ScanIntro {
notice: error.message(),
},
};
ScreenUpdate::new(true, RefreshHint::Full, OnboardingAction::None)
}
}
}
pub fn render<D>(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error>
where
D: DrawTarget<Color = Gray8> + OriginDimensions,
{
match &self.state {
OnboardingState::ScanIntro { notice } => self.render_scan_intro(frame, notice),
OnboardingState::Scanning => self.render_scanning(frame),
OnboardingState::NetworkList(state) => self.render_network_list(frame, state),
OnboardingState::PasswordEntry(state) => self.render_password_entry(frame, state),
OnboardingState::Joining(state) => self.render_joining(frame, state),
OnboardingState::SavedWifiConnecting { ssid } => {
self.render_saved_wifi_connecting(frame, ssid)
}
OnboardingState::Joined { ssid } => self.render_joined(frame, ssid),
}
}
fn render_scan_intro<D>(
&self,
frame: &mut Frame<'_, D>,
notice: &'static str,
) -> Result<(), D::Error>
where
D: DrawTarget<Color = Gray8> + OriginDimensions,
{
frame.title("WiFi setup")?;
frame.body(86, "Connect this device before loading books.")?;
frame.body(122, "Press select to scan for nearby networks.")?;
if !notice.is_empty() {
frame.small(174, notice)?;
}
frame.footer("Select: scan")
}
fn render_scanning<D>(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error>
where
D: DrawTarget<Color = Gray8> + OriginDimensions,
{
frame.title("Scanning")?;
frame.body(96, "Looking for WiFi networks...")?;
frame.small(148, "This can take a few seconds.")?;
frame.footer("Please wait")
}
fn render_network_list<D>(
&self,
frame: &mut Frame<'_, D>,
state: &NetworkListState,
) -> Result<(), D::Error>
where
D: DrawTarget<Color = Gray8> + OriginDimensions,
{
frame.title("Choose network")?;
let mut count_line = String::<48>::new();
let _ = write!(count_line, "{} networks found", state.networks.len());
frame.small(58, count_line.as_str())?;
let visible_rows = 8;
let first_row = if state.selected >= visible_rows {
state.selected + 1 - visible_rows
} else {
0
};
let last_row = (first_row + visible_rows).min(state.networks.len());
let mut y = 100;
for index in first_row..last_row {
let network = &state.networks[index];
let marker = if index == state.selected { ">" } else { " " };
let mut row = String::<128>::new();
let _ = write!(
row,
"{} {} {} {}",
marker,
network.ssid.as_str(),
network.signal_label(),
network.security.label()
);
frame.body(y, row.as_str())?;
y += 34;
}
if state.networks.len() > visible_rows {
let mut range_line = String::<48>::new();
let _ = write!(range_line, "Showing {}-{}", first_row + 1, last_row);
frame.small(380, range_line.as_str())?;
}
if !state.notice.is_empty() {
frame.small(400, state.notice)?;
}
frame.footer("Up/Down move Select choose Back rescan")
}
fn render_password_entry<D>(
&self,
frame: &mut Frame<'_, D>,
state: &PasswordEntryState,
) -> Result<(), D::Error>
where
D: DrawTarget<Color = Gray8> + OriginDimensions,
{
let access_point = state.access_point();
let mut network_line = String::<96>::new();
let _ = write!(network_line, "Network: {}", access_point.ssid.as_str());
frame.title("WiFi password")?;
frame.body(66, network_line.as_str())?;
frame.body(112, "Password:")?;
if state.password.is_empty() {
frame.body(146, "<empty>")?;
} else {
frame.body(146, state.password.as_str())?;
}
frame.body(204, "Current char:")?;
let mut char_line = String::<24>::new();
if state.current_char() == ' ' {
let _ = write!(char_line, "[ space ]");
} else {
let _ = write!(char_line, "[ {} ]", state.current_char());
}
frame.body(238, char_line.as_str())?;
if !state.notice.is_empty() {
frame.small(292, state.notice)?;
}
frame.footer("Up/Down char Select add Back del Hold Select join")
}
fn render_joining<D>(
&self,
frame: &mut Frame<'_, D>,
state: &JoiningState,
) -> Result<(), D::Error>
where
D: DrawTarget<Color = Gray8> + OriginDimensions,
{
let network = &state.networks[state.selected];
let mut network_line = String::<96>::new();
let _ = write!(network_line, "Joining {}", network.ssid.as_str());
frame.title("Joining")?;
frame.body(92, network_line.as_str())?;
frame.body(136, "Waiting for the access point...")?;
frame.footer("Please wait")
}
fn render_saved_wifi_connecting<D>(
&self,
frame: &mut Frame<'_, D>,
ssid: &WifiSsid,
) -> Result<(), D::Error>
where
D: DrawTarget<Color = Gray8> + OriginDimensions,
{
let mut network_line = String::<96>::new();
let _ = write!(network_line, "Connecting to {}", ssid.as_str());
frame.title("WiFi")?;
frame.body(92, network_line.as_str())?;
frame.body(136, "Using saved credentials...")?;
frame.footer("Please wait")
}
fn render_joined<D>(&self, frame: &mut Frame<'_, D>, ssid: &WifiSsid) -> Result<(), D::Error>
where
D: DrawTarget<Color = Gray8> + OriginDimensions,
{
let mut network_line = String::<96>::new();
let _ = write!(network_line, "Connected to {}", ssid.as_str());
frame.title("Connected")?;
frame.body(92, network_line.as_str())?;
frame.body(136, "Next screen: library.")?;
frame.footer("Select continue Back scan again")
}
}
impl PasswordEntryState {
fn new(networks: WifiNetworkList, selected: usize) -> Self {
Self {
networks,
selected,
password: WifiPassword::new(),
char_index: 0,
notice: "",
}
}
fn access_point(&self) -> &WifiAccessPoint {
&self.networks[self.selected]
}
fn previous_char(&mut self) {
self.notice = "";
self.char_index = if self.char_index == 0 {
PASSWORD_CHARS.len() - 1
} else {
self.char_index - 1
};
}
fn next_char(&mut self) {
self.notice = "";
self.char_index = (self.char_index + 1) % PASSWORD_CHARS.len();
}
fn append_char(&mut self) {
self.notice = "";
if self.password.push(self.current_char()).is_err() {
self.notice = "Password is full";
}
}
fn delete_char(&mut self) {
self.notice = "";
if self.password.pop().is_none() {
self.notice = "Nothing to delete";
}
}
fn can_join(&self) -> bool {
!self.access_point().is_locked() || (self.password.len() >= 8 && self.password.len() <= 63)
}
fn current_char(&self) -> char {
char::from(PASSWORD_CHARS[self.char_index])
}
}

View File

@ -1,98 +0,0 @@
use core::fmt::Write;
use embedded_graphics::{
pixelcolor::Gray8,
prelude::{DrawTarget, OriginDimensions},
};
use heapless::String;
use crate::input::InputEvent;
use super::ScreenUpdate;
use crate::ui::{books::BOOKS, frame::Frame, RefreshHint};
pub(crate) struct ReaderScreen {
book_index: usize,
page_index: usize,
show_help: bool,
}
#[derive(Clone, Copy)]
pub(crate) enum ReaderAction {
None,
OpenLibrary { selected_book: usize },
}
impl ReaderScreen {
pub fn new(book_index: usize) -> Self {
Self {
book_index,
page_index: 0,
show_help: false,
}
}
pub fn update(&mut self, event: InputEvent) -> ScreenUpdate<ReaderAction> {
match event {
InputEvent::Up => {
if self.page_index > 0 {
self.page_index -= 1;
self.show_help = false;
ScreenUpdate::new(true, RefreshHint::Fast, ReaderAction::None)
} else {
ScreenUpdate::new(false, RefreshHint::None, ReaderAction::None)
}
}
InputEvent::Down => {
let last_page = BOOKS[self.book_index].pages.len() - 1;
if self.page_index < last_page {
self.page_index += 1;
self.show_help = false;
ScreenUpdate::new(true, RefreshHint::Fast, ReaderAction::None)
} else {
ScreenUpdate::new(false, RefreshHint::None, ReaderAction::None)
}
}
InputEvent::Select => {
self.show_help = !self.show_help;
ScreenUpdate::new(true, RefreshHint::Fast, ReaderAction::None)
}
InputEvent::Back => ScreenUpdate::new(
true,
RefreshHint::Full,
ReaderAction::OpenLibrary {
selected_book: self.book_index,
},
),
_ => ScreenUpdate::new(false, RefreshHint::None, ReaderAction::None),
}
}
pub fn render<D>(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error>
where
D: DrawTarget<Color = Gray8> + OriginDimensions,
{
let book = &BOOKS[self.book_index];
let mut heading = String::<96>::new();
let _ = write!(heading, "{} - {}", book.title, book.author);
frame.title("Reader")?;
frame.small(58, heading.as_str())?;
frame.paragraph(100, book.pages[self.page_index])?;
let mut page_line = String::<48>::new();
let _ = write!(
page_line,
"Page {} of {}",
self.page_index + 1,
book.pages.len()
);
frame.small(392, page_line.as_str())?;
if self.show_help {
frame.small(346, "Controls: Up prev, Down next, Back library")?;
}
frame.footer("Up/Down page Select controls Back library")
}
}

View File

@ -1,219 +0,0 @@
use heapless::{String, Vec};
pub const MAX_NETWORKS: usize = 12;
pub const WIFI_CREDENTIAL_RECORD_BYTES: usize = 112;
const WIFI_CREDENTIAL_MAGIC: &[u8; 8] = b"KINDAWF1";
const WIFI_CREDENTIAL_CHECKSUM_OFFSET: usize = 8;
const WIFI_CREDENTIAL_SECURITY_OFFSET: usize = 12;
const WIFI_CREDENTIAL_SSID_LEN_OFFSET: usize = 13;
const WIFI_CREDENTIAL_PASSWORD_LEN_OFFSET: usize = 14;
const WIFI_CREDENTIAL_SSID_OFFSET: usize = 16;
const WIFI_CREDENTIAL_PASSWORD_OFFSET: usize = WIFI_CREDENTIAL_SSID_OFFSET + 32;
pub type WifiNetworkList = Vec<WifiAccessPoint, MAX_NETWORKS>;
pub type WifiPassword = String<64>;
pub type WifiSsid = String<32>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WifiAccessPoint {
pub ssid: WifiSsid,
pub bssid: [u8; 6],
pub channel: u8,
pub signal_strength: i8,
pub security: WifiSecurity,
}
impl WifiAccessPoint {
pub fn is_locked(&self) -> bool {
self.security.is_locked()
}
pub fn signal_label(&self) -> &'static str {
match self.signal_strength {
-50..=i8::MAX => "strong",
-70..=-51 => "medium",
_ => "weak",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WifiSecurity {
Open,
Wep,
Wpa,
Wpa2Personal,
WpaWpa2Personal,
Wpa2Wpa3Personal,
Wpa3Personal,
Other,
}
impl WifiSecurity {
pub fn is_locked(self) -> bool {
!matches!(self, Self::Open)
}
pub fn label(self) -> &'static str {
match self {
Self::Open => "open",
Self::Wep => "wep",
Self::Wpa => "wpa",
Self::Wpa2Personal => "wpa2",
Self::WpaWpa2Personal => "wpa/wpa2",
Self::Wpa2Wpa3Personal => "wpa2/wpa3",
Self::Wpa3Personal => "wpa3",
Self::Other => "locked",
}
}
fn to_storage_byte(self) -> u8 {
match self {
Self::Open => 0,
Self::Wep => 1,
Self::Wpa => 2,
Self::Wpa2Personal => 3,
Self::WpaWpa2Personal => 4,
Self::Wpa2Wpa3Personal => 5,
Self::Wpa3Personal => 6,
Self::Other => 7,
}
}
fn from_storage_byte(value: u8) -> Option<Self> {
match value {
0 => Some(Self::Open),
1 => Some(Self::Wep),
2 => Some(Self::Wpa),
3 => Some(Self::Wpa2Personal),
4 => Some(Self::WpaWpa2Personal),
5 => Some(Self::Wpa2Wpa3Personal),
6 => Some(Self::Wpa3Personal),
7 => Some(Self::Other),
_ => None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WifiCredentials {
pub ssid: WifiSsid,
pub password: WifiPassword,
pub security: WifiSecurity,
}
impl WifiCredentials {
pub fn new(ssid: WifiSsid, password: WifiPassword, security: WifiSecurity) -> Self {
Self {
ssid,
password,
security,
}
}
pub fn from_access_point(access_point: &WifiAccessPoint, password: WifiPassword) -> Self {
Self::new(access_point.ssid.clone(), password, access_point.security)
}
pub fn encode(&self) -> [u8; WIFI_CREDENTIAL_RECORD_BYTES] {
let mut record = [0u8; WIFI_CREDENTIAL_RECORD_BYTES];
record[..WIFI_CREDENTIAL_MAGIC.len()].copy_from_slice(WIFI_CREDENTIAL_MAGIC);
record[WIFI_CREDENTIAL_SECURITY_OFFSET] = self.security.to_storage_byte();
record[WIFI_CREDENTIAL_SSID_LEN_OFFSET] = self.ssid.len() as u8;
record[WIFI_CREDENTIAL_PASSWORD_LEN_OFFSET] = self.password.len() as u8;
record[WIFI_CREDENTIAL_SSID_OFFSET..][..self.ssid.len()]
.copy_from_slice(self.ssid.as_bytes());
record[WIFI_CREDENTIAL_PASSWORD_OFFSET..][..self.password.len()]
.copy_from_slice(self.password.as_bytes());
let checksum = Self::checksum(&record[WIFI_CREDENTIAL_SECURITY_OFFSET..]);
record[WIFI_CREDENTIAL_CHECKSUM_OFFSET..][..4].copy_from_slice(&checksum.to_le_bytes());
record
}
pub fn decode(record: &[u8]) -> Option<Self> {
if record.len() < WIFI_CREDENTIAL_RECORD_BYTES {
return None;
}
if &record[..WIFI_CREDENTIAL_MAGIC.len()] != WIFI_CREDENTIAL_MAGIC {
return None;
}
let expected_checksum = u32::from_le_bytes(
record[WIFI_CREDENTIAL_CHECKSUM_OFFSET..][..4]
.try_into()
.ok()?,
);
let actual_checksum = Self::checksum(&record[WIFI_CREDENTIAL_SECURITY_OFFSET..]);
if expected_checksum != actual_checksum {
return None;
}
let security = WifiSecurity::from_storage_byte(record[WIFI_CREDENTIAL_SECURITY_OFFSET])?;
let ssid_len = usize::from(record[WIFI_CREDENTIAL_SSID_LEN_OFFSET]);
let password_len = usize::from(record[WIFI_CREDENTIAL_PASSWORD_LEN_OFFSET]);
if ssid_len > 32 || password_len > 64 {
return None;
}
let ssid_text =
core::str::from_utf8(&record[WIFI_CREDENTIAL_SSID_OFFSET..][..ssid_len]).ok()?;
let password_text =
core::str::from_utf8(&record[WIFI_CREDENTIAL_PASSWORD_OFFSET..][..password_len])
.ok()?;
let mut ssid = WifiSsid::new();
ssid.push_str(ssid_text).ok()?;
let mut password = WifiPassword::new();
password.push_str(password_text).ok()?;
Some(Self {
ssid,
password,
security,
})
}
fn checksum(bytes: &[u8]) -> u32 {
let mut hash = 2_166_136_261u32;
for byte in bytes {
hash ^= u32::from(*byte);
hash = hash.wrapping_mul(16_777_619);
}
hash
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum WifiEvent {
ScanFinished(Result<WifiNetworkList, WifiFailure>),
JoinFinished(Result<WifiSsid, WifiFailure>),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WifiFailure {
ScanFailed,
JoinFailed,
InvalidCredentials,
OutOfMemory,
Unsupported,
NotConnected,
}
impl WifiFailure {
pub fn message(self) -> &'static str {
match self {
Self::ScanFailed => "WiFi scan failed",
Self::JoinFailed => "WiFi join failed",
Self::InvalidCredentials => "Invalid WiFi credentials",
Self::OutOfMemory => "WiFi ran out of memory",
Self::Unsupported => "WiFi operation unsupported",
Self::NotConnected => "WiFi is not connected",
}
}
}

View File

@ -10,25 +10,10 @@ options:
display_name: Enable allocations via the esp-alloc crate. display_name: Enable allocations via the esp-alloc crate.
help: esp-alloc comes with no stability guarantees at this time. help: esp-alloc comes with no stability guarantees at this time.
- !Option
name: wifi
display_name: Enable Wi-Fi via the esp-wifi crate.
help: esp-wifi comes with no stability guarantees at this time.
requires:
- alloc
- unstable-hal
chips:
- esp32
- esp32c2
- esp32c3
- esp32c6
- esp32s2
- esp32s3
- !Option - !Option
name: ble name: ble
display_name: Enable BLE via the esp-wifi crate. display_name: Enable BLE via the radio stack.
help: esp-wifi comes with no stability guarantees at this time. help: The radio stack comes with no stability guarantees at this time.
requires: requires:
- alloc - alloc
- unstable-hal - unstable-hal

View File

@ -19,7 +19,7 @@
<button type="button" data-event="4">Hold select</button> <button type="button" data-event="4">Hold select</button>
<button type="button" data-event="5">Hold back</button> <button type="button" data-event="5">Hold back</button>
</section> </section>
<p class="help">Keyboard: ArrowUp, ArrowDown, Enter, Escape or Backspace.</p> <p class="help">Keyboard: ArrowUp, ArrowDown, Enter, Escape or Backspace. Click or tap the display to send a touch.</p>
</main> </main>
<script type="module" src="./sim.js"></script> <script type="module" src="./sim.js"></script>
</body> </body>

View File

@ -9,6 +9,7 @@ const status = document.getElementById('status');
const buttons = Array.from(document.querySelectorAll('[data-event]')); const buttons = Array.from(document.querySelectorAll('[data-event]'));
let wasm; let wasm;
let lastPointerTime = -Infinity;
async function loadWasm() { async function loadWasm() {
const imports = {}; const imports = {};
@ -57,6 +58,67 @@ function sendInput(eventCode) {
render(); render();
} }
function sendTouch(clientX, clientY) {
if (!wasm) {
return;
}
const bounds = canvas.getBoundingClientRect();
const x = Math.max(
0,
Math.min(canvas.width - 1, Math.floor(((clientX - bounds.left) * canvas.width) / bounds.width))
);
const y = Math.max(
0,
Math.min(canvas.height - 1, Math.floor(((clientY - bounds.top) * canvas.height) / bounds.height))
);
wasm.kinda_sim_touch(x, y);
render();
}
function capturePointer(event) {
if (!canvas.setPointerCapture || event.pointerId === undefined) {
return;
}
try {
canvas.setPointerCapture(event.pointerId);
} catch (_) {
// Synthetic and some browser-generated events do not have an active pointer.
}
}
function handlePointerDown(event) {
event.preventDefault();
lastPointerTime = event.timeStamp;
capturePointer(event);
sendTouch(event.clientX, event.clientY);
}
function handleMouseDown(event) {
if (event.timeStamp - lastPointerTime < 50) {
return;
}
event.preventDefault();
sendTouch(event.clientX, event.clientY);
}
function handleTouchStart(event) {
if (event.timeStamp - lastPointerTime < 50) {
return;
}
const touch = event.changedTouches[0];
if (!touch) {
return;
}
event.preventDefault();
sendTouch(touch.clientX, touch.clientY);
}
function keyboardEventCode(event) { function keyboardEventCode(event) {
if (event.key === 'ArrowUp') { if (event.key === 'ArrowUp') {
return EVENT_UP; return EVENT_UP;
@ -82,6 +144,10 @@ buttons.forEach((button) => {
button.addEventListener('click', () => sendInput(Number(button.dataset.event))); button.addEventListener('click', () => sendInput(Number(button.dataset.event)));
}); });
canvas.addEventListener('pointerdown', handlePointerDown);
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('touchstart', handleTouchStart, { passive: false });
window.addEventListener('keydown', (event) => { window.addEventListener('keydown', (event) => {
const eventCode = keyboardEventCode(event); const eventCode = keyboardEventCode(event);
if (eventCode === null) { if (eventCode === null) {
@ -100,7 +166,7 @@ try {
buttons.forEach((button) => { buttons.forEach((button) => {
button.disabled = false; button.disabled = false;
}); });
status.textContent = 'Ready. Select scans with mock WiFi results.'; status.textContent = 'Ready. Press buttons or click/touch the display.';
} catch (error) { } catch (error) {
console.error(error); console.error(error);
status.textContent = error instanceof Error ? error.message : 'Could not load simulator.'; status.textContent = error instanceof Error ? error.message : 'Could not load simulator.';

View File

@ -12,7 +12,7 @@ main {
canvas { canvas {
display: block; display: block;
width: 100%; width: 100%;
max-width: 800px; max-width: 480px;
height: auto; height: auto;
border: 1px solid #222222; border: 1px solid #222222;
background: #f8f8f8; background: #f8f8f8;