snapshot
This commit is contained in:
parent
6c3da38fb9
commit
37ba2e0e6f
|
|
@ -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
494
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
31
Cargo.toml
31
Cargo.toml
|
|
@ -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
175
docs/power.md
Normal 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?
|
||||
|
|
@ -1,3 +1,2 @@
|
|||
# Name, Type, SubType, Offset, Size, Flags
|
||||
wifi,data,nvs,0x9000,0x7000,
|
||||
factory,app,factory,0x10000,0x3F0000,
|
||||
|
|
|
|||
|
|
|
@ -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
131
scripts/flash-font.sh
Executable 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
|
||||
530
src/bin/main.rs
530
src/bin/main.rs
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -6,4 +6,5 @@ pub enum InputEvent {
|
|||
Back,
|
||||
SelectLong,
|
||||
BackLong,
|
||||
Touch { x: u16, y: u16 },
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
119
src/ui/app.rs
119
src/ui/app.rs
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
333
src/ui/screens/demo.rs
Normal 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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
219
src/wifi.rs
219
src/wifi.rs
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
68
web/sim.js
68
web/sim.js
|
|
@ -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.';
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ main {
|
|||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
max-width: 480px;
|
||||
height: auto;
|
||||
border: 1px solid #222222;
|
||||
background: #f8f8f8;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user