diff --git a/.cargo/config.toml b/.cargo/config.toml index dc1c12d..870d9e7 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -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] diff --git a/Cargo.lock b/Cargo.lock index b9d64c7..26baf0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 6d5ec42..51459c8 100644 --- a/Cargo.toml +++ b/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 diff --git a/docs/power.md b/docs/power.md new file mode 100644 index 0000000..eaf8b5e --- /dev/null +++ b/docs/power.md @@ -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? diff --git a/partitions.csv b/partitions.csv index db5d425..3e65150 100644 --- a/partitions.csv +++ b/partitions.csv @@ -1,3 +1,2 @@ # Name, Type, SubType, Offset, Size, Flags -wifi,data,nvs,0x9000,0x7000, factory,app,factory,0x10000,0x3F0000, diff --git a/scripts/build-web-sim.sh b/scripts/build-web-sim.sh index fb9162e..69197da 100755 --- a/scripts/build-web-sim.sh +++ b/scripts/build-web-sim.sh @@ -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 diff --git a/scripts/flash-font.sh b/scripts/flash-font.sh new file mode 100755 index 0000000..926b0ea --- /dev/null +++ b/scripts/flash-font.sh @@ -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] + 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 + + 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 diff --git a/src/bin/main.rs b/src/bin/main.rs index 8bebf3d..ce19a43 100644 --- a/src/bin/main.rs +++ b/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 DrawTarget for Rotated180<'_, D> +impl DrawTarget for RotatedMinus90<'_, D> where D: DrawTarget + OriginDimensions, { @@ -576,26 +571,30 @@ where where I: IntoIterator>, { - 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 OriginDimensions for Rotated180<'_, D> +impl 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, + was_touching: bool, + read_failed: bool, +} + +impl TouchController +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 { + 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 { - 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 { - 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) -> 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( display: &mut Ssd1677, 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() { - 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"); - } else { - defmt::error!("Failed to save WiFi credentials"); + if let Some(event) = buttons.poll().or_else(|| touch.poll()) { + let update = app.update(event); + 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 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; diff --git a/src/bin/web_sim.rs b/src/bin/web_sim.rs index 3ff608f..19ba968 100644 --- a/src/bin/web_sim.rs +++ b/src/bin/web_sim.rs @@ -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 diff --git a/src/input.rs b/src/input.rs index 6b47eff..948dc21 100644 --- a/src/input.rs +++ b/src/input.rs @@ -6,4 +6,5 @@ pub enum InputEvent { Back, SelectLong, BackLong, + Touch { x: u16, y: u16 }, } diff --git a/src/lib.rs b/src/lib.rs index 92cc71f..ead13a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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, -}; diff --git a/src/ui/app.rs b/src/ui/app.rs index 0d2c4e0..f282f8f 100644 --- a/src/ui/app.rs +++ b/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, -} - -enum Route { - Onboarding(OnboardingScreen), - Library(LibraryScreen), - Reader(ReaderScreen), + screen: DemoScreen, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -33,104 +18,21 @@ pub struct AppUpdate { pub command: Option, } -#[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) -> 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 { - 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 => { - 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), - } + let update = self.screen.update(event); + AppUpdate::new(update.dirty, update.refresh, update.command) } pub fn render(&self, target: &mut D) -> Result<(), D::Error> @@ -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) } } diff --git a/src/ui/books.rs b/src/ui/books.rs deleted file mode 100644 index 8683599..0000000 --- a/src/ui/books.rs +++ /dev/null @@ -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, - }, -]; diff --git a/src/ui/frame.rs b/src/ui/frame.rs index 6e5aa73..1ebb2dd 100644 --- a/src/ui/frame.rs +++ b/src/ui/frame.rs @@ -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) } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 8be5b08..5e66af7 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,6 +1,6 @@ mod app; -mod books; mod frame; +#[allow(dead_code)] mod generated_fonts; mod screens; mod typography; diff --git a/src/ui/screens/demo.rs b/src/ui/screens/demo.rs new file mode 100644 index 0000000..83a81e5 --- /dev/null +++ b/src/ui/screens/demo.rs @@ -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, + last_button: Option, + 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(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error> + where + D: DrawTarget + 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(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error> + where + D: DrawTarget + 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( + &self, + frame: &mut Frame<'_, D>, + y: i32, + button: ButtonKind, + ) -> Result<(), D::Error> + where + D: DrawTarget + 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(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error> + where + D: DrawTarget + 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(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error> + where + D: DrawTarget + 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( + &self, + frame: &mut Frame<'_, D>, + touch: TouchPoint, + ) -> Result<(), D::Error> + where + D: DrawTarget + 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(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error> + where + D: DrawTarget + 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(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error> + where + D: DrawTarget + 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", + } + } +} diff --git a/src/ui/screens/library.rs b/src/ui/screens/library.rs deleted file mode 100644 index 70add8b..0000000 --- a/src/ui/screens/library.rs +++ /dev/null @@ -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 { - 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(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error> - where - D: DrawTarget + 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") - } -} diff --git a/src/ui/screens/mod.rs b/src/ui/screens/mod.rs index c65efa8..6fe7957 100644 --- a/src/ui/screens/mod.rs +++ b/src/ui/screens/mod.rs @@ -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 { +pub(crate) struct ScreenUpdate { pub dirty: bool, pub refresh: RefreshHint, - pub action: A, pub command: Option, } -impl ScreenUpdate { - 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), } } diff --git a/src/ui/screens/onboarding.rs b/src/ui/screens/onboarding.rs deleted file mode 100644 index cbde24c..0000000 --- a/src/ui/screens/onboarding.rs +++ /dev/null @@ -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 { - 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 { - 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(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error> - where - D: DrawTarget + 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( - &self, - frame: &mut Frame<'_, D>, - notice: &'static str, - ) -> Result<(), D::Error> - where - D: DrawTarget + 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(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error> - where - D: DrawTarget + 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( - &self, - frame: &mut Frame<'_, D>, - state: &NetworkListState, - ) -> Result<(), D::Error> - where - D: DrawTarget + 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( - &self, - frame: &mut Frame<'_, D>, - state: &PasswordEntryState, - ) -> Result<(), D::Error> - where - D: DrawTarget + 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, "")?; - } 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( - &self, - frame: &mut Frame<'_, D>, - state: &JoiningState, - ) -> Result<(), D::Error> - where - D: DrawTarget + 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( - &self, - frame: &mut Frame<'_, D>, - ssid: &WifiSsid, - ) -> Result<(), D::Error> - where - D: DrawTarget + 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(&self, frame: &mut Frame<'_, D>, ssid: &WifiSsid) -> Result<(), D::Error> - where - D: DrawTarget + 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]) - } -} diff --git a/src/ui/screens/reader.rs b/src/ui/screens/reader.rs deleted file mode 100644 index 4995356..0000000 --- a/src/ui/screens/reader.rs +++ /dev/null @@ -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 { - 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(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error> - where - D: DrawTarget + 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") - } -} diff --git a/src/wifi.rs b/src/wifi.rs deleted file mode 100644 index 97c6349..0000000 --- a/src/wifi.rs +++ /dev/null @@ -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; -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 { - 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 { - 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), - JoinFinished(Result), -} - -#[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", - } - } -} diff --git a/template.yaml b/template.yaml index 990c8aa..cbfd50d 100644 --- a/template.yaml +++ b/template.yaml @@ -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 diff --git a/web/index.html b/web/index.html index 18de990..fed5918 100644 --- a/web/index.html +++ b/web/index.html @@ -19,7 +19,7 @@ -

Keyboard: ArrowUp, ArrowDown, Enter, Escape or Backspace.

+

Keyboard: ArrowUp, ArrowDown, Enter, Escape or Backspace. Click or tap the display to send a touch.

diff --git a/web/sim.js b/web/sim.js index 2646891..14efe2b 100644 --- a/web/sim.js +++ b/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.'; diff --git a/web/style.css b/web/style.css index 52234dd..95a5add 100644 --- a/web/style.css +++ b/web/style.css @@ -12,7 +12,7 @@ main { canvas { display: block; width: 100%; - max-width: 800px; + max-width: 480px; height: auto; border: 1px solid #222222; background: #f8f8f8;