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]
# 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"]
[env]

494
Cargo.lock generated
View File

@ -2,15 +2,6 @@
# It is not intended for manual editing.
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]]
name = "aho-corasick"
version = "1.1.4"
@ -85,36 +76,6 @@ dependencies = [
"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]]
name = "bytemuck"
version = "1.25.0"
@ -137,33 +98,12 @@ dependencies = [
"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]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "const-default"
version = "1.0.0"
@ -196,15 +136,6 @@ dependencies = [
"typenum",
]
[[package]]
name = "ctr"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea"
dependencies = [
"cipher",
]
[[package]]
name = "darling"
version = "0.20.11"
@ -370,26 +301,6 @@ dependencies = [
"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]]
name = "document-features"
version = "0.2.11"
@ -466,29 +377,6 @@ dependencies = [
"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]]
name = "embassy-sync"
version = "0.6.2"
@ -532,22 +420,6 @@ dependencies = [
"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]]
name = "embassy-time"
version = "0.5.1"
@ -684,25 +556,6 @@ dependencies = [
"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]]
name = "embedded-storage"
version = "0.3.1"
@ -757,7 +610,7 @@ dependencies = [
"document-features",
"enumset",
"esp-config",
"esp-sync 0.2.1",
"esp-sync",
"linked_list_allocator",
"rlsf",
]
@ -772,7 +625,7 @@ dependencies = [
"defmt 1.0.1",
"document-features",
"esp-config",
"esp-metadata-generated 0.4.0",
"esp-metadata-generated",
"esp-println",
"heapless 0.9.3",
"riscv",
@ -789,8 +642,8 @@ dependencies = [
"document-features",
"embedded-storage",
"esp-config",
"esp-hal-procmacros 0.22.0",
"esp-metadata-generated 0.4.0",
"esp-hal-procmacros",
"esp-metadata-generated",
"esp-rom-sys",
"jiff",
"strum",
@ -803,7 +656,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d9b92fd9cfb0b4f8f1b6219b9763269a335571e307b014903b8201619374b80"
dependencies = [
"document-features",
"esp-metadata-generated 0.4.0",
"esp-metadata-generated",
"serde",
"serde_yaml",
"somni-expr",
@ -836,11 +689,11 @@ dependencies = [
"embedded-io-async 0.7.0",
"enumset",
"esp-config",
"esp-hal-procmacros 0.22.0",
"esp-metadata-generated 0.4.0",
"esp-hal-procmacros",
"esp-metadata-generated",
"esp-riscv-rt",
"esp-rom-sys",
"esp-sync 0.2.1",
"esp-sync",
"esp32",
"esp32c2",
"esp32c3",
@ -865,20 +718,6 @@ dependencies = [
"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]]
name = "esp-hal-procmacros"
version = "0.22.0"
@ -894,36 +733,12 @@ dependencies = [
"termcolor",
]
[[package]]
name = "esp-metadata-generated"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a93e39c8ad8d390d248dc7b9f4b59a873f313bf535218b8e2351356972399e3"
[[package]]
name = "esp-metadata-generated"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "esp-println"
version = "0.17.0"
@ -932,64 +747,12 @@ checksum = "42dee1e9ac7c3539bf6464db1707b0edd7557168f98278cf3c84fe70e63c6ce6"
dependencies = [
"defmt 1.0.1",
"document-features",
"esp-metadata-generated 0.4.0",
"esp-sync 0.2.1",
"esp-metadata-generated",
"esp-sync",
"log",
"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]]
name = "esp-riscv-rt"
version = "0.14.0"
@ -1010,7 +773,7 @@ checksum = "eae852ccb08971155023d1371c96d5490cbc26860f06aee2d629ef73f1a890c3"
dependencies = [
"cfg-if",
"document-features",
"esp-metadata-generated 0.4.0",
"esp-metadata-generated",
"esp32c6",
]
@ -1020,7 +783,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "551f90766e1527edaa0c91e8d559e9e2a60397b545e93357ac61fb31845e5712"
dependencies = [
"allocator-api2",
"cfg-if",
"defmt 1.0.1",
"document-features",
@ -1030,47 +792,15 @@ dependencies = [
"embassy-time-queue-utils",
"esp-config",
"esp-hal",
"esp-hal-procmacros 0.22.0",
"esp-metadata-generated 0.4.0",
"esp-radio-rtos-driver",
"esp-hal-procmacros",
"esp-metadata-generated",
"esp-rom-sys",
"esp-sync 0.2.1",
"esp-sync",
"portable-atomic",
"riscv",
"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]]
name = "esp-sync"
version = "0.2.1"
@ -1083,74 +813,11 @@ dependencies = [
"embassy-sync 0.6.2",
"embassy-sync 0.7.2",
"embassy-sync 0.8.0",
"esp-metadata-generated 0.4.0",
"esp-metadata-generated",
"riscv",
"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]]
name = "esp32"
version = "0.40.2"
@ -1278,16 +945,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "futures-sink"
version = "0.3.31"
@ -1343,15 +1000,6 @@ dependencies = [
"version_check",
]
[[package]]
name = "hash32"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
dependencies = [
"byteorder",
]
[[package]]
name = "hash32"
version = "0.3.1"
@ -1361,17 +1009,6 @@ dependencies = [
"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]]
name = "hashbrown"
version = "0.17.1"
@ -1384,7 +1021,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
dependencies = [
"hash32 0.3.1",
"hash32",
"stable_deref_trait",
]
@ -1394,8 +1031,7 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4"
dependencies = [
"defmt 1.0.1",
"hash32 0.3.1",
"hash32",
"stable_deref_trait",
]
@ -1411,20 +1047,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "indexmap"
version = "2.14.0"
@ -1491,25 +1113,17 @@ dependencies = [
"critical-section",
"defmt 1.0.1",
"embassy-executor",
"embassy-futures",
"embassy-net",
"embassy-time 0.5.1",
"embassy-time",
"embedded-graphics",
"embedded-hal 1.0.0",
"embedded-io 0.6.1",
"embedded-io-async 0.6.1",
"embedded-storage",
"esp-alloc",
"esp-backtrace",
"esp-bootloader-esp-idf",
"esp-hal",
"esp-println",
"esp-radio",
"esp-rtos",
"esp-storage",
"heapless 0.8.0",
"lm3630a",
"smoltcp",
"ssd1677",
"static_cell",
]
@ -1548,15 +1162,6 @@ dependencies = [
"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]]
name = "log"
version = "0.4.27"
@ -1576,12 +1181,6 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "managed"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d"
[[package]]
name = "matchers"
version = "0.2.0"
@ -1627,17 +1226,6 @@ dependencies = [
"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]]
name = "num-traits"
version = "0.2.19"
@ -1695,27 +1283,6 @@ dependencies = [
"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]]
name = "proc-macro-crate"
version = "3.5.0"
@ -1891,12 +1458,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.228"
@ -1961,19 +1522,6 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "somni-expr"
version = "0.2.0"
@ -2041,12 +1589,6 @@ dependencies = [
"syn 2.0.101",
]
[[package]]
name = "subtle"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "svgbobdoc"
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"] }
ssd1677 = { path = "../ssd1677" }
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-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"
embassy-executor = { version = "0.10.0", features = ["defmt", "nightly"] }
embassy-time = { version = "0.5.0", features = ["generic-queue-8"] }
esp-rtos = { version = "0.3.0", features = [
"embassy",
"defmt",
"esp-radio",
"esp32c6",
] }
esp-radio = { version = "1.0.0-beta.0", features = [
"defmt",
"esp32c6",
"wifi",
] }
static_cell = { version = "2.1.0", features = ["nightly"] }
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 = [
"defmt-espflash",
"esp32c6",
@ -75,7 +48,6 @@ esp-backtrace = { version = "0.19.0", features = [
"defmt",
] }
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"] }
[profile.dev]
@ -83,9 +55,6 @@ lm3630a = { version = "0.1.1", features = ["async"] }
# For debug builds always builds with some optimization
opt-level = "s"
[profile.dev.package.esp-radio]
opt-level = 3
[profile.release]
codegen-units = 1 # LLVM can perform better optimizations using a single thread
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
wifi,data,nvs,0x9000,0x7000,
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")/.."
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
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]
#![feature(impl_trait_in_assoc_type)]
use alloc::string::String as AllocString;
use core::convert::Infallible;
use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
@ -14,10 +13,9 @@ use embedded_graphics::{
primitives::Rectangle,
};
use embedded_hal::delay::DelayNs;
use embedded_hal::i2c::I2c as EmbeddedI2c;
use embedded_hal::spi::{ErrorType, Operation, SpiBus, SpiDevice};
use embedded_storage::{ReadStorage, Storage};
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::i2c::master::{Config as I2cConfig, I2c};
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::{spi, Blocking};
use esp_println as _;
use esp_radio::wifi::{
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 kinda::{App, AppCommand, InputEvent, RefreshHint};
use lm3630a::{
Address, Bank, Brightness, Configuration, ControlConfig, FullScaleCurrent, Led2Bank, Lm3630a,
Mapping,
@ -554,18 +543,24 @@ const LONG_PRESS_TICKS: u16 = 32;
const MAX_FAST_DISPLAY_UPDATES: u8 = 12;
const PARTIAL_UPDATE_X_PADDING: u16 = 16;
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,
}
impl<'a, D> Rotated180<'a, D> {
impl<'a, D> RotatedMinus90<'a, D> {
fn new(target: &'a mut D) -> Self {
Self { target }
}
}
impl<D> DrawTarget for Rotated180<'_, D>
impl<D> DrawTarget for RotatedMinus90<'_, D>
where
D: DrawTarget + OriginDimensions,
{
@ -576,26 +571,30 @@ where
where
I: IntoIterator<Item = Pixel<Self::Color>>,
{
let size = self.target.size();
let width = size.width as i32;
let height = size.height as i32;
let logical_size = self.size();
let logical_width = logical_size.width as i32;
let logical_height = logical_size.height as i32;
self.target
.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;
}
Some(Pixel(
Point::new(width - 1 - point.x, height - 1 - point.y),
Point::new(logical_height - 1 - point.y, point.x),
color,
))
}))
}
fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> {
let size = self.target.size();
let bounds = Rectangle::new(Point::new(0, 0), size);
let logical_size = self.size();
let bounds = Rectangle::new(Point::new(0, 0), logical_size);
let drawable_area = area.intersection(&bounds);
if drawable_area.size.width == 0 || drawable_area.size.height == 0 {
@ -604,10 +603,12 @@ where
let rotated_area = Rectangle::new(
Point::new(
size.width as i32 - drawable_area.top_left.x - drawable_area.size.width as i32,
size.height as i32 - drawable_area.top_left.y - drawable_area.size.height as i32,
logical_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)
}
@ -617,12 +618,177 @@ where
}
}
impl<D> OriginDimensions for Rotated180<'_, D>
impl<D> OriginDimensions for RotatedMinus90<'_, D>
where
D: OriginDimensions,
{
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>(
display: &mut Ssd1677<SPI, DC, RST, BUSY>,
screen: &Framebuffer<'_>,
@ -1070,46 +1038,32 @@ async fn main(spawner: Spawner) {
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 button_bottom = Input::new(
peripherals.GPIO14,
peripherals.GPIO4,
InputConfig::default().with_pull(Pull::Up),
);
let button_left = Input::new(
peripherals.GPIO19,
peripherals.GPIO5,
InputConfig::default().with_pull(Pull::Up),
);
let button_right_up = Input::new(
peripherals.GPIO20,
peripherals.GPIO6,
InputConfig::default().with_pull(Pull::Up),
);
let button_right_down = Input::new(
peripherals.GPIO21,
peripherals.GPIO7,
InputConfig::default().with_pull(Pull::Up),
);
let mut display_pins = DisplayPins {
cs: Flex::new(peripherals.GPIO2),
dc: Flex::new(peripherals.GPIO3),
rst: Flex::new(peripherals.GPIO4),
busy: Flex::new(peripherals.GPIO5),
sck: Flex::new(peripherals.GPIO6),
mosi: Flex::new(peripherals.GPIO7),
cs: Flex::new(peripherals.GPIO20),
dc: Flex::new(peripherals.GPIO21),
rst: Flex::new(peripherals.GPIO22),
busy: Flex::new(peripherals.GPIO23),
sck: Flex::new(peripherals.GPIO19),
mosi: Flex::new(peripherals.GPIO18),
};
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 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();
Timer::after_millis(10).await;
@ -1157,8 +1111,8 @@ async fn main(spawner: Spawner) {
I2cConfig::default().with_frequency(Rate::from_khz(10)),
)
.expect("did not get I2C")
.with_sda(peripherals.GPIO22)
.with_scl(peripherals.GPIO23);
.with_sda(peripherals.GPIO3)
.with_scl(peripherals.GPIO2);
info!("Scanning I2C bus");
let mut devices_found = 0u8;
@ -1237,17 +1191,21 @@ async fn main(spawner: Spawner) {
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(
button_bottom,
button_left,
button_right_up,
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 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");
}
DISPLAY_BUSY_STATS.reset();
@ -1263,58 +1221,37 @@ async fn main(spawner: Spawner) {
.copy_to(previous_buf)
.expect("did not sync framebuffer");
let mut fast_update_count = 0u8;
let mut pending_command = app.take_pending_command();
loop {
if pending_command.is_none() {
if let Some(event) = buttons.poll() {
if let Some(event) = buttons.poll().or_else(|| touch.poll()) {
let update = app.update(event);
if update.dirty {
{
let mut threshold_screen = ThresholdFramebuffer::new(&mut screen);
let mut display_target = Rotated180::new(&mut threshold_screen);
app.render(&mut display_target).expect("did not render app");
}
present_display_update(
&mut display,
&screen,
previous_buf,
update.refresh,
&mut fast_update_count,
);
}
pending_command = update.command;
}
}
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");
if let Some(command) = update.command {
match command {
AppCommand::SetFrontLight { on } => {
if on {
backlight_enable.set_high();
Timer::after_millis(10).await;
if touch.set_front_light(true).is_ok() {
info!("Front light on");
} else {
defmt::error!("Failed to save WiFi credentials");
defmt::error!("Failed to turn front light on");
}
} else {
if touch.set_front_light(false).is_ok() {
info!("Front light off");
} else {
defmt::error!("Failed to turn front light off");
}
backlight_enable.set_low();
}
}
}
}
let update = app.handle_wifi_event(wifi_event);
if update.dirty {
{
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");
}
present_display_update(
@ -1325,7 +1262,6 @@ async fn main(spawner: Spawner) {
&mut fast_update_count,
);
}
pending_command = update.command;
}
Timer::after_millis(25).await;

View File

@ -10,13 +10,10 @@ use embedded_graphics::{
Pixel,
};
use kinda::{
App, AppCommand, AppUpdate, InputEvent, WifiAccessPoint, WifiCredentials, WifiEvent,
WifiNetworkList, WifiSecurity, WifiSsid,
};
use kinda::{App, AppUpdate, InputEvent};
const WIDTH: u32 = 800;
const HEIGHT: u32 = 480;
const WIDTH: u32 = 480;
const HEIGHT: u32 = 800;
const PIXEL_COUNT: usize = (WIDTH * HEIGHT) as usize;
const EVENT_UP: u32 = 0;
@ -68,88 +65,11 @@ impl WebSimulator {
}
fn apply_update(&mut self, update: AppUpdate) {
let mut dirty = 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 {
if update.dirty {
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) {
let app = self.app();
let mut display = PixelDisplay::new(PIXELS.get());
@ -251,6 +171,13 @@ pub extern "C" fn kinda_sim_input(event: u32) {
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]
pub extern "C" fn kinda_sim_width() -> u32 {
WIDTH

View File

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

View File

@ -2,11 +2,6 @@
pub mod input;
pub mod ui;
pub mod wifi;
pub use input::InputEvent;
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},
};
use crate::{input::InputEvent, wifi::WifiEvent, WifiAccessPoint, WifiCredentials, WifiPassword};
use crate::input::InputEvent;
use super::{
frame::Frame,
screens::{
library::{LibraryAction, LibraryScreen},
onboarding::{OnboardingAction, OnboardingScreen},
reader::{ReaderAction, ReaderScreen},
},
RefreshHint,
};
use super::{frame::Frame, screens::demo::DemoScreen, RefreshHint};
pub struct App {
route: Route,
pending_command: Option<AppCommand>,
}
enum Route {
Onboarding(OnboardingScreen),
Library(LibraryScreen),
Reader(ReaderScreen),
screen: DemoScreen,
}
#[derive(Clone, Debug, Eq, PartialEq)]
@ -33,105 +18,22 @@ pub struct AppUpdate {
pub command: Option<AppCommand>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AppCommand {
ScanWifi,
JoinWifi {
access_point: WifiAccessPoint,
password: WifiPassword,
},
JoinSavedWifi {
credentials: WifiCredentials,
},
SetFrontLight { on: bool },
}
impl App {
pub fn new() -> Self {
Self {
route: Route::Onboarding(OnboardingScreen::new()),
pending_command: None,
screen: DemoScreen::new(),
}
}
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 {
match &mut self.route {
Route::Onboarding(screen) => {
let update = screen.update(event);
match update.action {
OnboardingAction::None => {
let update = self.screen.update(event);
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>
where
@ -139,12 +41,7 @@ impl App {
{
let mut frame = Frame::new(target);
frame.clear()?;
match &self.route {
Route::Onboarding(screen) => screen.render(&mut frame),
Route::Library(screen) => screen.render(&mut frame),
Route::Reader(screen) => screen.render(&mut frame),
}
self.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::{
geometry::{Point, Size},
pixelcolor::Gray8,
prelude::{DrawTarget, OriginDimensions},
primitives::Rectangle,
prelude::{DrawTarget, OriginDimensions, Primitive},
primitives::{Circle, Line, PrimitiveStyle, Rectangle},
Drawable,
};
use crate::ui::{
@ -34,11 +35,19 @@ where
}
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> {
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> {
@ -64,6 +73,7 @@ where
self.wrapped_text(text, bounds, small_style)
}
#[allow(dead_code)]
pub fn paragraph(&mut self, y: i32, text: &str) -> Result<(), D::Error> {
let size = self.target.size();
let x = 72;
@ -75,6 +85,41 @@ where
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> {
TextRenderer::new(self.target).draw_text(text, position, style)
}
@ -96,6 +141,7 @@ where
TextStyle::new(&generated_fonts::QUATTRO_BODY, Gray8::new(0), 38)
}
#[allow(dead_code)]
fn reader_style() -> TextStyle {
TextStyle::new(&generated_fonts::QUATTRO_READER, Gray8::new(0), 43)
}

View File

@ -1,6 +1,6 @@
mod app;
mod books;
mod frame;
#[allow(dead_code)]
mod generated_fonts;
mod screens;
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 onboarding;
pub(crate) mod reader;
pub(crate) mod demo;
use super::{app::AppCommand, RefreshHint};
pub(crate) struct ScreenUpdate<A> {
pub(crate) struct ScreenUpdate {
pub dirty: bool,
pub refresh: RefreshHint,
pub action: A,
pub command: Option<AppCommand>,
}
impl<A> ScreenUpdate<A> {
pub fn new(dirty: bool, refresh: RefreshHint, action: A) -> Self {
impl ScreenUpdate {
pub const fn new(dirty: bool, refresh: RefreshHint) -> Self {
Self {
dirty,
refresh,
action,
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 {
dirty,
refresh,
action,
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.
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
name: ble
display_name: Enable BLE via the esp-wifi crate.
help: esp-wifi comes with no stability guarantees at this time.
display_name: Enable BLE via the radio stack.
help: The radio stack comes with no stability guarantees at this time.
requires:
- alloc
- unstable-hal

View File

@ -19,7 +19,7 @@
<button type="button" data-event="4">Hold select</button>
<button type="button" data-event="5">Hold back</button>
</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>
<script type="module" src="./sim.js"></script>
</body>

View File

@ -9,6 +9,7 @@ const status = document.getElementById('status');
const buttons = Array.from(document.querySelectorAll('[data-event]'));
let wasm;
let lastPointerTime = -Infinity;
async function loadWasm() {
const imports = {};
@ -57,6 +58,67 @@ function sendInput(eventCode) {
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) {
if (event.key === 'ArrowUp') {
return EVENT_UP;
@ -82,6 +144,10 @@ buttons.forEach((button) => {
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) => {
const eventCode = keyboardEventCode(event);
if (eventCode === null) {
@ -100,7 +166,7 @@ try {
buttons.forEach((button) => {
button.disabled = false;
});
status.textContent = 'Ready. Select scans with mock WiFi results.';
status.textContent = 'Ready. Press buttons or click/touch the display.';
} catch (error) {
console.error(error);
status.textContent = error instanceof Error ? error.message : 'Could not load simulator.';

View File

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