Bump kinda to 0.1.1 with WiFi and UI features
This commit is contained in:
parent
548636779e
commit
6c3da38fb9
|
|
@ -1,6 +1,6 @@
|
||||||
[target.riscv32imac-unknown-none-elf]
|
[target.riscv32imac-unknown-none-elf]
|
||||||
# runner = "probe-rs run --chip=esp32c6 --always-print-stacktrace --no-location --catch-hardfault"
|
# runner = "probe-rs run --chip=esp32c6 --always-print-stacktrace --no-location --catch-hardfault"
|
||||||
runner = "espflash flash --monitor -L defmt --no-skip --chip esp32c6"
|
runner = "espflash flash --monitor -L defmt --no-skip --chip esp32c6 --partition-table partitions.csv"
|
||||||
rustflags = ["-C", "force-frame-pointers"]
|
rustflags = ["-C", "force-frame-pointers"]
|
||||||
|
|
||||||
[env]
|
[env]
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,6 +2,7 @@
|
||||||
# will have compiled files and executables
|
# will have compiled files and executables
|
||||||
debug/
|
debug/
|
||||||
target/
|
target/
|
||||||
|
web/pkg/
|
||||||
|
|
||||||
# These are backup files generated by rustfmt
|
# These are backup files generated by rustfmt
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
|
|
||||||
412
Cargo.lock
generated
412
Cargo.lock
generated
|
|
@ -2,6 +2,15 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aead"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
|
|
@ -76,6 +85,36 @@ dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bt-hci"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "211713d2e9fb4793ce4360a712c0764264aff6be48932ccf02ca2a331c0436a9"
|
||||||
|
dependencies = [
|
||||||
|
"btuuid",
|
||||||
|
"defmt 1.0.1",
|
||||||
|
"embassy-sync 0.8.0",
|
||||||
|
"embedded-io 0.7.1",
|
||||||
|
"embedded-io-async 0.7.0",
|
||||||
|
"futures-intrusive",
|
||||||
|
"heapless 0.9.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "btuuid"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f5f48f1e9b0aad0a4f05d17bdeae0fa20ff798e272a03a6940ca27ad9c5a6ae7"
|
||||||
|
dependencies = [
|
||||||
|
"defmt 0.3.100",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byte"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21c7ab3e4ae80853c7f8dcdcd904dfa25c02cc373534b8d165194325a088a7cc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.25.0"
|
version = "1.25.0"
|
||||||
|
|
@ -98,12 +137,33 @@ dependencies = [
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ccm"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0a9cf981c7e62b6fb02225592ee7ebf221e0b0b5317984a57a1e9d21af20e317"
|
||||||
|
dependencies = [
|
||||||
|
"aead",
|
||||||
|
"cipher",
|
||||||
|
"ctr",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cipher"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const-default"
|
name = "const-default"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|
@ -136,6 +196,15 @@ dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ctr"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea"
|
||||||
|
dependencies = [
|
||||||
|
"cipher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.20.11"
|
version = "0.20.11"
|
||||||
|
|
@ -301,6 +370,26 @@ dependencies = [
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "docsplay"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8547ea80db62c5bb9d7796fcce5e6e07d1136bdc1a02269095061e806758fab4"
|
||||||
|
dependencies = [
|
||||||
|
"docsplay-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "docsplay-macros"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11772ed3eb3db124d826f3abeadf5a791a557f62c19b123e3f07288158a71fdd"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.101",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "document-features"
|
name = "document-features"
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
|
|
@ -668,7 +757,7 @@ dependencies = [
|
||||||
"document-features",
|
"document-features",
|
||||||
"enumset",
|
"enumset",
|
||||||
"esp-config",
|
"esp-config",
|
||||||
"esp-sync",
|
"esp-sync 0.2.1",
|
||||||
"linked_list_allocator",
|
"linked_list_allocator",
|
||||||
"rlsf",
|
"rlsf",
|
||||||
]
|
]
|
||||||
|
|
@ -683,7 +772,7 @@ dependencies = [
|
||||||
"defmt 1.0.1",
|
"defmt 1.0.1",
|
||||||
"document-features",
|
"document-features",
|
||||||
"esp-config",
|
"esp-config",
|
||||||
"esp-metadata-generated",
|
"esp-metadata-generated 0.4.0",
|
||||||
"esp-println",
|
"esp-println",
|
||||||
"heapless 0.9.3",
|
"heapless 0.9.3",
|
||||||
"riscv",
|
"riscv",
|
||||||
|
|
@ -700,8 +789,8 @@ dependencies = [
|
||||||
"document-features",
|
"document-features",
|
||||||
"embedded-storage",
|
"embedded-storage",
|
||||||
"esp-config",
|
"esp-config",
|
||||||
"esp-hal-procmacros",
|
"esp-hal-procmacros 0.22.0",
|
||||||
"esp-metadata-generated",
|
"esp-metadata-generated 0.4.0",
|
||||||
"esp-rom-sys",
|
"esp-rom-sys",
|
||||||
"jiff",
|
"jiff",
|
||||||
"strum",
|
"strum",
|
||||||
|
|
@ -714,7 +803,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4d9b92fd9cfb0b4f8f1b6219b9763269a335571e307b014903b8201619374b80"
|
checksum = "4d9b92fd9cfb0b4f8f1b6219b9763269a335571e307b014903b8201619374b80"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"document-features",
|
"document-features",
|
||||||
"esp-metadata-generated",
|
"esp-metadata-generated 0.4.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"somni-expr",
|
"somni-expr",
|
||||||
|
|
@ -747,11 +836,11 @@ dependencies = [
|
||||||
"embedded-io-async 0.7.0",
|
"embedded-io-async 0.7.0",
|
||||||
"enumset",
|
"enumset",
|
||||||
"esp-config",
|
"esp-config",
|
||||||
"esp-hal-procmacros",
|
"esp-hal-procmacros 0.22.0",
|
||||||
"esp-metadata-generated",
|
"esp-metadata-generated 0.4.0",
|
||||||
"esp-riscv-rt",
|
"esp-riscv-rt",
|
||||||
"esp-rom-sys",
|
"esp-rom-sys",
|
||||||
"esp-sync",
|
"esp-sync 0.2.1",
|
||||||
"esp32",
|
"esp32",
|
||||||
"esp32c2",
|
"esp32c2",
|
||||||
"esp32c3",
|
"esp32c3",
|
||||||
|
|
@ -776,6 +865,20 @@ dependencies = [
|
||||||
"xtensa-lx-rt",
|
"xtensa-lx-rt",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "esp-hal-procmacros"
|
||||||
|
version = "0.21.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3e025a7a7a0affdb4ff913b5c4494aef96ee03d085bf83c27453ae3a71d50da6"
|
||||||
|
dependencies = [
|
||||||
|
"document-features",
|
||||||
|
"proc-macro-crate",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.101",
|
||||||
|
"termcolor",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "esp-hal-procmacros"
|
name = "esp-hal-procmacros"
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
|
|
@ -791,12 +894,36 @@ dependencies = [
|
||||||
"termcolor",
|
"termcolor",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "esp-metadata-generated"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a93e39c8ad8d390d248dc7b9f4b59a873f313bf535218b8e2351356972399e3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "esp-metadata-generated"
|
name = "esp-metadata-generated"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42c2ee95b945a4780796e4359e72c033aed3b45073880e8029458f538532db8a"
|
checksum = "42c2ee95b945a4780796e4359e72c033aed3b45073880e8029458f538532db8a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "esp-phy"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7c0a29815cd105ae1a02f3d0c6e7aafda9504a41effae17fac4c3f827719228"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"defmt 1.0.1",
|
||||||
|
"document-features",
|
||||||
|
"embassy-sync 0.8.0",
|
||||||
|
"esp-config",
|
||||||
|
"esp-hal",
|
||||||
|
"esp-metadata-generated 0.4.0",
|
||||||
|
"esp-sync 0.2.1",
|
||||||
|
"esp-wifi-sys-esp32c6",
|
||||||
|
"esp32c6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "esp-println"
|
name = "esp-println"
|
||||||
version = "0.17.0"
|
version = "0.17.0"
|
||||||
|
|
@ -805,12 +932,64 @@ checksum = "42dee1e9ac7c3539bf6464db1707b0edd7557168f98278cf3c84fe70e63c6ce6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt 1.0.1",
|
"defmt 1.0.1",
|
||||||
"document-features",
|
"document-features",
|
||||||
"esp-metadata-generated",
|
"esp-metadata-generated 0.4.0",
|
||||||
"esp-sync",
|
"esp-sync 0.2.1",
|
||||||
"log",
|
"log",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "esp-radio"
|
||||||
|
version = "1.0.0-beta.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0f25cc4e3ce27476b42c4a68943f10a92f9dec3c24bb001269958f0318fef02c"
|
||||||
|
dependencies = [
|
||||||
|
"allocator-api2",
|
||||||
|
"bt-hci",
|
||||||
|
"cfg-if",
|
||||||
|
"defmt 1.0.1",
|
||||||
|
"docsplay",
|
||||||
|
"document-features",
|
||||||
|
"embassy-net-driver",
|
||||||
|
"embassy-sync 0.8.0",
|
||||||
|
"enumset",
|
||||||
|
"esp-alloc",
|
||||||
|
"esp-config",
|
||||||
|
"esp-hal",
|
||||||
|
"esp-hal-procmacros 0.22.0",
|
||||||
|
"esp-metadata-generated 0.4.0",
|
||||||
|
"esp-phy",
|
||||||
|
"esp-radio-rtos-driver",
|
||||||
|
"esp-rom-sys",
|
||||||
|
"esp-sync 0.2.1",
|
||||||
|
"esp-wifi-sys-esp32",
|
||||||
|
"esp-wifi-sys-esp32c2",
|
||||||
|
"esp-wifi-sys-esp32c3",
|
||||||
|
"esp-wifi-sys-esp32c6",
|
||||||
|
"esp-wifi-sys-esp32h2",
|
||||||
|
"esp-wifi-sys-esp32s2",
|
||||||
|
"esp-wifi-sys-esp32s3",
|
||||||
|
"esp32c6",
|
||||||
|
"heapless 0.9.3",
|
||||||
|
"ieee802154",
|
||||||
|
"instability",
|
||||||
|
"num-derive",
|
||||||
|
"num-traits",
|
||||||
|
"portable-atomic",
|
||||||
|
"portable_atomic_enum",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "esp-radio-rtos-driver"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0bd75cd9073a90ffaa53db0bf17df7dc14164f2407a6ff36c725d2d1f78ff494"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"esp-sync 0.2.1",
|
||||||
|
"portable-atomic",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "esp-riscv-rt"
|
name = "esp-riscv-rt"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
|
|
@ -831,7 +1010,7 @@ checksum = "eae852ccb08971155023d1371c96d5490cbc26860f06aee2d629ef73f1a890c3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"document-features",
|
"document-features",
|
||||||
"esp-metadata-generated",
|
"esp-metadata-generated 0.4.0",
|
||||||
"esp32c6",
|
"esp32c6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -841,6 +1020,7 @@ version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "551f90766e1527edaa0c91e8d559e9e2a60397b545e93357ac61fb31845e5712"
|
checksum = "551f90766e1527edaa0c91e8d559e9e2a60397b545e93357ac61fb31845e5712"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"allocator-api2",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"defmt 1.0.1",
|
"defmt 1.0.1",
|
||||||
"document-features",
|
"document-features",
|
||||||
|
|
@ -850,15 +1030,47 @@ dependencies = [
|
||||||
"embassy-time-queue-utils",
|
"embassy-time-queue-utils",
|
||||||
"esp-config",
|
"esp-config",
|
||||||
"esp-hal",
|
"esp-hal",
|
||||||
"esp-hal-procmacros",
|
"esp-hal-procmacros 0.22.0",
|
||||||
"esp-metadata-generated",
|
"esp-metadata-generated 0.4.0",
|
||||||
|
"esp-radio-rtos-driver",
|
||||||
"esp-rom-sys",
|
"esp-rom-sys",
|
||||||
"esp-sync",
|
"esp-sync 0.2.1",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"riscv",
|
"riscv",
|
||||||
"xtensa-lx",
|
"xtensa-lx",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "esp-storage"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1495fc1f5549bdd840b52d9ceb201746200e1620d2636f46958c11e765623b80"
|
||||||
|
dependencies = [
|
||||||
|
"defmt 1.0.1",
|
||||||
|
"document-features",
|
||||||
|
"embedded-storage",
|
||||||
|
"esp-hal",
|
||||||
|
"esp-hal-procmacros 0.21.0",
|
||||||
|
"esp-metadata-generated 0.3.0",
|
||||||
|
"esp-rom-sys",
|
||||||
|
"esp-sync 0.1.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "esp-sync"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d44974639b4e88914f83fe60d2832c00276657d7d857628fdfc966cc7302e8a8"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"document-features",
|
||||||
|
"embassy-sync 0.6.2",
|
||||||
|
"embassy-sync 0.7.2",
|
||||||
|
"esp-metadata-generated 0.3.0",
|
||||||
|
"riscv",
|
||||||
|
"xtensa-lx",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "esp-sync"
|
name = "esp-sync"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|
@ -871,11 +1083,74 @@ dependencies = [
|
||||||
"embassy-sync 0.6.2",
|
"embassy-sync 0.6.2",
|
||||||
"embassy-sync 0.7.2",
|
"embassy-sync 0.7.2",
|
||||||
"embassy-sync 0.8.0",
|
"embassy-sync 0.8.0",
|
||||||
"esp-metadata-generated",
|
"esp-metadata-generated 0.4.0",
|
||||||
"riscv",
|
"riscv",
|
||||||
"xtensa-lx",
|
"xtensa-lx",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "esp-wifi-sys-esp32"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2556f38f5292d9735d4e156e276815fc001c9a0a2be0544a575c5fb867129d24"
|
||||||
|
dependencies = [
|
||||||
|
"defmt 1.0.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "esp-wifi-sys-esp32c2"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4b3eb3435dae84de611d4384639e61eed862818223e93fa70c525cb6a70127f"
|
||||||
|
dependencies = [
|
||||||
|
"defmt 1.0.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "esp-wifi-sys-esp32c3"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b290846b53db9a3965866964260220b67f2c41cc2dbc0b377e7136239fe168a7"
|
||||||
|
dependencies = [
|
||||||
|
"defmt 1.0.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "esp-wifi-sys-esp32c6"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57649401fc2f906a16e2268de88693724a125adcd0eba89b594a157affcee2d5"
|
||||||
|
dependencies = [
|
||||||
|
"defmt 1.0.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "esp-wifi-sys-esp32h2"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5cf1be2e311f4b4e75b5507c6b007ffc3fcb8c6cb57f83cc6a9569ecdcf58484"
|
||||||
|
dependencies = [
|
||||||
|
"defmt 1.0.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "esp-wifi-sys-esp32s2"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2a9a7f1bc51e7026c3012cda4f11d8799e5e0c0bb3be85797462219def6c26e"
|
||||||
|
dependencies = [
|
||||||
|
"defmt 1.0.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "esp-wifi-sys-esp32s3"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d8321f44b57d8112cbd8607cc83b85783b9195289acb45532728e3e229e7786"
|
||||||
|
dependencies = [
|
||||||
|
"defmt 1.0.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "esp32"
|
name = "esp32"
|
||||||
version = "0.40.2"
|
version = "0.40.2"
|
||||||
|
|
@ -1003,6 +1278,16 @@ version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-intrusive"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"lock_api",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
|
|
@ -1058,6 +1343,15 @@ dependencies = [
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hash32"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hash32"
|
name = "hash32"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
|
@ -1067,6 +1361,17 @@ dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hash32-derive"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59d2aba832b60be25c1b169146b27c64115470981b128ed84c8db18c1b03c6ff"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
|
|
@ -1079,7 +1384,7 @@ version = "0.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
|
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hash32",
|
"hash32 0.3.1",
|
||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1089,7 +1394,8 @@ version = "0.9.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4"
|
checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hash32",
|
"defmt 1.0.1",
|
||||||
|
"hash32 0.3.1",
|
||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1105,6 +1411,20 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ieee802154"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fcb6de62f20180795db19ae2ab338852a66f8576581554fa8a730e437b450a5"
|
||||||
|
dependencies = [
|
||||||
|
"byte",
|
||||||
|
"ccm",
|
||||||
|
"cipher",
|
||||||
|
"defmt 0.3.100",
|
||||||
|
"hash32 0.2.1",
|
||||||
|
"hash32-derive",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.14.0"
|
version = "2.14.0"
|
||||||
|
|
@ -1166,7 +1486,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kinda"
|
name = "kinda"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"critical-section",
|
"critical-section",
|
||||||
"defmt 1.0.1",
|
"defmt 1.0.1",
|
||||||
|
|
@ -1178,12 +1498,15 @@ dependencies = [
|
||||||
"embedded-hal 1.0.0",
|
"embedded-hal 1.0.0",
|
||||||
"embedded-io 0.6.1",
|
"embedded-io 0.6.1",
|
||||||
"embedded-io-async 0.6.1",
|
"embedded-io-async 0.6.1",
|
||||||
|
"embedded-storage",
|
||||||
"esp-alloc",
|
"esp-alloc",
|
||||||
"esp-backtrace",
|
"esp-backtrace",
|
||||||
"esp-bootloader-esp-idf",
|
"esp-bootloader-esp-idf",
|
||||||
"esp-hal",
|
"esp-hal",
|
||||||
"esp-println",
|
"esp-println",
|
||||||
|
"esp-radio",
|
||||||
"esp-rtos",
|
"esp-rtos",
|
||||||
|
"esp-storage",
|
||||||
"heapless 0.8.0",
|
"heapless 0.8.0",
|
||||||
"lm3630a",
|
"lm3630a",
|
||||||
"smoltcp",
|
"smoltcp",
|
||||||
|
|
@ -1225,6 +1548,15 @@ dependencies = [
|
||||||
"embedded-hal-async",
|
"embedded-hal-async",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||||
|
dependencies = [
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.27"
|
version = "0.4.27"
|
||||||
|
|
@ -1295,6 +1627,17 @@ dependencies = [
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-derive"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.101",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
|
|
@ -1352,6 +1695,27 @@ dependencies = [
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable_atomic_enum"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "30d48f60c43e0120bb2bb48589a16d4bed2f4b911be41e299f2d0fc0e0e20885"
|
||||||
|
dependencies = [
|
||||||
|
"portable-atomic",
|
||||||
|
"portable_atomic_enum_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable_atomic_enum_macros"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a33fa6ec7f2047f572d49317cca19c87195de99c6e5b6ee492da701cfe02b053"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.101",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "3.5.0"
|
version = "3.5.0"
|
||||||
|
|
@ -1527,6 +1891,12 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
|
|
@ -1671,6 +2041,12 @@ dependencies = [
|
||||||
"syn 2.0.101",
|
"syn 2.0.101",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "svgbobdoc"
|
name = "svgbobdoc"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|
|
||||||
51
Cargo.toml
51
Cargo.toml
|
|
@ -1,14 +1,29 @@
|
||||||
[package]
|
[package]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "kinda"
|
name = "kinda"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
|
default-run = "kinda"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
web-sim = []
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "kinda"
|
name = "kinda"
|
||||||
path = "./src/bin/main.rs"
|
path = "./src/bin/main.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "kinda-web-sim"
|
||||||
|
path = "./src/bin/web_sim.rs"
|
||||||
|
required-features = ["web-sim"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ssd1677 = { path = "../forks/SSD1677" }
|
embedded-graphics = "0.8.1"
|
||||||
|
heapless = { version = "0.8.0", default-features = false }
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "riscv32")'.dependencies]
|
||||||
|
embedded-graphics = { version = "0.8.1", features = ["defmt"] }
|
||||||
|
ssd1677 = { path = "../ssd1677" }
|
||||||
defmt = "1.0.1"
|
defmt = "1.0.1"
|
||||||
embassy-net = { version = "0.6.0", features = [
|
embassy-net = { version = "0.6.0", features = [
|
||||||
"dhcpv4",
|
"dhcpv4",
|
||||||
|
|
@ -19,11 +34,7 @@ embassy-net = { version = "0.6.0", features = [
|
||||||
embedded-io = "0.6.1"
|
embedded-io = "0.6.1"
|
||||||
embedded-io-async = "0.6.1"
|
embedded-io-async = "0.6.1"
|
||||||
esp-alloc = "0.10.0"
|
esp-alloc = "0.10.0"
|
||||||
esp-hal = { version = "1.1.1", features = [
|
esp-hal = { version = "1.1.1", features = ["defmt", "esp32c6", "unstable"] }
|
||||||
"defmt",
|
|
||||||
"esp32c6",
|
|
||||||
"unstable",
|
|
||||||
] }
|
|
||||||
smoltcp = { version = "0.12.0", default-features = false, features = [
|
smoltcp = { version = "0.12.0", default-features = false, features = [
|
||||||
"proto-dhcpv4",
|
"proto-dhcpv4",
|
||||||
"proto-dns",
|
"proto-dns",
|
||||||
|
|
@ -38,19 +49,32 @@ smoltcp = { version = "0.12.0", default-features = false, features = [
|
||||||
critical-section = "1.2.0"
|
critical-section = "1.2.0"
|
||||||
embassy-executor = { version = "0.10.0", features = ["defmt", "nightly"] }
|
embassy-executor = { version = "0.10.0", features = ["defmt", "nightly"] }
|
||||||
embassy-time = { version = "0.5.0", features = ["generic-queue-8"] }
|
embassy-time = { version = "0.5.0", features = ["generic-queue-8"] }
|
||||||
esp-rtos = { version = "0.3.0", features = ["embassy", "defmt", "esp32c6"] }
|
esp-rtos = { version = "0.3.0", features = [
|
||||||
|
"embassy",
|
||||||
heapless = { version = "0.8.0", default-features = false }
|
"defmt",
|
||||||
|
"esp-radio",
|
||||||
|
"esp32c6",
|
||||||
|
] }
|
||||||
|
esp-radio = { version = "1.0.0-beta.0", features = [
|
||||||
|
"defmt",
|
||||||
|
"esp32c6",
|
||||||
|
"wifi",
|
||||||
|
] }
|
||||||
static_cell = { version = "2.1.0", features = ["nightly"] }
|
static_cell = { version = "2.1.0", features = ["nightly"] }
|
||||||
embedded-hal = { version = "1.0.0", features = ["defmt-03"] }
|
embedded-hal = { version = "1.0.0", features = ["defmt-03"] }
|
||||||
|
embedded-storage = "0.3.1"
|
||||||
|
esp-storage = { version = "0.8.1", features = ["defmt", "esp32c6"] }
|
||||||
esp-println = { version = "0.17.0", features = [
|
esp-println = { version = "0.17.0", features = [
|
||||||
"defmt-espflash",
|
"defmt-espflash",
|
||||||
"esp32c6",
|
"esp32c6",
|
||||||
"log-04",
|
"log-04",
|
||||||
] }
|
] }
|
||||||
esp-backtrace = { version = "0.19.0", features = ["panic-handler", "esp32c6", "defmt"] }
|
esp-backtrace = { version = "0.19.0", features = [
|
||||||
|
"panic-handler",
|
||||||
|
"esp32c6",
|
||||||
|
"defmt",
|
||||||
|
] }
|
||||||
esp-bootloader-esp-idf = { version = "0.5.0", features = ["esp32c6"] }
|
esp-bootloader-esp-idf = { version = "0.5.0", features = ["esp32c6"] }
|
||||||
embedded-graphics = { version = "0.8.1", features = ["defmt"] }
|
|
||||||
embassy-futures = { version = "0.1.1", features = ["defmt"] }
|
embassy-futures = { version = "0.1.1", features = ["defmt"] }
|
||||||
lm3630a = { version = "0.1.1", features = ["async"] }
|
lm3630a = { version = "0.1.1", features = ["async"] }
|
||||||
|
|
||||||
|
|
@ -59,6 +83,9 @@ lm3630a = { version = "0.1.1", features = ["async"] }
|
||||||
# For debug builds always builds with some optimization
|
# For debug builds always builds with some optimization
|
||||||
opt-level = "s"
|
opt-level = "s"
|
||||||
|
|
||||||
|
[profile.dev.package.esp-radio]
|
||||||
|
opt-level = 3
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
codegen-units = 1 # LLVM can perform better optimizations using a single thread
|
codegen-units = 1 # LLVM can perform better optimizations using a single thread
|
||||||
debug = 2
|
debug = 2
|
||||||
|
|
|
||||||
5
build.rs
5
build.rs
|
|
@ -1,4 +1,9 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
|
let target = std::env::var("TARGET").unwrap_or_default();
|
||||||
|
if target != "riscv32imac-unknown-none-elf" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
linker_be_nice();
|
linker_be_nice();
|
||||||
println!("cargo:rustc-link-arg=-Tdefmt.x");
|
println!("cargo:rustc-link-arg=-Tdefmt.x");
|
||||||
// make sure linkall.x is the last linker script (otherwise might cause problems with flip-link)
|
// make sure linkall.x is the last linker script (otherwise might cause problems with flip-link)
|
||||||
|
|
|
||||||
8
package.json
Normal file
8
package.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"sim:build": "./scripts/build-web-sim.sh",
|
||||||
|
"sim:dev": "npm run sim:build && python3 -m http.server 5174 --directory web"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
partitions.csv
Normal file
3
partitions.csv
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
|
wifi,data,nvs,0x9000,0x7000,
|
||||||
|
factory,app,factory,0x10000,0x3F0000,
|
||||||
|
11
scripts/build-web-sim.sh
Executable file
11
scripts/build-web-sim.sh
Executable file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
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"
|
||||||
|
|
||||||
|
cargo build --bin kinda-web-sim --features web-sim --target wasm32-unknown-unknown --release
|
||||||
|
mkdir -p web/pkg
|
||||||
|
target_dir="${CARGO_TARGET_DIR:-target}"
|
||||||
|
cp "$target_dir/wasm32-unknown-unknown/release/kinda-web-sim.wasm" web/pkg/kinda.wasm
|
||||||
299
scripts/generate-fonts-ftmono.c
Normal file
299
scripts/generate-fonts-ftmono.c
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
// Generate embedded 4-bit-alpha font tables from FreeType's native monochrome renderer.
|
||||||
|
//
|
||||||
|
// Build/run example:
|
||||||
|
// cc scripts/generate-fonts-ftmono.c $(freetype-config --cflags --libs) -o /tmp/kinda-fontgen-ftmono
|
||||||
|
// /tmp/kinda-fontgen-ftmono /System/Library/Fonts/Supplemental/Verdana.ttf src/ui/generated_fonts.rs
|
||||||
|
//
|
||||||
|
// This path is intended for 1-bit e-paper output. It preserves TrueType mono
|
||||||
|
// hinting instead of thresholding an antialiased outline after rasterization.
|
||||||
|
|
||||||
|
#include <ft2build.h>
|
||||||
|
#include FT_FREETYPE_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
struct FontSize {
|
||||||
|
const char *name;
|
||||||
|
int pixels;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Glyph {
|
||||||
|
uint32_t codepoint;
|
||||||
|
uint8_t width;
|
||||||
|
uint8_t height;
|
||||||
|
uint32_t bitmap_offset;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Buffer {
|
||||||
|
uint8_t *data;
|
||||||
|
size_t len;
|
||||||
|
size_t cap;
|
||||||
|
};
|
||||||
|
|
||||||
|
static const struct FontSize SIZES[] = {
|
||||||
|
{"QUATTRO_SMALL", 24},
|
||||||
|
{"QUATTRO_BODY", 30},
|
||||||
|
{"QUATTRO_READER", 34},
|
||||||
|
{"QUATTRO_TITLE", 38},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint32_t EXTRA_CODEPOINTS[] = {
|
||||||
|
0x00A0,
|
||||||
|
0x2018,
|
||||||
|
0x2019,
|
||||||
|
0x201C,
|
||||||
|
0x201D,
|
||||||
|
0x2013,
|
||||||
|
0x2014,
|
||||||
|
0x2026,
|
||||||
|
};
|
||||||
|
|
||||||
|
static int div_ceil_int(int value, int divisor) {
|
||||||
|
return (value + divisor - 1) / divisor;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int round_26_6(FT_Pos value) {
|
||||||
|
if (value >= 0) {
|
||||||
|
return (int)((value + 32) >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
return -(int)(((-value) + 32) >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ceil_26_6(FT_Pos value) {
|
||||||
|
if (value >= 0) {
|
||||||
|
return (int)((value + 63) >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
return -(int)((-value) >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void buffer_push(struct Buffer *buffer, uint8_t value) {
|
||||||
|
if (buffer->len == buffer->cap) {
|
||||||
|
size_t next_cap = buffer->cap == 0 ? 4096 : buffer->cap * 2;
|
||||||
|
uint8_t *next = realloc(buffer->data, next_cap);
|
||||||
|
if (next == NULL) {
|
||||||
|
fprintf(stderr, "out of memory\n");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
buffer->data = next;
|
||||||
|
buffer->cap = next_cap;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer->data[buffer->len++] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int mono_bitmap_on(const FT_Bitmap *bitmap, unsigned int x, unsigned int y) {
|
||||||
|
int pitch = bitmap->pitch;
|
||||||
|
const uint8_t *row;
|
||||||
|
if (pitch < 0) {
|
||||||
|
row = bitmap->buffer + (bitmap->rows - 1 - y) * (unsigned int)(-pitch);
|
||||||
|
} else {
|
||||||
|
row = bitmap->buffer + y * (unsigned int)pitch;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (row[x / 8] & (0x80 >> (x & 7))) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void set_alpha4(uint8_t *cell, int width, int x, int y, uint8_t alpha) {
|
||||||
|
int pixel = y * width + x;
|
||||||
|
int byte = pixel / 2;
|
||||||
|
if ((pixel & 1) == 0) {
|
||||||
|
cell[byte] = (uint8_t)((cell[byte] & 0x0F) | (alpha << 4));
|
||||||
|
} else {
|
||||||
|
cell[byte] = (uint8_t)((cell[byte] & 0xF0) | alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int load_codepoints(uint32_t *codepoints, int max_codepoints) {
|
||||||
|
int count = 0;
|
||||||
|
for (uint32_t codepoint = 32; codepoint < 127; codepoint++) {
|
||||||
|
if (count >= max_codepoints) {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
codepoints[count++] = codepoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sizeof(EXTRA_CODEPOINTS) / sizeof(EXTRA_CODEPOINTS[0]); i++) {
|
||||||
|
if (count >= max_codepoints) {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
codepoints[count++] = EXTRA_CODEPOINTS[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void emit_bytes(FILE *out, const struct Buffer *bitmap) {
|
||||||
|
for (size_t i = 0; i < bitmap->len; i++) {
|
||||||
|
if (i % 16 == 0) {
|
||||||
|
fputs(" ", out);
|
||||||
|
}
|
||||||
|
fprintf(out, "0x%02x,", bitmap->data[i]);
|
||||||
|
if (i % 16 == 15 || i + 1 == bitmap->len) {
|
||||||
|
fputc('\n', out);
|
||||||
|
} else {
|
||||||
|
fputc(' ', out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void emit_font(FILE *out, FT_Face face, const char *name, int pixels, const uint32_t *codepoints, int codepoint_count) {
|
||||||
|
if (FT_Set_Pixel_Sizes(face, 0, (FT_UInt)pixels) != 0) {
|
||||||
|
fprintf(stderr, "could not set pixel size %d\n", pixels);
|
||||||
|
exit(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
int cell_height = ceil_26_6(face->size->metrics.height);
|
||||||
|
int baseline = ceil_26_6(face->size->metrics.ascender);
|
||||||
|
if (cell_height <= 0) {
|
||||||
|
cell_height = pixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Glyph *glyphs = calloc((size_t)codepoint_count, sizeof(struct Glyph));
|
||||||
|
if (glyphs == NULL) {
|
||||||
|
fprintf(stderr, "out of memory\n");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Buffer bitmap = {0};
|
||||||
|
int glyph_count = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < codepoint_count; i++) {
|
||||||
|
uint32_t codepoint = codepoints[i];
|
||||||
|
FT_UInt glyph_index = FT_Get_Char_Index(face, codepoint);
|
||||||
|
if (glyph_index == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FT_Load_Glyph(face, glyph_index, FT_LOAD_RENDER | FT_LOAD_TARGET_MONO) != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
FT_GlyphSlot slot = face->glyph;
|
||||||
|
FT_Bitmap *source = &slot->bitmap;
|
||||||
|
int advance = round_26_6(slot->advance.x);
|
||||||
|
int left = slot->bitmap_left;
|
||||||
|
int top = slot->bitmap_top;
|
||||||
|
int right = left + (int)source->width;
|
||||||
|
int cell_width = advance;
|
||||||
|
if (right > cell_width) {
|
||||||
|
cell_width = right;
|
||||||
|
}
|
||||||
|
if (cell_width <= 0) {
|
||||||
|
cell_width = (int)source->width;
|
||||||
|
}
|
||||||
|
if (cell_width <= 0) {
|
||||||
|
cell_width = 1;
|
||||||
|
}
|
||||||
|
if (cell_width > 255 || cell_height > 255) {
|
||||||
|
fprintf(stderr, "glyph U+%04X too large at %dpx\n", codepoint, pixels);
|
||||||
|
exit(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
int cell_len = div_ceil_int(cell_width * cell_height, 2);
|
||||||
|
uint8_t *cell = calloc((size_t)cell_len, 1);
|
||||||
|
if (cell == NULL) {
|
||||||
|
fprintf(stderr, "out of memory\n");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
int x_origin = left < 0 ? 0 : left;
|
||||||
|
int y_origin = baseline - top;
|
||||||
|
for (unsigned int y = 0; y < source->rows; y++) {
|
||||||
|
int target_y = y_origin + (int)y;
|
||||||
|
if (target_y < 0 || target_y >= cell_height) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (unsigned int x = 0; x < source->width; x++) {
|
||||||
|
int target_x = x_origin + (int)x;
|
||||||
|
if (target_x < 0 || target_x >= cell_width) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (mono_bitmap_on(source, x, y)) {
|
||||||
|
set_alpha4(cell, cell_width, target_x, target_y, 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t offset = (uint32_t)bitmap.len;
|
||||||
|
for (int j = 0; j < cell_len; j++) {
|
||||||
|
buffer_push(&bitmap, cell[j]);
|
||||||
|
}
|
||||||
|
free(cell);
|
||||||
|
|
||||||
|
glyphs[glyph_count++] = (struct Glyph){
|
||||||
|
.codepoint = codepoint,
|
||||||
|
.width = (uint8_t)cell_width,
|
||||||
|
.height = (uint8_t)cell_height,
|
||||||
|
.bitmap_offset = offset,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(out, "// %d px; cell height %d px; %d glyphs; %zu mono alpha bytes.\n", pixels, cell_height, glyph_count, bitmap.len);
|
||||||
|
fprintf(out, "pub(crate) static %s: BitmapFont = BitmapFont {\n", name);
|
||||||
|
fprintf(out, " cell_height: %d,\n", cell_height);
|
||||||
|
fprintf(out, " glyphs: &%s_GLYPHS,\n", name);
|
||||||
|
fprintf(out, " bitmap: &%s_BITMAP,\n", name);
|
||||||
|
fprintf(out, "};\n\n");
|
||||||
|
|
||||||
|
fprintf(out, "#[rustfmt::skip]\n");
|
||||||
|
fprintf(out, "static %s_GLYPHS: [Glyph; %d] = [\n", name, glyph_count);
|
||||||
|
for (int i = 0; i < glyph_count; i++) {
|
||||||
|
fprintf(out, " Glyph { codepoint: 0x%04x, width: %u, height: %u, bitmap_offset: %u },\n",
|
||||||
|
glyphs[i].codepoint, glyphs[i].width, glyphs[i].height, glyphs[i].bitmap_offset);
|
||||||
|
}
|
||||||
|
fprintf(out, "];\n\n");
|
||||||
|
|
||||||
|
fprintf(out, "#[rustfmt::skip]\n");
|
||||||
|
fprintf(out, "static %s_BITMAP: [u8; %zu] = [\n", name, bitmap.len);
|
||||||
|
emit_bytes(out, &bitmap);
|
||||||
|
fprintf(out, "];\n\n");
|
||||||
|
|
||||||
|
free(glyphs);
|
||||||
|
free(bitmap.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc != 3) {
|
||||||
|
fprintf(stderr, "usage: %s FONT.ttf OUTPUT.rs\n", argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
FT_Library library;
|
||||||
|
FT_Face face;
|
||||||
|
if (FT_Init_FreeType(&library) != 0) {
|
||||||
|
fprintf(stderr, "could not init freetype\n");
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
if (FT_New_Face(library, argv[1], 0, &face) != 0) {
|
||||||
|
fprintf(stderr, "could not open font %s\n", argv[1]);
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t codepoints[128];
|
||||||
|
int codepoint_count = load_codepoints(codepoints, 128);
|
||||||
|
|
||||||
|
FILE *out = fopen(argv[2], "w");
|
||||||
|
if (out == NULL) {
|
||||||
|
perror("fopen");
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(out, "// @generated by scripts/generate-fonts-ftmono.c. Do not edit by hand.\n");
|
||||||
|
fprintf(out, "// Source font: %s\n", argv[1]);
|
||||||
|
fprintf(out, "// Rasterizer: FreeType mono hinting\n\n");
|
||||||
|
fprintf(out, "use crate::ui::typography::{BitmapFont, Glyph};\n\n");
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sizeof(SIZES) / sizeof(SIZES[0]); i++) {
|
||||||
|
emit_font(out, face, SIZES[i].name, SIZES[i].pixels, codepoints, codepoint_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose(out);
|
||||||
|
FT_Done_Face(face);
|
||||||
|
FT_Done_FreeType(library);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
205
scripts/generate-fonts.py
Normal file
205
scripts/generate-fonts.py
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
# Generate embedded 1-bit bitmap fonts from a TrueType/OpenType font.
|
||||||
|
#
|
||||||
|
# FontForge's direct monochrome XBM export is too thin for this display, especially
|
||||||
|
# at footer sizes. This exports antialiased BMP glyphs and packs 4-bit alpha.
|
||||||
|
#
|
||||||
|
# Run with FontForge, not python3:
|
||||||
|
# fontforge -lang=py -script scripts/generate-fonts.py \
|
||||||
|
# '/Volumes/Drawer/Fonts/iAFonts/iA Writer Quattro/Static/iAWriterQuattroS-Regular.ttf' \
|
||||||
|
# src/ui/generated_fonts.rs
|
||||||
|
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import fontforge
|
||||||
|
|
||||||
|
SIZES = [
|
||||||
|
("QUATTRO_SMALL", 24),
|
||||||
|
("QUATTRO_BODY", 30),
|
||||||
|
("QUATTRO_READER", 34),
|
||||||
|
("QUATTRO_TITLE", 38),
|
||||||
|
]
|
||||||
|
|
||||||
|
CODEPOINTS = list(range(32, 127)) + [
|
||||||
|
0x00A0, # no-break space
|
||||||
|
0x2018, # left single quotation mark
|
||||||
|
0x2019, # right single quotation mark
|
||||||
|
0x201C, # left double quotation mark
|
||||||
|
0x201D, # right double quotation mark
|
||||||
|
0x2013, # en dash
|
||||||
|
0x2014, # em dash
|
||||||
|
0x2026, # ellipsis
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def parse_bmp(path):
|
||||||
|
with open(path, "rb") as file:
|
||||||
|
data = file.read()
|
||||||
|
|
||||||
|
if data[0:2] != b"BM":
|
||||||
|
raise RuntimeError("not a BMP file: {}".format(path))
|
||||||
|
|
||||||
|
pixel_offset = struct.unpack_from("<I", data, 10)[0]
|
||||||
|
header_size = struct.unpack_from("<I", data, 14)[0]
|
||||||
|
width = struct.unpack_from("<i", data, 18)[0]
|
||||||
|
height = struct.unpack_from("<i", data, 22)[0]
|
||||||
|
planes = struct.unpack_from("<H", data, 26)[0]
|
||||||
|
bits_per_pixel = struct.unpack_from("<H", data, 28)[0]
|
||||||
|
compression = struct.unpack_from("<I", data, 30)[0]
|
||||||
|
|
||||||
|
if width <= 0 or height == 0 or planes != 1 or bits_per_pixel != 8 or compression != 0:
|
||||||
|
raise RuntimeError("unsupported BMP format: {}".format(path))
|
||||||
|
|
||||||
|
palette_start = 14 + header_size
|
||||||
|
palette = []
|
||||||
|
for index in range(256):
|
||||||
|
offset = palette_start + index * 4
|
||||||
|
blue, green, red, _alpha = data[offset : offset + 4]
|
||||||
|
palette.append((int(red) + int(green) + int(blue)) // 3)
|
||||||
|
|
||||||
|
top_down = height < 0
|
||||||
|
height = abs(height)
|
||||||
|
bmp_row_bytes = ((width * bits_per_pixel + 31) // 32) * 4
|
||||||
|
packed = [0] * ((width * height + 1) // 2)
|
||||||
|
|
||||||
|
for row in range(height):
|
||||||
|
source_row = row if top_down else height - 1 - row
|
||||||
|
source_offset = pixel_offset + source_row * bmp_row_bytes
|
||||||
|
for column in range(width):
|
||||||
|
palette_index = data[source_offset + column]
|
||||||
|
alpha = (255 - palette[palette_index] + 8) // 17
|
||||||
|
alpha = min(15, max(0, alpha))
|
||||||
|
pixel_index = row * width + column
|
||||||
|
byte_index = pixel_index // 2
|
||||||
|
if pixel_index % 2 == 0:
|
||||||
|
packed[byte_index] |= alpha << 4
|
||||||
|
else:
|
||||||
|
packed[byte_index] |= alpha
|
||||||
|
|
||||||
|
return width, height, packed
|
||||||
|
|
||||||
|
|
||||||
|
def has_codepoint(font, codepoint):
|
||||||
|
try:
|
||||||
|
glyph = font[codepoint]
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return glyph is not None and glyph.isWorthOutputting()
|
||||||
|
|
||||||
|
|
||||||
|
def rust_array(values, indent=" ", per_line=16):
|
||||||
|
lines = []
|
||||||
|
for index in range(0, len(values), per_line):
|
||||||
|
chunk = values[index : index + per_line]
|
||||||
|
lines.append(indent + ", ".join("0x{:02x}".format(value) for value in chunk) + ",")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_font(font, temp_dir, name, pixelsize):
|
||||||
|
glyphs = []
|
||||||
|
bitmap = []
|
||||||
|
cell_height = 0
|
||||||
|
|
||||||
|
for codepoint in CODEPOINTS:
|
||||||
|
if not has_codepoint(font, codepoint):
|
||||||
|
continue
|
||||||
|
|
||||||
|
glyph = font[codepoint]
|
||||||
|
path = os.path.join(temp_dir, "glyph_{:04x}_{}.bmp".format(codepoint, pixelsize))
|
||||||
|
glyph.export(path, pixelsize=pixelsize)
|
||||||
|
width, height, data = parse_bmp(path)
|
||||||
|
|
||||||
|
cell_height = max(cell_height, height)
|
||||||
|
|
||||||
|
glyphs.append((codepoint, width, height, len(bitmap)))
|
||||||
|
bitmap.extend(data)
|
||||||
|
|
||||||
|
if cell_height == 0:
|
||||||
|
raise RuntimeError("no glyphs generated for {}".format(name))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"pixelsize": pixelsize,
|
||||||
|
"cell_height": cell_height,
|
||||||
|
"glyphs": glyphs,
|
||||||
|
"bitmap": bitmap,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def emit_font(output, generated):
|
||||||
|
name = generated["name"]
|
||||||
|
glyphs = generated["glyphs"]
|
||||||
|
bitmap = generated["bitmap"]
|
||||||
|
|
||||||
|
output.write(
|
||||||
|
"pub(crate) static {name}: BitmapFont = BitmapFont {{\n"
|
||||||
|
" cell_height: {cell_height},\n"
|
||||||
|
" glyphs: &{name}_GLYPHS,\n"
|
||||||
|
" bitmap: &{name}_BITMAP,\n"
|
||||||
|
"}};\n\n".format(name=name, cell_height=generated["cell_height"])
|
||||||
|
)
|
||||||
|
|
||||||
|
output.write("#[rustfmt::skip]\n")
|
||||||
|
output.write(
|
||||||
|
"static {name}_GLYPHS: [Glyph; {count}] = [\n".format(
|
||||||
|
name=name, count=len(glyphs)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for codepoint, width, height, offset in glyphs:
|
||||||
|
output.write(
|
||||||
|
" Glyph {{ codepoint: 0x{codepoint:04x}, width: {width}, height: {height}, bitmap_offset: {offset} }},\n".format(
|
||||||
|
codepoint=codepoint, width=width, height=height, offset=offset
|
||||||
|
)
|
||||||
|
)
|
||||||
|
output.write("];\n\n")
|
||||||
|
|
||||||
|
output.write("#[rustfmt::skip]\n")
|
||||||
|
output.write(
|
||||||
|
"static {name}_BITMAP: [u8; {count}] = [\n".format(
|
||||||
|
name=name, count=len(bitmap)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if bitmap:
|
||||||
|
output.write(rust_array(bitmap))
|
||||||
|
output.write("\n")
|
||||||
|
output.write("];\n\n")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print("usage: fontforge -lang=py -script scripts/generate-fonts.py FONT.ttf OUTPUT.rs", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
font_path = sys.argv[1]
|
||||||
|
output_path = sys.argv[2]
|
||||||
|
|
||||||
|
font = fontforge.open(font_path)
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
generated_fonts = [
|
||||||
|
generate_font(font, temp_dir, name, pixelsize) for name, pixelsize in SIZES
|
||||||
|
]
|
||||||
|
|
||||||
|
with open(output_path, "w", encoding="utf-8") as output:
|
||||||
|
output.write("// @generated by scripts/generate-fonts.py. Do not edit by hand.\n")
|
||||||
|
output.write("// Source font: {}\n\n".format(font_path))
|
||||||
|
output.write("use crate::ui::typography::{BitmapFont, Glyph};\n\n")
|
||||||
|
for generated in generated_fonts:
|
||||||
|
output.write(
|
||||||
|
"// {} px; cell height {} px; {} glyphs; {} alpha bytes.\n".format(
|
||||||
|
generated["pixelsize"],
|
||||||
|
generated["cell_height"],
|
||||||
|
len(generated["glyphs"]),
|
||||||
|
len(generated["bitmap"]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
emit_font(output, generated)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
1209
src/bin/main.rs
1209
src/bin/main.rs
File diff suppressed because it is too large
Load Diff
289
src/bin/web_sim.rs
Normal file
289
src/bin/web_sim.rs
Normal file
|
|
@ -0,0 +1,289 @@
|
||||||
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
|
||||||
|
use core::{cell::UnsafeCell, convert::Infallible, panic::PanicInfo};
|
||||||
|
|
||||||
|
use embedded_graphics::{
|
||||||
|
pixelcolor::{Gray8, GrayColor},
|
||||||
|
prelude::*,
|
||||||
|
primitives::Rectangle,
|
||||||
|
Pixel,
|
||||||
|
};
|
||||||
|
|
||||||
|
use kinda::{
|
||||||
|
App, AppCommand, AppUpdate, InputEvent, WifiAccessPoint, WifiCredentials, WifiEvent,
|
||||||
|
WifiNetworkList, WifiSecurity, WifiSsid,
|
||||||
|
};
|
||||||
|
|
||||||
|
const WIDTH: u32 = 800;
|
||||||
|
const HEIGHT: u32 = 480;
|
||||||
|
const PIXEL_COUNT: usize = (WIDTH * HEIGHT) as usize;
|
||||||
|
|
||||||
|
const EVENT_UP: u32 = 0;
|
||||||
|
const EVENT_DOWN: u32 = 1;
|
||||||
|
const EVENT_SELECT: u32 = 2;
|
||||||
|
const EVENT_BACK: u32 = 3;
|
||||||
|
const EVENT_SELECT_LONG: u32 = 4;
|
||||||
|
const EVENT_BACK_LONG: u32 = 5;
|
||||||
|
|
||||||
|
struct Shared<T> {
|
||||||
|
value: UnsafeCell<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl<T> Sync for Shared<T> {}
|
||||||
|
|
||||||
|
impl<T> Shared<T> {
|
||||||
|
const fn new(value: T) -> Self {
|
||||||
|
Self {
|
||||||
|
value: UnsafeCell::new(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self) -> &mut T {
|
||||||
|
unsafe { &mut *self.value.get() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WebSimulator {
|
||||||
|
app: Option<App>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebSimulator {
|
||||||
|
const fn new() -> Self {
|
||||||
|
Self { app: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.app = Some(App::new());
|
||||||
|
self.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input(&mut self, event: InputEvent) {
|
||||||
|
let update = self.app().update(event);
|
||||||
|
self.apply_update(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(&mut self) -> &mut App {
|
||||||
|
self.app.get_or_insert_with(App::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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());
|
||||||
|
match app.render(&mut display) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(error) => match error {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PixelDisplay<'a> {
|
||||||
|
pixels: &'a mut [u8; PIXEL_COUNT],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PixelDisplay<'a> {
|
||||||
|
fn new(pixels: &'a mut [u8; PIXEL_COUNT]) -> Self {
|
||||||
|
Self { pixels }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_pixel(&mut self, point: Point, color: Gray8) {
|
||||||
|
if point.x < 0 || point.y < 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = point.x as u32;
|
||||||
|
let y = point.y as u32;
|
||||||
|
if x >= WIDTH || y >= HEIGHT {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pixels[(y * WIDTH + x) as usize] = color.luma();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DrawTarget for PixelDisplay<'_> {
|
||||||
|
type Color = Gray8;
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = Pixel<Self::Color>>,
|
||||||
|
{
|
||||||
|
for Pixel(point, color) in pixels {
|
||||||
|
self.set_pixel(point, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> {
|
||||||
|
let bounds = Rectangle::new(Point::new(0, 0), self.size());
|
||||||
|
let area = area.intersection(&bounds);
|
||||||
|
if area.size.width == 0 || area.size.height == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = color.luma();
|
||||||
|
let x_start = area.top_left.x as u32;
|
||||||
|
let y_start = area.top_left.y as u32;
|
||||||
|
let x_end = x_start + area.size.width;
|
||||||
|
let y_end = y_start + area.size.height;
|
||||||
|
|
||||||
|
for y in y_start..y_end {
|
||||||
|
let row_start = (y * WIDTH) as usize;
|
||||||
|
for x in x_start..x_end {
|
||||||
|
self.pixels[row_start + x as usize] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
|
||||||
|
self.pixels.fill(color.luma());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OriginDimensions for PixelDisplay<'_> {
|
||||||
|
fn size(&self) -> Size {
|
||||||
|
Size::new(WIDTH, HEIGHT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static SIMULATOR: Shared<WebSimulator> = Shared::new(WebSimulator::new());
|
||||||
|
static PIXELS: Shared<[u8; PIXEL_COUNT]> = Shared::new([0; PIXEL_COUNT]);
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn kinda_sim_init() {
|
||||||
|
SIMULATOR.get().reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn kinda_sim_input(event: u32) {
|
||||||
|
let Some(event) = input_event(event) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
SIMULATOR.get().input(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn kinda_sim_width() -> u32 {
|
||||||
|
WIDTH
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn kinda_sim_height() -> u32 {
|
||||||
|
HEIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn kinda_sim_pixels() -> *const u8 {
|
||||||
|
PIXELS.get().as_ptr()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn kinda_sim_pixel_len() -> usize {
|
||||||
|
PIXEL_COUNT
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_event(event: u32) -> Option<InputEvent> {
|
||||||
|
match event {
|
||||||
|
EVENT_UP => Some(InputEvent::Up),
|
||||||
|
EVENT_DOWN => Some(InputEvent::Down),
|
||||||
|
EVENT_SELECT => Some(InputEvent::Select),
|
||||||
|
EVENT_BACK => Some(InputEvent::Back),
|
||||||
|
EVENT_SELECT_LONG => Some(InputEvent::SelectLong),
|
||||||
|
EVENT_BACK_LONG => Some(InputEvent::BackLong),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[panic_handler]
|
||||||
|
fn panic(_info: &PanicInfo<'_>) -> ! {
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
9
src/input.rs
Normal file
9
src/input.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum InputEvent {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Select,
|
||||||
|
Back,
|
||||||
|
SelectLong,
|
||||||
|
BackLong,
|
||||||
|
}
|
||||||
11
src/lib.rs
11
src/lib.rs
|
|
@ -1 +1,12 @@
|
||||||
#![no_std]
|
#![no_std]
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
|
||||||
165
src/ui/app.rs
Normal file
165
src/ui/app.rs
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
use embedded_graphics::{
|
||||||
|
pixelcolor::Gray8,
|
||||||
|
prelude::{DrawTarget, OriginDimensions},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{input::InputEvent, wifi::WifiEvent, WifiAccessPoint, WifiCredentials, WifiPassword};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
frame::Frame,
|
||||||
|
screens::{
|
||||||
|
library::{LibraryAction, LibraryScreen},
|
||||||
|
onboarding::{OnboardingAction, OnboardingScreen},
|
||||||
|
reader::{ReaderAction, ReaderScreen},
|
||||||
|
},
|
||||||
|
RefreshHint,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
route: Route,
|
||||||
|
pending_command: Option<AppCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Route {
|
||||||
|
Onboarding(OnboardingScreen),
|
||||||
|
Library(LibraryScreen),
|
||||||
|
Reader(ReaderScreen),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct AppUpdate {
|
||||||
|
pub dirty: bool,
|
||||||
|
pub refresh: RefreshHint,
|
||||||
|
pub command: Option<AppCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum AppCommand {
|
||||||
|
ScanWifi,
|
||||||
|
JoinWifi {
|
||||||
|
access_point: WifiAccessPoint,
|
||||||
|
password: WifiPassword,
|
||||||
|
},
|
||||||
|
JoinSavedWifi {
|
||||||
|
credentials: WifiCredentials,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
route: Route::Onboarding(OnboardingScreen::new()),
|
||||||
|
pending_command: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_saved_wifi(credentials: Option<WifiCredentials>) -> Self {
|
||||||
|
let Some(credentials) = credentials else {
|
||||||
|
return Self::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
let ssid = credentials.ssid.clone();
|
||||||
|
Self {
|
||||||
|
route: Route::Onboarding(OnboardingScreen::new_connecting_saved(ssid)),
|
||||||
|
pending_command: Some(AppCommand::JoinSavedWifi { credentials }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_pending_command(&mut self) -> Option<AppCommand> {
|
||||||
|
self.pending_command.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, event: InputEvent) -> AppUpdate {
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render<D>(&self, target: &mut D) -> Result<(), D::Error>
|
||||||
|
where
|
||||||
|
D: DrawTarget<Color = Gray8> + OriginDimensions,
|
||||||
|
{
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppUpdate {
|
||||||
|
pub const fn new(dirty: bool, refresh: RefreshHint, command: Option<AppCommand>) -> Self {
|
||||||
|
Self {
|
||||||
|
dirty,
|
||||||
|
refresh,
|
||||||
|
command,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for App {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/ui/books.rs
Normal file
39
src/ui/books.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
];
|
||||||
106
src/ui/frame.rs
Normal file
106
src/ui/frame.rs
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
use embedded_graphics::{
|
||||||
|
geometry::{Point, Size},
|
||||||
|
pixelcolor::Gray8,
|
||||||
|
prelude::{DrawTarget, OriginDimensions},
|
||||||
|
primitives::Rectangle,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::ui::{
|
||||||
|
generated_fonts,
|
||||||
|
typography::{TextRenderer, TextStyle},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) struct Frame<'a, D>
|
||||||
|
where
|
||||||
|
D: DrawTarget<Color = Gray8> + OriginDimensions,
|
||||||
|
{
|
||||||
|
target: &'a mut D,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, D> Frame<'a, D>
|
||||||
|
where
|
||||||
|
D: DrawTarget<Color = Gray8> + OriginDimensions,
|
||||||
|
{
|
||||||
|
pub fn new(target: &'a mut D) -> Self {
|
||||||
|
Self { target }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) -> Result<(), D::Error> {
|
||||||
|
self.target.clear(Gray8::new(255))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title(&mut self, text: &str) -> Result<(), D::Error> {
|
||||||
|
self.text(text, Point::new(40, 18), Self::title_style())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn body(&mut self, y: i32, text: &str) -> Result<(), D::Error> {
|
||||||
|
self.text(text, Point::new(48, y), 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn footer(&mut self, text: &str) -> Result<(), D::Error> {
|
||||||
|
let size = self.target.size();
|
||||||
|
let body_style = Self::body_style();
|
||||||
|
let small_style = Self::small_style();
|
||||||
|
let max_width = size.width as i32 - 64;
|
||||||
|
|
||||||
|
if body_style.measure_width(text) <= max_width {
|
||||||
|
let y = size.height as i32 - i32::from(body_style.line_height()) - 6;
|
||||||
|
return self.text(text, Point::new(32, y), body_style);
|
||||||
|
}
|
||||||
|
|
||||||
|
let y = if small_style.measure_width(text) <= max_width {
|
||||||
|
size.height as i32 - i32::from(small_style.line_height()) - 6
|
||||||
|
} else {
|
||||||
|
size.height as i32 - i32::from(small_style.line_height()) * 2 - 6
|
||||||
|
};
|
||||||
|
let bounds = Rectangle::new(
|
||||||
|
Point::new(32, y),
|
||||||
|
Size::new(max_width as u32, u32::from(small_style.line_height()) * 2),
|
||||||
|
);
|
||||||
|
self.wrapped_text(text, bounds, small_style)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paragraph(&mut self, y: i32, text: &str) -> Result<(), D::Error> {
|
||||||
|
let size = self.target.size();
|
||||||
|
let x = 72;
|
||||||
|
let bottom_margin = 88;
|
||||||
|
let width = size.width.saturating_sub((x * 2) as u32);
|
||||||
|
let height = size.height.saturating_sub(y as u32 + bottom_margin);
|
||||||
|
let bounds = Rectangle::new(Point::new(x, y), Size::new(width, height));
|
||||||
|
|
||||||
|
self.wrapped_text(text, bounds, Self::reader_style())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text(&mut self, text: &str, position: Point, style: TextStyle) -> Result<(), D::Error> {
|
||||||
|
TextRenderer::new(self.target).draw_text(text, position, style)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wrapped_text(
|
||||||
|
&mut self,
|
||||||
|
text: &str,
|
||||||
|
bounds: Rectangle,
|
||||||
|
style: TextStyle,
|
||||||
|
) -> Result<(), D::Error> {
|
||||||
|
TextRenderer::new(self.target).draw_wrapped_text(text, bounds, style)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title_style() -> TextStyle {
|
||||||
|
TextStyle::new(&generated_fonts::QUATTRO_TITLE, Gray8::new(0), 46)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body_style() -> TextStyle {
|
||||||
|
TextStyle::new(&generated_fonts::QUATTRO_BODY, Gray8::new(0), 38)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reader_style() -> TextStyle {
|
||||||
|
TextStyle::new(&generated_fonts::QUATTRO_READER, Gray8::new(0), 43)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn small_style() -> TextStyle {
|
||||||
|
TextStyle::new(&generated_fonts::QUATTRO_SMALL, Gray8::new(0), 30)
|
||||||
|
}
|
||||||
|
}
|
||||||
10153
src/ui/generated_fonts.rs
Normal file
10153
src/ui/generated_fonts.rs
Normal file
File diff suppressed because it is too large
Load Diff
15
src/ui/mod.rs
Normal file
15
src/ui/mod.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
mod app;
|
||||||
|
mod books;
|
||||||
|
mod frame;
|
||||||
|
mod generated_fonts;
|
||||||
|
mod screens;
|
||||||
|
mod typography;
|
||||||
|
|
||||||
|
pub use app::{App, AppCommand, AppUpdate};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum RefreshHint {
|
||||||
|
None,
|
||||||
|
Fast,
|
||||||
|
Full,
|
||||||
|
}
|
||||||
86
src/ui/screens/library.rs
Normal file
86
src/ui/screens/library.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
use core::fmt::Write;
|
||||||
|
|
||||||
|
use embedded_graphics::{
|
||||||
|
pixelcolor::Gray8,
|
||||||
|
prelude::{DrawTarget, OriginDimensions},
|
||||||
|
};
|
||||||
|
use heapless::String;
|
||||||
|
|
||||||
|
use crate::input::InputEvent;
|
||||||
|
|
||||||
|
use super::ScreenUpdate;
|
||||||
|
use crate::ui::{books::BOOKS, frame::Frame, RefreshHint};
|
||||||
|
|
||||||
|
pub(crate) struct LibraryScreen {
|
||||||
|
selected: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) enum LibraryAction {
|
||||||
|
None,
|
||||||
|
OpenOnboarding,
|
||||||
|
OpenReader { book_index: usize },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LibraryScreen {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { selected: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_selected(selected: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
selected: selected.min(BOOKS.len() - 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, event: InputEvent) -> ScreenUpdate<LibraryAction> {
|
||||||
|
match event {
|
||||||
|
InputEvent::Up => {
|
||||||
|
self.selected = if self.selected == 0 {
|
||||||
|
BOOKS.len() - 1
|
||||||
|
} else {
|
||||||
|
self.selected - 1
|
||||||
|
};
|
||||||
|
ScreenUpdate::new(true, RefreshHint::Fast, LibraryAction::None)
|
||||||
|
}
|
||||||
|
InputEvent::Down => {
|
||||||
|
self.selected = (self.selected + 1) % BOOKS.len();
|
||||||
|
ScreenUpdate::new(true, RefreshHint::Fast, LibraryAction::None)
|
||||||
|
}
|
||||||
|
InputEvent::Select => ScreenUpdate::new(
|
||||||
|
true,
|
||||||
|
RefreshHint::Full,
|
||||||
|
LibraryAction::OpenReader {
|
||||||
|
book_index: self.selected,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
InputEvent::Back => {
|
||||||
|
ScreenUpdate::new(true, RefreshHint::Full, LibraryAction::OpenOnboarding)
|
||||||
|
}
|
||||||
|
_ => ScreenUpdate::new(false, RefreshHint::None, LibraryAction::None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render<D>(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error>
|
||||||
|
where
|
||||||
|
D: DrawTarget<Color = Gray8> + OriginDimensions,
|
||||||
|
{
|
||||||
|
frame.title("Library")?;
|
||||||
|
frame.small(58, "Mock book list")?;
|
||||||
|
|
||||||
|
let mut y = 100;
|
||||||
|
for (index, book) in BOOKS.iter().enumerate() {
|
||||||
|
let marker = if index == self.selected { ">" } else { " " };
|
||||||
|
let mut row = String::<96>::new();
|
||||||
|
let _ = write!(row, "{} {} - {}", marker, book.title, book.author);
|
||||||
|
frame.body(y, row.as_str())?;
|
||||||
|
y += 38;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.small(
|
||||||
|
274,
|
||||||
|
"Book sync and storage will plug in behind this screen.",
|
||||||
|
)?;
|
||||||
|
frame.footer("Up/Down move Select open Back WiFi")
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/ui/screens/mod.rs
Normal file
32
src/ui/screens/mod.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
pub(crate) mod library;
|
||||||
|
pub(crate) mod onboarding;
|
||||||
|
pub(crate) mod reader;
|
||||||
|
|
||||||
|
use super::{app::AppCommand, RefreshHint};
|
||||||
|
|
||||||
|
pub(crate) struct ScreenUpdate<A> {
|
||||||
|
pub dirty: bool,
|
||||||
|
pub refresh: RefreshHint,
|
||||||
|
pub action: A,
|
||||||
|
pub command: Option<AppCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A> ScreenUpdate<A> {
|
||||||
|
pub fn new(dirty: bool, refresh: RefreshHint, action: A) -> Self {
|
||||||
|
Self {
|
||||||
|
dirty,
|
||||||
|
refresh,
|
||||||
|
action,
|
||||||
|
command: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_command(dirty: bool, refresh: RefreshHint, action: A, command: AppCommand) -> Self {
|
||||||
|
Self {
|
||||||
|
dirty,
|
||||||
|
refresh,
|
||||||
|
action,
|
||||||
|
command: Some(command),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
495
src/ui/screens/onboarding.rs
Normal file
495
src/ui/screens/onboarding.rs
Normal file
|
|
@ -0,0 +1,495 @@
|
||||||
|
use core::fmt::Write;
|
||||||
|
|
||||||
|
use embedded_graphics::{
|
||||||
|
pixelcolor::Gray8,
|
||||||
|
prelude::{DrawTarget, OriginDimensions},
|
||||||
|
};
|
||||||
|
use heapless::String;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
input::InputEvent,
|
||||||
|
ui::app::AppCommand,
|
||||||
|
wifi::{WifiAccessPoint, WifiEvent, WifiNetworkList, WifiPassword, WifiSsid},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::ScreenUpdate;
|
||||||
|
use crate::ui::{frame::Frame, RefreshHint};
|
||||||
|
|
||||||
|
const PASSWORD_CHARS: &[u8] =
|
||||||
|
b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 .-_!?@#$%&*+/=:";
|
||||||
|
|
||||||
|
pub(crate) struct OnboardingScreen {
|
||||||
|
state: OnboardingState,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OnboardingState {
|
||||||
|
ScanIntro { notice: &'static str },
|
||||||
|
Scanning,
|
||||||
|
NetworkList(NetworkListState),
|
||||||
|
PasswordEntry(PasswordEntryState),
|
||||||
|
Joining(JoiningState),
|
||||||
|
SavedWifiConnecting { ssid: WifiSsid },
|
||||||
|
Joined { ssid: WifiSsid },
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NetworkListState {
|
||||||
|
networks: WifiNetworkList,
|
||||||
|
selected: usize,
|
||||||
|
notice: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PasswordEntryState {
|
||||||
|
networks: WifiNetworkList,
|
||||||
|
selected: usize,
|
||||||
|
password: WifiPassword,
|
||||||
|
char_index: usize,
|
||||||
|
notice: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct JoiningState {
|
||||||
|
networks: WifiNetworkList,
|
||||||
|
selected: usize,
|
||||||
|
password: WifiPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) enum OnboardingAction {
|
||||||
|
None,
|
||||||
|
OpenLibrary,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OnboardingScreen {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
state: OnboardingState::ScanIntro { notice: "" },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_connecting_saved(ssid: WifiSsid) -> Self {
|
||||||
|
Self {
|
||||||
|
state: OnboardingState::SavedWifiConnecting { ssid },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, event: InputEvent) -> ScreenUpdate<OnboardingAction> {
|
||||||
|
match &mut self.state {
|
||||||
|
OnboardingState::ScanIntro { .. } => match event {
|
||||||
|
InputEvent::Select => {
|
||||||
|
self.state = OnboardingState::Scanning;
|
||||||
|
ScreenUpdate::with_command(
|
||||||
|
true,
|
||||||
|
RefreshHint::Full,
|
||||||
|
OnboardingAction::None,
|
||||||
|
AppCommand::ScanWifi,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => ScreenUpdate::new(false, RefreshHint::None, OnboardingAction::None),
|
||||||
|
},
|
||||||
|
OnboardingState::Scanning => {
|
||||||
|
ScreenUpdate::new(false, RefreshHint::None, OnboardingAction::None)
|
||||||
|
}
|
||||||
|
OnboardingState::NetworkList(state) => match event {
|
||||||
|
InputEvent::Up => {
|
||||||
|
state.selected = if state.selected == 0 {
|
||||||
|
state.networks.len() - 1
|
||||||
|
} else {
|
||||||
|
state.selected - 1
|
||||||
|
};
|
||||||
|
state.notice = "";
|
||||||
|
ScreenUpdate::new(true, RefreshHint::Fast, OnboardingAction::None)
|
||||||
|
}
|
||||||
|
InputEvent::Down => {
|
||||||
|
state.selected = (state.selected + 1) % state.networks.len();
|
||||||
|
state.notice = "";
|
||||||
|
ScreenUpdate::new(true, RefreshHint::Fast, OnboardingAction::None)
|
||||||
|
}
|
||||||
|
InputEvent::Select => {
|
||||||
|
let access_point = state.networks[state.selected].clone();
|
||||||
|
if access_point.is_locked() {
|
||||||
|
self.state = OnboardingState::PasswordEntry(PasswordEntryState::new(
|
||||||
|
state.networks.clone(),
|
||||||
|
state.selected,
|
||||||
|
));
|
||||||
|
ScreenUpdate::new(true, RefreshHint::Full, OnboardingAction::None)
|
||||||
|
} else {
|
||||||
|
let password = WifiPassword::new();
|
||||||
|
self.state = OnboardingState::Joining(JoiningState {
|
||||||
|
networks: state.networks.clone(),
|
||||||
|
selected: state.selected,
|
||||||
|
password: password.clone(),
|
||||||
|
});
|
||||||
|
ScreenUpdate::with_command(
|
||||||
|
true,
|
||||||
|
RefreshHint::Full,
|
||||||
|
OnboardingAction::None,
|
||||||
|
AppCommand::JoinWifi {
|
||||||
|
access_point,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputEvent::Back | InputEvent::SelectLong => {
|
||||||
|
self.state = OnboardingState::Scanning;
|
||||||
|
ScreenUpdate::with_command(
|
||||||
|
true,
|
||||||
|
RefreshHint::Full,
|
||||||
|
OnboardingAction::None,
|
||||||
|
AppCommand::ScanWifi,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => ScreenUpdate::new(false, RefreshHint::None, OnboardingAction::None),
|
||||||
|
},
|
||||||
|
OnboardingState::PasswordEntry(state) => match event {
|
||||||
|
InputEvent::Up => {
|
||||||
|
state.previous_char();
|
||||||
|
ScreenUpdate::new(true, RefreshHint::Fast, OnboardingAction::None)
|
||||||
|
}
|
||||||
|
InputEvent::Down => {
|
||||||
|
state.next_char();
|
||||||
|
ScreenUpdate::new(true, RefreshHint::Fast, OnboardingAction::None)
|
||||||
|
}
|
||||||
|
InputEvent::Select => {
|
||||||
|
state.append_char();
|
||||||
|
ScreenUpdate::new(true, RefreshHint::Fast, OnboardingAction::None)
|
||||||
|
}
|
||||||
|
InputEvent::Back => {
|
||||||
|
state.delete_char();
|
||||||
|
ScreenUpdate::new(true, RefreshHint::Fast, OnboardingAction::None)
|
||||||
|
}
|
||||||
|
InputEvent::SelectLong => {
|
||||||
|
if state.can_join() {
|
||||||
|
let access_point = state.access_point().clone();
|
||||||
|
let password = state.password.clone();
|
||||||
|
self.state = OnboardingState::Joining(JoiningState {
|
||||||
|
networks: state.networks.clone(),
|
||||||
|
selected: state.selected,
|
||||||
|
password: password.clone(),
|
||||||
|
});
|
||||||
|
ScreenUpdate::with_command(
|
||||||
|
true,
|
||||||
|
RefreshHint::Full,
|
||||||
|
OnboardingAction::None,
|
||||||
|
AppCommand::JoinWifi {
|
||||||
|
access_point,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
state.notice = "Password must be 8-63 characters";
|
||||||
|
ScreenUpdate::new(true, RefreshHint::Fast, OnboardingAction::None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputEvent::BackLong => {
|
||||||
|
self.state = OnboardingState::NetworkList(NetworkListState {
|
||||||
|
networks: state.networks.clone(),
|
||||||
|
selected: state.selected,
|
||||||
|
notice: "",
|
||||||
|
});
|
||||||
|
ScreenUpdate::new(true, RefreshHint::Full, OnboardingAction::None)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
OnboardingState::Joining(_) | OnboardingState::SavedWifiConnecting { .. } => {
|
||||||
|
ScreenUpdate::new(false, RefreshHint::None, OnboardingAction::None)
|
||||||
|
}
|
||||||
|
OnboardingState::Joined { .. } => match event {
|
||||||
|
InputEvent::Select => {
|
||||||
|
ScreenUpdate::new(true, RefreshHint::Full, OnboardingAction::OpenLibrary)
|
||||||
|
}
|
||||||
|
InputEvent::Back => {
|
||||||
|
self.state = OnboardingState::ScanIntro { notice: "" };
|
||||||
|
ScreenUpdate::new(true, RefreshHint::Full, OnboardingAction::None)
|
||||||
|
}
|
||||||
|
_ => ScreenUpdate::new(false, RefreshHint::None, OnboardingAction::None),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_wifi_event(&mut self, event: WifiEvent) -> ScreenUpdate<OnboardingAction> {
|
||||||
|
match event {
|
||||||
|
WifiEvent::ScanFinished(Ok(networks)) => {
|
||||||
|
if networks.is_empty() {
|
||||||
|
self.state = OnboardingState::ScanIntro {
|
||||||
|
notice: "No networks found",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
self.state = OnboardingState::NetworkList(NetworkListState {
|
||||||
|
networks,
|
||||||
|
selected: 0,
|
||||||
|
notice: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ScreenUpdate::new(true, RefreshHint::Full, OnboardingAction::None)
|
||||||
|
}
|
||||||
|
WifiEvent::ScanFinished(Err(error)) => {
|
||||||
|
self.state = OnboardingState::ScanIntro {
|
||||||
|
notice: error.message(),
|
||||||
|
};
|
||||||
|
ScreenUpdate::new(true, RefreshHint::Full, OnboardingAction::None)
|
||||||
|
}
|
||||||
|
WifiEvent::JoinFinished(Ok(ssid)) => {
|
||||||
|
let action = if matches!(self.state, OnboardingState::SavedWifiConnecting { .. }) {
|
||||||
|
OnboardingAction::OpenLibrary
|
||||||
|
} else {
|
||||||
|
OnboardingAction::None
|
||||||
|
};
|
||||||
|
self.state = OnboardingState::Joined { ssid };
|
||||||
|
ScreenUpdate::new(true, RefreshHint::Full, action)
|
||||||
|
}
|
||||||
|
WifiEvent::JoinFinished(Err(error)) => {
|
||||||
|
let previous =
|
||||||
|
core::mem::replace(&mut self.state, OnboardingState::ScanIntro { notice: "" });
|
||||||
|
|
||||||
|
self.state = match previous {
|
||||||
|
OnboardingState::Joining(state) => {
|
||||||
|
OnboardingState::PasswordEntry(PasswordEntryState {
|
||||||
|
networks: state.networks,
|
||||||
|
selected: state.selected,
|
||||||
|
password: state.password,
|
||||||
|
char_index: 0,
|
||||||
|
notice: error.message(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => OnboardingState::ScanIntro {
|
||||||
|
notice: error.message(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
ScreenUpdate::new(true, RefreshHint::Full, OnboardingAction::None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render<D>(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error>
|
||||||
|
where
|
||||||
|
D: DrawTarget<Color = Gray8> + OriginDimensions,
|
||||||
|
{
|
||||||
|
match &self.state {
|
||||||
|
OnboardingState::ScanIntro { notice } => self.render_scan_intro(frame, notice),
|
||||||
|
OnboardingState::Scanning => self.render_scanning(frame),
|
||||||
|
OnboardingState::NetworkList(state) => self.render_network_list(frame, state),
|
||||||
|
OnboardingState::PasswordEntry(state) => self.render_password_entry(frame, state),
|
||||||
|
OnboardingState::Joining(state) => self.render_joining(frame, state),
|
||||||
|
OnboardingState::SavedWifiConnecting { ssid } => {
|
||||||
|
self.render_saved_wifi_connecting(frame, ssid)
|
||||||
|
}
|
||||||
|
OnboardingState::Joined { ssid } => self.render_joined(frame, ssid),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_scan_intro<D>(
|
||||||
|
&self,
|
||||||
|
frame: &mut Frame<'_, D>,
|
||||||
|
notice: &'static str,
|
||||||
|
) -> Result<(), D::Error>
|
||||||
|
where
|
||||||
|
D: DrawTarget<Color = Gray8> + OriginDimensions,
|
||||||
|
{
|
||||||
|
frame.title("WiFi setup")?;
|
||||||
|
frame.body(86, "Connect this device before loading books.")?;
|
||||||
|
frame.body(122, "Press select to scan for nearby networks.")?;
|
||||||
|
if !notice.is_empty() {
|
||||||
|
frame.small(174, notice)?;
|
||||||
|
}
|
||||||
|
frame.footer("Select: scan")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_scanning<D>(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error>
|
||||||
|
where
|
||||||
|
D: DrawTarget<Color = Gray8> + OriginDimensions,
|
||||||
|
{
|
||||||
|
frame.title("Scanning")?;
|
||||||
|
frame.body(96, "Looking for WiFi networks...")?;
|
||||||
|
frame.small(148, "This can take a few seconds.")?;
|
||||||
|
frame.footer("Please wait")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_network_list<D>(
|
||||||
|
&self,
|
||||||
|
frame: &mut Frame<'_, D>,
|
||||||
|
state: &NetworkListState,
|
||||||
|
) -> Result<(), D::Error>
|
||||||
|
where
|
||||||
|
D: DrawTarget<Color = Gray8> + OriginDimensions,
|
||||||
|
{
|
||||||
|
frame.title("Choose network")?;
|
||||||
|
|
||||||
|
let mut count_line = String::<48>::new();
|
||||||
|
let _ = write!(count_line, "{} networks found", state.networks.len());
|
||||||
|
frame.small(58, count_line.as_str())?;
|
||||||
|
|
||||||
|
let visible_rows = 8;
|
||||||
|
let first_row = if state.selected >= visible_rows {
|
||||||
|
state.selected + 1 - visible_rows
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let last_row = (first_row + visible_rows).min(state.networks.len());
|
||||||
|
|
||||||
|
let mut y = 100;
|
||||||
|
for index in first_row..last_row {
|
||||||
|
let network = &state.networks[index];
|
||||||
|
let marker = if index == state.selected { ">" } else { " " };
|
||||||
|
let mut row = String::<128>::new();
|
||||||
|
let _ = write!(
|
||||||
|
row,
|
||||||
|
"{} {} {} {}",
|
||||||
|
marker,
|
||||||
|
network.ssid.as_str(),
|
||||||
|
network.signal_label(),
|
||||||
|
network.security.label()
|
||||||
|
);
|
||||||
|
frame.body(y, row.as_str())?;
|
||||||
|
y += 34;
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.networks.len() > visible_rows {
|
||||||
|
let mut range_line = String::<48>::new();
|
||||||
|
let _ = write!(range_line, "Showing {}-{}", first_row + 1, last_row);
|
||||||
|
frame.small(380, range_line.as_str())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !state.notice.is_empty() {
|
||||||
|
frame.small(400, state.notice)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.footer("Up/Down move Select choose Back rescan")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_password_entry<D>(
|
||||||
|
&self,
|
||||||
|
frame: &mut Frame<'_, D>,
|
||||||
|
state: &PasswordEntryState,
|
||||||
|
) -> Result<(), D::Error>
|
||||||
|
where
|
||||||
|
D: DrawTarget<Color = Gray8> + OriginDimensions,
|
||||||
|
{
|
||||||
|
let access_point = state.access_point();
|
||||||
|
let mut network_line = String::<96>::new();
|
||||||
|
let _ = write!(network_line, "Network: {}", access_point.ssid.as_str());
|
||||||
|
|
||||||
|
frame.title("WiFi password")?;
|
||||||
|
frame.body(66, network_line.as_str())?;
|
||||||
|
frame.body(112, "Password:")?;
|
||||||
|
if state.password.is_empty() {
|
||||||
|
frame.body(146, "<empty>")?;
|
||||||
|
} else {
|
||||||
|
frame.body(146, state.password.as_str())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.body(204, "Current char:")?;
|
||||||
|
let mut char_line = String::<24>::new();
|
||||||
|
if state.current_char() == ' ' {
|
||||||
|
let _ = write!(char_line, "[ space ]");
|
||||||
|
} else {
|
||||||
|
let _ = write!(char_line, "[ {} ]", state.current_char());
|
||||||
|
}
|
||||||
|
frame.body(238, char_line.as_str())?;
|
||||||
|
|
||||||
|
if !state.notice.is_empty() {
|
||||||
|
frame.small(292, state.notice)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.footer("Up/Down char Select add Back del Hold Select join")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_joining<D>(
|
||||||
|
&self,
|
||||||
|
frame: &mut Frame<'_, D>,
|
||||||
|
state: &JoiningState,
|
||||||
|
) -> Result<(), D::Error>
|
||||||
|
where
|
||||||
|
D: DrawTarget<Color = Gray8> + OriginDimensions,
|
||||||
|
{
|
||||||
|
let network = &state.networks[state.selected];
|
||||||
|
let mut network_line = String::<96>::new();
|
||||||
|
let _ = write!(network_line, "Joining {}", network.ssid.as_str());
|
||||||
|
|
||||||
|
frame.title("Joining")?;
|
||||||
|
frame.body(92, network_line.as_str())?;
|
||||||
|
frame.body(136, "Waiting for the access point...")?;
|
||||||
|
frame.footer("Please wait")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_saved_wifi_connecting<D>(
|
||||||
|
&self,
|
||||||
|
frame: &mut Frame<'_, D>,
|
||||||
|
ssid: &WifiSsid,
|
||||||
|
) -> Result<(), D::Error>
|
||||||
|
where
|
||||||
|
D: DrawTarget<Color = Gray8> + OriginDimensions,
|
||||||
|
{
|
||||||
|
let mut network_line = String::<96>::new();
|
||||||
|
let _ = write!(network_line, "Connecting to {}", ssid.as_str());
|
||||||
|
|
||||||
|
frame.title("WiFi")?;
|
||||||
|
frame.body(92, network_line.as_str())?;
|
||||||
|
frame.body(136, "Using saved credentials...")?;
|
||||||
|
frame.footer("Please wait")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_joined<D>(&self, frame: &mut Frame<'_, D>, ssid: &WifiSsid) -> Result<(), D::Error>
|
||||||
|
where
|
||||||
|
D: DrawTarget<Color = Gray8> + OriginDimensions,
|
||||||
|
{
|
||||||
|
let mut network_line = String::<96>::new();
|
||||||
|
let _ = write!(network_line, "Connected to {}", ssid.as_str());
|
||||||
|
|
||||||
|
frame.title("Connected")?;
|
||||||
|
frame.body(92, network_line.as_str())?;
|
||||||
|
frame.body(136, "Next screen: library.")?;
|
||||||
|
frame.footer("Select continue Back scan again")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PasswordEntryState {
|
||||||
|
fn new(networks: WifiNetworkList, selected: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
networks,
|
||||||
|
selected,
|
||||||
|
password: WifiPassword::new(),
|
||||||
|
char_index: 0,
|
||||||
|
notice: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn access_point(&self) -> &WifiAccessPoint {
|
||||||
|
&self.networks[self.selected]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn previous_char(&mut self) {
|
||||||
|
self.notice = "";
|
||||||
|
self.char_index = if self.char_index == 0 {
|
||||||
|
PASSWORD_CHARS.len() - 1
|
||||||
|
} else {
|
||||||
|
self.char_index - 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_char(&mut self) {
|
||||||
|
self.notice = "";
|
||||||
|
self.char_index = (self.char_index + 1) % PASSWORD_CHARS.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_char(&mut self) {
|
||||||
|
self.notice = "";
|
||||||
|
if self.password.push(self.current_char()).is_err() {
|
||||||
|
self.notice = "Password is full";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_char(&mut self) {
|
||||||
|
self.notice = "";
|
||||||
|
if self.password.pop().is_none() {
|
||||||
|
self.notice = "Nothing to delete";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_join(&self) -> bool {
|
||||||
|
!self.access_point().is_locked() || (self.password.len() >= 8 && self.password.len() <= 63)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_char(&self) -> char {
|
||||||
|
char::from(PASSWORD_CHARS[self.char_index])
|
||||||
|
}
|
||||||
|
}
|
||||||
98
src/ui/screens/reader.rs
Normal file
98
src/ui/screens/reader.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
use core::fmt::Write;
|
||||||
|
|
||||||
|
use embedded_graphics::{
|
||||||
|
pixelcolor::Gray8,
|
||||||
|
prelude::{DrawTarget, OriginDimensions},
|
||||||
|
};
|
||||||
|
use heapless::String;
|
||||||
|
|
||||||
|
use crate::input::InputEvent;
|
||||||
|
|
||||||
|
use super::ScreenUpdate;
|
||||||
|
use crate::ui::{books::BOOKS, frame::Frame, RefreshHint};
|
||||||
|
|
||||||
|
pub(crate) struct ReaderScreen {
|
||||||
|
book_index: usize,
|
||||||
|
page_index: usize,
|
||||||
|
show_help: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) enum ReaderAction {
|
||||||
|
None,
|
||||||
|
OpenLibrary { selected_book: usize },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReaderScreen {
|
||||||
|
pub fn new(book_index: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
book_index,
|
||||||
|
page_index: 0,
|
||||||
|
show_help: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, event: InputEvent) -> ScreenUpdate<ReaderAction> {
|
||||||
|
match event {
|
||||||
|
InputEvent::Up => {
|
||||||
|
if self.page_index > 0 {
|
||||||
|
self.page_index -= 1;
|
||||||
|
self.show_help = false;
|
||||||
|
ScreenUpdate::new(true, RefreshHint::Fast, ReaderAction::None)
|
||||||
|
} else {
|
||||||
|
ScreenUpdate::new(false, RefreshHint::None, ReaderAction::None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputEvent::Down => {
|
||||||
|
let last_page = BOOKS[self.book_index].pages.len() - 1;
|
||||||
|
if self.page_index < last_page {
|
||||||
|
self.page_index += 1;
|
||||||
|
self.show_help = false;
|
||||||
|
ScreenUpdate::new(true, RefreshHint::Fast, ReaderAction::None)
|
||||||
|
} else {
|
||||||
|
ScreenUpdate::new(false, RefreshHint::None, ReaderAction::None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputEvent::Select => {
|
||||||
|
self.show_help = !self.show_help;
|
||||||
|
ScreenUpdate::new(true, RefreshHint::Fast, ReaderAction::None)
|
||||||
|
}
|
||||||
|
InputEvent::Back => ScreenUpdate::new(
|
||||||
|
true,
|
||||||
|
RefreshHint::Full,
|
||||||
|
ReaderAction::OpenLibrary {
|
||||||
|
selected_book: self.book_index,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_ => ScreenUpdate::new(false, RefreshHint::None, ReaderAction::None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render<D>(&self, frame: &mut Frame<'_, D>) -> Result<(), D::Error>
|
||||||
|
where
|
||||||
|
D: DrawTarget<Color = Gray8> + OriginDimensions,
|
||||||
|
{
|
||||||
|
let book = &BOOKS[self.book_index];
|
||||||
|
let mut heading = String::<96>::new();
|
||||||
|
let _ = write!(heading, "{} - {}", book.title, book.author);
|
||||||
|
|
||||||
|
frame.title("Reader")?;
|
||||||
|
frame.small(58, heading.as_str())?;
|
||||||
|
frame.paragraph(100, book.pages[self.page_index])?;
|
||||||
|
|
||||||
|
let mut page_line = String::<48>::new();
|
||||||
|
let _ = write!(
|
||||||
|
page_line,
|
||||||
|
"Page {} of {}",
|
||||||
|
self.page_index + 1,
|
||||||
|
book.pages.len()
|
||||||
|
);
|
||||||
|
frame.small(392, page_line.as_str())?;
|
||||||
|
|
||||||
|
if self.show_help {
|
||||||
|
frame.small(346, "Controls: Up prev, Down next, Back library")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.footer("Up/Down page Select controls Back library")
|
||||||
|
}
|
||||||
|
}
|
||||||
384
src/ui/typography.rs
Normal file
384
src/ui/typography.rs
Normal file
|
|
@ -0,0 +1,384 @@
|
||||||
|
use embedded_graphics::{
|
||||||
|
geometry::Point,
|
||||||
|
pixelcolor::{Gray8, GrayColor},
|
||||||
|
prelude::{DrawTarget, Pixel},
|
||||||
|
primitives::Rectangle,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) struct Glyph {
|
||||||
|
pub codepoint: u32,
|
||||||
|
pub width: u8,
|
||||||
|
pub height: u8,
|
||||||
|
pub bitmap_offset: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct BitmapFont {
|
||||||
|
pub cell_height: u8,
|
||||||
|
pub glyphs: &'static [Glyph],
|
||||||
|
pub bitmap: &'static [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) struct TextStyle {
|
||||||
|
font: &'static BitmapFont,
|
||||||
|
color: Gray8,
|
||||||
|
line_height: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct TextRenderer<'a, D>
|
||||||
|
where
|
||||||
|
D: DrawTarget<Color = Gray8>,
|
||||||
|
{
|
||||||
|
target: &'a mut D,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WrapCursor {
|
||||||
|
left: i32,
|
||||||
|
right: i32,
|
||||||
|
bottom: i32,
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
has_text: bool,
|
||||||
|
pending_space: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GlyphPixels<'a> {
|
||||||
|
bitmap: &'a [u8],
|
||||||
|
origin: Point,
|
||||||
|
color: Gray8,
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
next_pixel: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextStyle {
|
||||||
|
pub const fn new(font: &'static BitmapFont, color: Gray8, line_height: u8) -> Self {
|
||||||
|
Self {
|
||||||
|
font,
|
||||||
|
color,
|
||||||
|
line_height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn char_width(&self, character: char) -> i32 {
|
||||||
|
if character == '\t' {
|
||||||
|
return self.space_width() * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.font
|
||||||
|
.glyph_for(character)
|
||||||
|
.map(|glyph| i32::from(glyph.width))
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn measure_width(&self, text: &str) -> i32 {
|
||||||
|
text.chars()
|
||||||
|
.map(|character| self.char_width(character))
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn line_height(&self) -> u8 {
|
||||||
|
self.line_height
|
||||||
|
}
|
||||||
|
|
||||||
|
fn space_width(&self) -> i32 {
|
||||||
|
self.char_width(' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BitmapFont {
|
||||||
|
fn glyph_for(&self, character: char) -> Option<&Glyph> {
|
||||||
|
let codepoint = match character {
|
||||||
|
'\u{00a0}' => ' ' as u32,
|
||||||
|
_ => character as u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.glyph(codepoint).or_else(|| self.glyph('?' as u32))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn glyph(&self, codepoint: u32) -> Option<&Glyph> {
|
||||||
|
for glyph in self.glyphs {
|
||||||
|
if glyph.codepoint == codepoint {
|
||||||
|
return Some(glyph);
|
||||||
|
}
|
||||||
|
|
||||||
|
if glyph.codepoint > codepoint {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, D> TextRenderer<'a, D>
|
||||||
|
where
|
||||||
|
D: DrawTarget<Color = Gray8>,
|
||||||
|
{
|
||||||
|
pub fn new(target: &'a mut D) -> Self {
|
||||||
|
Self { target }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_text(
|
||||||
|
&mut self,
|
||||||
|
text: &str,
|
||||||
|
position: Point,
|
||||||
|
style: TextStyle,
|
||||||
|
) -> Result<(), D::Error> {
|
||||||
|
let mut x = position.x;
|
||||||
|
let mut y = position.y;
|
||||||
|
|
||||||
|
for character in text.chars() {
|
||||||
|
match character {
|
||||||
|
'\r' => {}
|
||||||
|
'\n' => {
|
||||||
|
x = position.x;
|
||||||
|
y += i32::from(style.line_height);
|
||||||
|
}
|
||||||
|
'\t' => {
|
||||||
|
x += style.space_width() * 4;
|
||||||
|
}
|
||||||
|
' ' | '\u{00a0}' => {
|
||||||
|
x += style.space_width();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if let Some(glyph) = style.font.glyph_for(character) {
|
||||||
|
self.draw_glyph(glyph, Point::new(x, y), style)?;
|
||||||
|
x += i32::from(glyph.width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_wrapped_text(
|
||||||
|
&mut self,
|
||||||
|
text: &str,
|
||||||
|
bounds: Rectangle,
|
||||||
|
style: TextStyle,
|
||||||
|
) -> Result<(), D::Error> {
|
||||||
|
let mut cursor = WrapCursor::new(bounds);
|
||||||
|
let mut index = 0;
|
||||||
|
|
||||||
|
while index < text.len() {
|
||||||
|
let Some(character) = text[index..].chars().next() else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
if character == '\r' {
|
||||||
|
index += character.len_utf8();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if character == '\n' {
|
||||||
|
cursor.new_line(style);
|
||||||
|
index += character.len_utf8();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if character.is_whitespace() {
|
||||||
|
cursor.mark_space();
|
||||||
|
index += character.len_utf8();
|
||||||
|
while index < text.len() {
|
||||||
|
let Some(next) = text[index..].chars().next() else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
if next == '\n' || !next.is_whitespace() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
index += next.len_utf8();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let word_start = index;
|
||||||
|
while index < text.len() {
|
||||||
|
let Some(next) = text[index..].chars().next() else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
if next.is_whitespace() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
index += next.len_utf8();
|
||||||
|
}
|
||||||
|
|
||||||
|
let word = &text[word_start..index];
|
||||||
|
self.draw_wrapped_word(word, &mut cursor, style)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_wrapped_word(
|
||||||
|
&mut self,
|
||||||
|
word: &str,
|
||||||
|
cursor: &mut WrapCursor,
|
||||||
|
style: TextStyle,
|
||||||
|
) -> Result<(), D::Error> {
|
||||||
|
let word_width = style.measure_width(word);
|
||||||
|
let space_width = cursor.pending_space_width(style);
|
||||||
|
|
||||||
|
if cursor.has_text && cursor.x + space_width + word_width > cursor.right {
|
||||||
|
cursor.new_line(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
if word_width <= cursor.line_width() {
|
||||||
|
if !cursor.can_draw(style) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.apply_pending_space(style);
|
||||||
|
self.draw_text(word, Point::new(cursor.x, cursor.y), style)?;
|
||||||
|
cursor.x += word_width;
|
||||||
|
cursor.has_text = true;
|
||||||
|
cursor.pending_space = false;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.apply_pending_space(style);
|
||||||
|
for character in word.chars() {
|
||||||
|
let character_width = style.char_width(character);
|
||||||
|
if cursor.has_text && cursor.x + character_width > cursor.right {
|
||||||
|
cursor.new_line(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cursor.can_draw(style) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(glyph) = style.font.glyph_for(character) {
|
||||||
|
self.draw_glyph(glyph, Point::new(cursor.x, cursor.y), style)?;
|
||||||
|
}
|
||||||
|
cursor.x += character_width;
|
||||||
|
cursor.has_text = true;
|
||||||
|
}
|
||||||
|
cursor.pending_space = false;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_glyph(
|
||||||
|
&mut self,
|
||||||
|
glyph: &Glyph,
|
||||||
|
position: Point,
|
||||||
|
style: TextStyle,
|
||||||
|
) -> Result<(), D::Error> {
|
||||||
|
if glyph.width == 0 || glyph.height == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let bitmap_offset = glyph.bitmap_offset as usize;
|
||||||
|
let bitmap_len = (usize::from(glyph.width) * usize::from(glyph.height)).div_ceil(2);
|
||||||
|
let bitmap = &style.font.bitmap[bitmap_offset..bitmap_offset + bitmap_len];
|
||||||
|
self.target.draw_iter(GlyphPixels::new(
|
||||||
|
bitmap,
|
||||||
|
position,
|
||||||
|
style.color,
|
||||||
|
glyph.width,
|
||||||
|
glyph.height,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WrapCursor {
|
||||||
|
fn new(bounds: Rectangle) -> Self {
|
||||||
|
let left = bounds.top_left.x;
|
||||||
|
let right = left + bounds.size.width as i32;
|
||||||
|
let bottom = bounds.top_left.y + bounds.size.height as i32;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
bottom,
|
||||||
|
x: left,
|
||||||
|
y: bounds.top_left.y,
|
||||||
|
has_text: false,
|
||||||
|
pending_space: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_width(&self) -> i32 {
|
||||||
|
self.right - self.left
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_draw(&self, style: TextStyle) -> bool {
|
||||||
|
self.y + i32::from(style.font.cell_height) <= self.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_line(&mut self, style: TextStyle) {
|
||||||
|
self.x = self.left;
|
||||||
|
self.y += i32::from(style.line_height);
|
||||||
|
self.has_text = false;
|
||||||
|
self.pending_space = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_space(&mut self) {
|
||||||
|
if self.has_text {
|
||||||
|
self.pending_space = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pending_space_width(&self, style: TextStyle) -> i32 {
|
||||||
|
if self.has_text && self.pending_space {
|
||||||
|
style.space_width()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_pending_space(&mut self, style: TextStyle) {
|
||||||
|
self.x += self.pending_space_width(style);
|
||||||
|
self.pending_space = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GlyphPixels<'a> {
|
||||||
|
fn new(bitmap: &'a [u8], origin: Point, color: Gray8, width: u8, height: u8) -> Self {
|
||||||
|
Self {
|
||||||
|
bitmap,
|
||||||
|
origin,
|
||||||
|
color,
|
||||||
|
width: usize::from(width),
|
||||||
|
height: usize::from(height),
|
||||||
|
next_pixel: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blend_luma(color: Gray8, alpha: u8) -> u8 {
|
||||||
|
let foreground = u16::from(color.luma());
|
||||||
|
let alpha = u16::from(alpha) * 17;
|
||||||
|
let luma = (foreground * alpha + 255 * (255 - alpha)) / 255;
|
||||||
|
luma as u8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for GlyphPixels<'_> {
|
||||||
|
type Item = Pixel<Gray8>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
while self.next_pixel < self.width * self.height {
|
||||||
|
let pixel = self.next_pixel;
|
||||||
|
self.next_pixel += 1;
|
||||||
|
|
||||||
|
let x = pixel % self.width;
|
||||||
|
let y = pixel / self.width;
|
||||||
|
let byte = self.bitmap[pixel / 2];
|
||||||
|
let alpha = if pixel % 2 == 0 {
|
||||||
|
byte >> 4
|
||||||
|
} else {
|
||||||
|
byte & 0x0F
|
||||||
|
};
|
||||||
|
if alpha != 0 {
|
||||||
|
return Some(Pixel(
|
||||||
|
Point::new(self.origin.x + x as i32, self.origin.y + y as i32),
|
||||||
|
Gray8::new(Self::blend_luma(self.color, alpha)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
219
src/wifi.rs
Normal file
219
src/wifi.rs
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
use heapless::{String, Vec};
|
||||||
|
|
||||||
|
pub const MAX_NETWORKS: usize = 12;
|
||||||
|
pub const WIFI_CREDENTIAL_RECORD_BYTES: usize = 112;
|
||||||
|
|
||||||
|
const WIFI_CREDENTIAL_MAGIC: &[u8; 8] = b"KINDAWF1";
|
||||||
|
const WIFI_CREDENTIAL_CHECKSUM_OFFSET: usize = 8;
|
||||||
|
const WIFI_CREDENTIAL_SECURITY_OFFSET: usize = 12;
|
||||||
|
const WIFI_CREDENTIAL_SSID_LEN_OFFSET: usize = 13;
|
||||||
|
const WIFI_CREDENTIAL_PASSWORD_LEN_OFFSET: usize = 14;
|
||||||
|
const WIFI_CREDENTIAL_SSID_OFFSET: usize = 16;
|
||||||
|
const WIFI_CREDENTIAL_PASSWORD_OFFSET: usize = WIFI_CREDENTIAL_SSID_OFFSET + 32;
|
||||||
|
|
||||||
|
pub type WifiNetworkList = Vec<WifiAccessPoint, MAX_NETWORKS>;
|
||||||
|
pub type WifiPassword = String<64>;
|
||||||
|
pub type WifiSsid = String<32>;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct WifiAccessPoint {
|
||||||
|
pub ssid: WifiSsid,
|
||||||
|
pub bssid: [u8; 6],
|
||||||
|
pub channel: u8,
|
||||||
|
pub signal_strength: i8,
|
||||||
|
pub security: WifiSecurity,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WifiAccessPoint {
|
||||||
|
pub fn is_locked(&self) -> bool {
|
||||||
|
self.security.is_locked()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn signal_label(&self) -> &'static str {
|
||||||
|
match self.signal_strength {
|
||||||
|
-50..=i8::MAX => "strong",
|
||||||
|
-70..=-51 => "medium",
|
||||||
|
_ => "weak",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum WifiSecurity {
|
||||||
|
Open,
|
||||||
|
Wep,
|
||||||
|
Wpa,
|
||||||
|
Wpa2Personal,
|
||||||
|
WpaWpa2Personal,
|
||||||
|
Wpa2Wpa3Personal,
|
||||||
|
Wpa3Personal,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WifiSecurity {
|
||||||
|
pub fn is_locked(self) -> bool {
|
||||||
|
!matches!(self, Self::Open)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Open => "open",
|
||||||
|
Self::Wep => "wep",
|
||||||
|
Self::Wpa => "wpa",
|
||||||
|
Self::Wpa2Personal => "wpa2",
|
||||||
|
Self::WpaWpa2Personal => "wpa/wpa2",
|
||||||
|
Self::Wpa2Wpa3Personal => "wpa2/wpa3",
|
||||||
|
Self::Wpa3Personal => "wpa3",
|
||||||
|
Self::Other => "locked",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_storage_byte(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::Open => 0,
|
||||||
|
Self::Wep => 1,
|
||||||
|
Self::Wpa => 2,
|
||||||
|
Self::Wpa2Personal => 3,
|
||||||
|
Self::WpaWpa2Personal => 4,
|
||||||
|
Self::Wpa2Wpa3Personal => 5,
|
||||||
|
Self::Wpa3Personal => 6,
|
||||||
|
Self::Other => 7,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_storage_byte(value: u8) -> Option<Self> {
|
||||||
|
match value {
|
||||||
|
0 => Some(Self::Open),
|
||||||
|
1 => Some(Self::Wep),
|
||||||
|
2 => Some(Self::Wpa),
|
||||||
|
3 => Some(Self::Wpa2Personal),
|
||||||
|
4 => Some(Self::WpaWpa2Personal),
|
||||||
|
5 => Some(Self::Wpa2Wpa3Personal),
|
||||||
|
6 => Some(Self::Wpa3Personal),
|
||||||
|
7 => Some(Self::Other),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct WifiCredentials {
|
||||||
|
pub ssid: WifiSsid,
|
||||||
|
pub password: WifiPassword,
|
||||||
|
pub security: WifiSecurity,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WifiCredentials {
|
||||||
|
pub fn new(ssid: WifiSsid, password: WifiPassword, security: WifiSecurity) -> Self {
|
||||||
|
Self {
|
||||||
|
ssid,
|
||||||
|
password,
|
||||||
|
security,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_access_point(access_point: &WifiAccessPoint, password: WifiPassword) -> Self {
|
||||||
|
Self::new(access_point.ssid.clone(), password, access_point.security)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(&self) -> [u8; WIFI_CREDENTIAL_RECORD_BYTES] {
|
||||||
|
let mut record = [0u8; WIFI_CREDENTIAL_RECORD_BYTES];
|
||||||
|
record[..WIFI_CREDENTIAL_MAGIC.len()].copy_from_slice(WIFI_CREDENTIAL_MAGIC);
|
||||||
|
record[WIFI_CREDENTIAL_SECURITY_OFFSET] = self.security.to_storage_byte();
|
||||||
|
record[WIFI_CREDENTIAL_SSID_LEN_OFFSET] = self.ssid.len() as u8;
|
||||||
|
record[WIFI_CREDENTIAL_PASSWORD_LEN_OFFSET] = self.password.len() as u8;
|
||||||
|
record[WIFI_CREDENTIAL_SSID_OFFSET..][..self.ssid.len()]
|
||||||
|
.copy_from_slice(self.ssid.as_bytes());
|
||||||
|
record[WIFI_CREDENTIAL_PASSWORD_OFFSET..][..self.password.len()]
|
||||||
|
.copy_from_slice(self.password.as_bytes());
|
||||||
|
|
||||||
|
let checksum = Self::checksum(&record[WIFI_CREDENTIAL_SECURITY_OFFSET..]);
|
||||||
|
record[WIFI_CREDENTIAL_CHECKSUM_OFFSET..][..4].copy_from_slice(&checksum.to_le_bytes());
|
||||||
|
|
||||||
|
record
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode(record: &[u8]) -> Option<Self> {
|
||||||
|
if record.len() < WIFI_CREDENTIAL_RECORD_BYTES {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if &record[..WIFI_CREDENTIAL_MAGIC.len()] != WIFI_CREDENTIAL_MAGIC {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let expected_checksum = u32::from_le_bytes(
|
||||||
|
record[WIFI_CREDENTIAL_CHECKSUM_OFFSET..][..4]
|
||||||
|
.try_into()
|
||||||
|
.ok()?,
|
||||||
|
);
|
||||||
|
let actual_checksum = Self::checksum(&record[WIFI_CREDENTIAL_SECURITY_OFFSET..]);
|
||||||
|
if expected_checksum != actual_checksum {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let security = WifiSecurity::from_storage_byte(record[WIFI_CREDENTIAL_SECURITY_OFFSET])?;
|
||||||
|
let ssid_len = usize::from(record[WIFI_CREDENTIAL_SSID_LEN_OFFSET]);
|
||||||
|
let password_len = usize::from(record[WIFI_CREDENTIAL_PASSWORD_LEN_OFFSET]);
|
||||||
|
|
||||||
|
if ssid_len > 32 || password_len > 64 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ssid_text =
|
||||||
|
core::str::from_utf8(&record[WIFI_CREDENTIAL_SSID_OFFSET..][..ssid_len]).ok()?;
|
||||||
|
let password_text =
|
||||||
|
core::str::from_utf8(&record[WIFI_CREDENTIAL_PASSWORD_OFFSET..][..password_len])
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let mut ssid = WifiSsid::new();
|
||||||
|
ssid.push_str(ssid_text).ok()?;
|
||||||
|
|
||||||
|
let mut password = WifiPassword::new();
|
||||||
|
password.push_str(password_text).ok()?;
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
ssid,
|
||||||
|
password,
|
||||||
|
security,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn checksum(bytes: &[u8]) -> u32 {
|
||||||
|
let mut hash = 2_166_136_261u32;
|
||||||
|
for byte in bytes {
|
||||||
|
hash ^= u32::from(*byte);
|
||||||
|
hash = hash.wrapping_mul(16_777_619);
|
||||||
|
}
|
||||||
|
hash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum WifiEvent {
|
||||||
|
ScanFinished(Result<WifiNetworkList, WifiFailure>),
|
||||||
|
JoinFinished(Result<WifiSsid, WifiFailure>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum WifiFailure {
|
||||||
|
ScanFailed,
|
||||||
|
JoinFailed,
|
||||||
|
InvalidCredentials,
|
||||||
|
OutOfMemory,
|
||||||
|
Unsupported,
|
||||||
|
NotConnected,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WifiFailure {
|
||||||
|
pub fn message(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::ScanFailed => "WiFi scan failed",
|
||||||
|
Self::JoinFailed => "WiFi join failed",
|
||||||
|
Self::InvalidCredentials => "Invalid WiFi credentials",
|
||||||
|
Self::OutOfMemory => "WiFi ran out of memory",
|
||||||
|
Self::Unsupported => "WiFi operation unsupported",
|
||||||
|
Self::NotConnected => "WiFi is not connected",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
web/index.html
Normal file
26
web/index.html
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Kinda web simulator</title>
|
||||||
|
<link rel="stylesheet" href="./style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>Kinda web simulator</h1>
|
||||||
|
<canvas id="display" aria-label="Kinda display simulator"></canvas>
|
||||||
|
<p id="status">Loading simulator...</p>
|
||||||
|
<section class="controls" aria-label="Input controls">
|
||||||
|
<button type="button" data-event="0">Up</button>
|
||||||
|
<button type="button" data-event="1">Down</button>
|
||||||
|
<button type="button" data-event="2">Select</button>
|
||||||
|
<button type="button" data-event="3">Back</button>
|
||||||
|
<button type="button" data-event="4">Hold select</button>
|
||||||
|
<button type="button" data-event="5">Hold back</button>
|
||||||
|
</section>
|
||||||
|
<p class="help">Keyboard: ArrowUp, ArrowDown, Enter, Escape or Backspace.</p>
|
||||||
|
</main>
|
||||||
|
<script type="module" src="./sim.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
107
web/sim.js
Normal file
107
web/sim.js
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
const EVENT_UP = 0;
|
||||||
|
const EVENT_DOWN = 1;
|
||||||
|
const EVENT_SELECT = 2;
|
||||||
|
const EVENT_BACK = 3;
|
||||||
|
|
||||||
|
const canvas = document.getElementById('display');
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
const status = document.getElementById('status');
|
||||||
|
const buttons = Array.from(document.querySelectorAll('[data-event]'));
|
||||||
|
|
||||||
|
let wasm;
|
||||||
|
|
||||||
|
async function loadWasm() {
|
||||||
|
const imports = {};
|
||||||
|
const response = fetch('./pkg/kinda.wasm');
|
||||||
|
|
||||||
|
if ('instantiateStreaming' in WebAssembly) {
|
||||||
|
try {
|
||||||
|
return await WebAssembly.instantiateStreaming(response, imports);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Falling back to ArrayBuffer WebAssembly load', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytes = await (await fetch('./pkg/kinda.wasm')).arrayBuffer();
|
||||||
|
return WebAssembly.instantiate(bytes, imports);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
const width = wasm.kinda_sim_width();
|
||||||
|
const height = wasm.kinda_sim_height();
|
||||||
|
const pixelPointer = wasm.kinda_sim_pixels();
|
||||||
|
const pixelLength = wasm.kinda_sim_pixel_len();
|
||||||
|
const pixels = new Uint8Array(wasm.memory.buffer, pixelPointer, pixelLength);
|
||||||
|
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
|
||||||
|
const image = context.createImageData(width, height);
|
||||||
|
for (let source = 0, target = 0; source < pixels.length; source += 1, target += 4) {
|
||||||
|
const value = Math.round(18 + (pixels[source] * 230) / 255);
|
||||||
|
image.data[target] = value;
|
||||||
|
image.data[target + 1] = value;
|
||||||
|
image.data[target + 2] = value;
|
||||||
|
image.data[target + 3] = 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.putImageData(image, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendInput(eventCode) {
|
||||||
|
if (!wasm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wasm.kinda_sim_input(eventCode);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyboardEventCode(event) {
|
||||||
|
if (event.key === 'ArrowUp') {
|
||||||
|
return EVENT_UP;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowDown') {
|
||||||
|
return EVENT_DOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
return EVENT_SELECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Escape' || event.key === 'Backspace') {
|
||||||
|
return EVENT_BACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.forEach((button) => {
|
||||||
|
button.disabled = true;
|
||||||
|
button.addEventListener('click', () => sendInput(Number(button.dataset.event)));
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('keydown', (event) => {
|
||||||
|
const eventCode = keyboardEventCode(event);
|
||||||
|
if (eventCode === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
sendInput(eventCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const instance = await loadWasm();
|
||||||
|
wasm = instance.instance.exports;
|
||||||
|
wasm.kinda_sim_init();
|
||||||
|
render();
|
||||||
|
buttons.forEach((button) => {
|
||||||
|
button.disabled = false;
|
||||||
|
});
|
||||||
|
status.textContent = 'Ready. Select scans with mock WiFi results.';
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
status.textContent = error instanceof Error ? error.message : 'Could not load simulator.';
|
||||||
|
}
|
||||||
45
web/style.css
Normal file
45
web/style.css
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
body {
|
||||||
|
margin: 24px;
|
||||||
|
background: #eeeeee;
|
||||||
|
color: #111111;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
max-width: 900px;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
height: auto;
|
||||||
|
border: 1px solid #222222;
|
||||||
|
background: #f8f8f8;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #222222;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #111111;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
cursor: wait;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help,
|
||||||
|
#status {
|
||||||
|
margin: 12px 0 0;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user