diff --git a/.cargo/config.toml b/.cargo/config.toml index c80b1a36..08025a6b 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -31,10 +31,10 @@ build-std = ["std", "panic_abort"] # Comment out this when using the PlatformIO build (it only supports `v4.3.2`) ESP_IDF_VERSION = { value = "release/v4.4" } +ESP_IDF = { value = ".embuild/espressif/esp-idf/release-v4.4" } # These configurations will pick up your custom "sdkconfig.release", "sdkconfig.debug" or "sdkconfig.defaults[.*]" files # that you might put in the root of the project # The easiest way to generate a full "sdkconfig" configuration (as opposed to manually enabling only the necessary flags via "sdkconfig.defaults[.*]" # is by running "cargo pio espidf menuconfig" (that is, if using the pio builder) -#ESP_IDF_SDKCONFIG = { value = "sdkconfig.release;sdkconfig.debug" } -ESP_IDF_SDKCONFIG_DEFAULTS = { value = "sdkconfig.defaults;sdkconfig.defaults.esp32;sdkconfig.defaults.esp32s2" } +ESP_IDF_SDKCONFIG_DEFAULTS = { value = "sdkconfig.defaults;sdkconfig.defaults.esp32;sdkconfig.defaults.esp32s2" } \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe46b80d..69b32aee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ on: - cron: '50 6 * * *' env: - rust_toolchain: nightly-2022-04-07 + rust_toolchain: nightly jobs: compile: diff --git a/.gitignore b/.gitignore index 881f5874..6230cac4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,7 @@ /.espressif /.embuild /target +/flasher/target +/messages/target /Cargo.lock **/*.rs.bk diff --git a/Cargo.toml b/Cargo.toml index 484403d7..80eb63e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,22 +1,20 @@ +[workspace] +members = [ + "messages", + "flasher", +] + [package] -name = "rust-esp32-std-demo" -version = "0.25.0" -authors = ["ivmarkov"] -edition = "2018" +name = "rust-ota-demo" +authors = ["ivmarkov", "Maxime Borges","MartinBroers"] +version = "0.2.0" +edition = "2021" categories = ["embedded", "hardware-support"] keywords = ["embedded", "svc", "idf", "esp-idf", "esp32"] -description = "A demo binary crate for the ESP32 and ESP-IDF, which connects to WiFi, Ethernet, drives a small HTTP server and draws on a LED screen" -repository = "https://github.com/ivmarkov/rust-esp32-std-demo" +description = "A demo app that allows OTA update via UART" license = "MIT OR Apache-2.0" readme = "README.md" -[patch.crates-io] -smol = { git = "https://github.com/esp-rs-compat/smol" } -polling = { git = "https://github.com/esp-rs-compat/polling" } -socket2 = { git = "https://github.com/esp-rs-compat/socket2" } -getrandom = { version = "0.2", git = "https://github.com/esp-rs-compat/getrandom.git" } -#getrandom1 = { version = "0.1", git = "https://github.com/esp-rs-compat/getrandom.git", package = "getrandom", branch = "0.1" } - [profile.release] opt-level = "s" @@ -24,64 +22,21 @@ opt-level = "s" debug = true # Symbols are nice and they don't increase the size on Flash opt-level = "z" -[features] -default = ["experimental"] - -# Enable this feature for the build to use the PlatformIO tooling instead of the native ESP-IDF tooling under the hood -pio = ["esp-idf-sys/pio"] - -# Enable this feature if you are building for QEMU -qemu = [] - -# Enable this feature in case you have a Kaluga board and would like to see a LED screen demo -kaluga = [] - -# Enable this feature in case you have a TTGO board and would like to see a LED screen demo -ttgo = [] - -# Enable this feature in case you have an ESP32S3-USB-OTG board and would like to see a LED screen demo -heltec = [] - -# Enable this feature in case you have a generic SSD1306 Display connected via SPI to pins 3, 4, 5, 16, 18, 23 (SPI3) of your board -ssd1306g_spi = [] - -# Enable this feature in case you have a generic SSD1306 screen connected to pins 14, 22 and 21 of your board -ssd1306g = [] - -esp32s3_usb_otg = [] - -# Enable this feature in case you have an RMII IP101 Ethernet adapter -ip101 = [] - -# Enable this feature in case you have an SPI W5500 Ethernet adapter -w5500 = [] - -# Enable this feature in case you have a Waveshare board and 4.2" e-paper -waveshare_epd = [] - -experimental = ["esp-idf-svc/experimental", "esp-idf-hal/experimental", "embedded-svc/experimental"] - [dependencies] anyhow = {version = "1", features = ["backtrace"]} log = "0.4" -url = "2" -esp-idf-sys = { version = "0.31", features = ["binstart"] } -esp-idf-svc = "0.41" -esp-idf-hal = "0.37" -embedded-svc = "0.21" embedded-hal = "0.2" -embedded-graphics = "0.7" -display-interface = "0.4" -display-interface-spi = "0.4" -st7789 = "0.6" -ili9341 = { version = "0.5", git = "https://github.com/yuri91/ili9341-rs" } -ssd1306 = "0.7" -epd-waveshare = "0.5.0" -smol = "1.2" +embedded-svc = "0.23" +esp-idf-hal = {version = "0.40"} +esp-idf-svc = { version = "0.44", features = ["experimental"] } +esp-idf-sys = { version = "0.32", features = ["binstart"] } +messages = { path = "messages" } +smlang = "0.5.1" [build-dependencies] embuild = "0.29" anyhow = "1" -# Future; might be possible once https://github.com/rust-lang/cargo/issues/9096 hits Cargo nightly: -#rust-esp32-ulp-blink = { git = "https://github.com/ivmarkov/rust-esp32-ulp-blink", artifact = "bin" } +[patch.crates-io] +# https://codesti.com/issue/ivmarkov/rust-esp32-std-demo/123 +embedded-io = { git = "https://github.com/embassy-rs/embedded-io.git", tag = "v0.3.0"} diff --git a/README.md b/README.md index 93ca4bc6..fcda2e69 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ A demo STD binary crate for the ESP32[XX] and ESP-IDF, which connects to WiFi, E ![CI](https://github.com/ivmarkov/rust-esp32-std-demo/actions/workflows/ci.yml/badge.svg) +[Join in](https://matrix.to/#/#esp-rs:matrix.org) on the discussion! + Highlights: - **Pure Rust and pure Cargo build!** No CMake, no PlatformIO, no C helpers @@ -34,7 +36,7 @@ Highlights: - Enter it: `cd rust-esp32-std-demo` - Export two environment variables that would contain the SSID & password of your wireless network: - `export RUST_ESP32_STD_DEMO_WIFI_SSID=` - - `export RUST_ESP32_STD_DEMO_WIFI_PASS=` + - `export RUST_ESP32_STD_DEMO_WIFI_PASS=` - To configure the demo for your particular board, please uncomment the relevant [Rust target for your board](https://github.com/ivmarkov/rust-esp32-std-demo/blob/main/.cargo/config.toml#L2) and comment the others. Alternatively, just append the `--target ` flag to all `cargo build` lines below. - Build: `cargo build` or `cargo build --release` - (Only if you happen to have a [TTGO T-Display board](http://www.lilygo.cn/prod_view.aspx?TypeId=50033&Id=1126&FId=t3:50033:3)): Add `ttgo` to the `--features` build flags above (as in `cargo build --features ttgo`) to be greeted with a `Hello Rust!` message on the board's LED screen @@ -74,12 +76,18 @@ Highlights: - (After each cargo build) Convert the elf image to binary: `esptool.py --chip [esp32|esp32s2|esp32c3] elf2image target/xtensa-esp32-espidf/debug/rust-esp32-std-demo` - (After each cargo build) Flash the resulting binary: `esptool.py --chip [esp32|esp32s2|esp32c3] -p /dev/ttyUSB0 -b 460800 --before=default_reset --after=hard_reset write_flash --flash_mode dio --flash_freq 40m --flash_size 4MB 0x10000 target/xtensa-esp32-espidf/debug/rust-esp32-std-demo.bin` +## Preparing an OTA binary + + cargo espflash --release --partition-table ./partitions.csv save-image ota.bin + +## Flashing the OTA over UART + + cargo run -p flasher --target x86_64-unknown-linux-gnu -- ota.bin --uart /dev/ttyUSB0 --baudrate 921600 + ## Monitor - Once flashed, the board can be connected with any suitable serial monitor, e.g.: - - ESPMonitor: `espmonitor /dev/ttyUSB0` (you need to `cargo install espmonitor` first) - - Cargo PIO (this one **decodes stack traces**!): `cargo pio espidf monitor /dev/ttyUSB0` (you need to `cargo install cargo-pio` first) - - Please run it from within the `rust-esp32-std-demo` project directory, or else the built ELF file will not be detected, and the stack traces will not be decoded! + - (Recommended) `espflash`: `espflash serial-monitor` - Built-in Linux/MacOS screen: `screen /dev/ttyUSB0 115200` (use `Ctrl+A` and then type `:quit` to stop it) - Miniterm: `miniterm --raw /dev/ttyUSB0 115200` @@ -91,6 +99,9 @@ Highlights: - `http:///bar` - `http:///ulp` (ESP32-S2 only) +- Alternatively you can connect directly to the ESP Accesspoint by connecting to the 'aptest' network using the default IP address: + - `http://192.168.71.1` + - The monitor should output more or less the following: ``` Hello, world from Rust! diff --git a/flasher/Cargo.lock b/flasher/Cargo.lock new file mode 100644 index 00000000..bee15aa0 --- /dev/null +++ b/flasher/Cargo.lock @@ -0,0 +1,663 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "CoreFoundation-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0e9889e6db118d49d88d84728d0e964d973a5680befb5f85f55141beea5c20b" +dependencies = [ + "libc", + "mach 0.1.2", +] + +[[package]] +name = "IOKit-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99696c398cbaf669d2368076bdb3d627fb0ce51a26899d7c61228c5c0af3bf4a" +dependencies = [ + "CoreFoundation-sys", + "libc", + "mach 0.1.2", +] + +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" +dependencies = [ + "backtrace", +] + +[[package]] +name = "argh" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb41d85d92dfab96cb95ab023c265c5e4261bb956c0fb49ca06d90c570f1958" +dependencies = [ + "argh_derive", + "argh_shared", +] + +[[package]] +name = "argh_derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be69f70ef5497dd6ab331a50bd95c6ac6b8f7f17a7967838332743fbd58dc3b5" +dependencies = [ + "argh_shared", + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "argh_shared" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f8c380fa28aa1b36107cd97f0196474bb7241bb95a453c5c01a15ac74b2eac" + +[[package]] +name = "atomic-polyfill" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14bf7b4f565e5e717d7a7a65b2a05c0b8c96e4db636d6f780f03b15108cdd1b" +dependencies = [ + "critical-section", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bare-metal" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5deb64efa5bd81e31fcd1938615a6d98c82eafcbcd787162b6f63b91d6bac5b3" +dependencies = [ + "rustc_version 0.2.3", +] + +[[package]] +name = "bare-metal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fe8f5a8a398345e52358e18ff07cc17a568fbca5c6f73873d3a62056309603" + +[[package]] +name = "bit_field" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4" + +[[package]] +name = "bitfield" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cortex-m" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd20d4ac4aa86f4f75f239d59e542ef67de87cce2c282818dc6e84155d3ea126" +dependencies = [ + "bare-metal 0.2.5", + "bitfield", + "embedded-hal", + "volatile-register", +] + +[[package]] +name = "crc" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53757d12b596c16c78b83458d732a5d1a17ab3f53f2f7412f6fb57cc8a140ab3" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff" + +[[package]] +name = "critical-section" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95da181745b56d4bd339530ec393508910c909c784e8962d15d722bacf0bcbcd" +dependencies = [ + "bare-metal 1.0.0", + "cfg-if", + "cortex-m", + "riscv", +] + +[[package]] +name = "embedded-hal" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff" +dependencies = [ + "nb 0.1.3", + "void", +] + +[[package]] +name = "esp32rogue-postcard" +version = "0.1.0" +dependencies = [ + "anyhow", + "crc", + "heapless", + "postcard", + "serde", +] + +[[package]] +name = "gimli" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a08e755adbc0ad283725b29f4a4883deee15336f372d5f61fae59efec40f983" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version 0.4.0", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + +[[package]] +name = "libudev" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0" +dependencies = [ + "libc", + "libudev-sys", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "mach" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd13ee2dd61cc82833ba05ade5a30bb3d63f7ced605ef827063c63078302de9" +dependencies = [ + "libc", +] + +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "miniz_oxide" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +dependencies = [ + "adler", +] + +[[package]] +name = "nb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" +dependencies = [ + "nb 1.0.0", +] + +[[package]] +name = "nb" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "546c37ac5d9e56f55e73b677106873d9d9f5190605e41a856503623648488cae" + +[[package]] +name = "nix" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f17df307904acd05aa8e32e97bb20f2a0df1728bbc2d771ae8f9a90463441e9" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "object" +version = "0.28.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424" +dependencies = [ + "memchr", +] + +[[package]] +name = "ota-flash" +version = "0.1.0" +dependencies = [ + "anyhow", + "argh", + "esp32rogue-postcard", + "log", + "serialport", + "thiserror", +] + +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + +[[package]] +name = "postcard" +version = "1.0.0-alpha.1" +source = "git+https://github.com/jamesmunns/postcard.git?tag=v1.0.0-alpha.1#7597a91ed6c9543d433f4e9a51c1c7c0f6bc9a4f" +dependencies = [ + "heapless", + "postcard-cobs", + "serde", +] + +[[package]] +name = "postcard-cobs" +version = "0.1.5-pre" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c68cb38ed13fd7bc9dd5db8f165b7c8d9c1a315104083a2b10f11354c2af97f" + +[[package]] +name = "proc-macro2" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" + +[[package]] +name = "riscv" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6907ccdd7a31012b70faf2af85cd9e5ba97657cc3987c4f13f8e4d2c2a088aba" +dependencies = [ + "bare-metal 1.0.0", + "bit_field", + "riscv-target", +] + +[[package]] +name = "riscv-target" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88aa938cda42a0cf62a20cfe8d139ff1af20c2e681212b5b34adb5a58333f222" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver 1.0.10", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41d061efea015927ac527063765e73601444cdc344ba855bc7bd44578b25e1c" + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serialport" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aab92efb5cf60ad310548bc3f16fa6b0d950019cb7ed8ff41968c3d03721cf12" +dependencies = [ + "CoreFoundation-sys", + "IOKit-sys", + "bitflags", + "cfg-if", + "libudev", + "mach 0.3.2", + "nix", + "regex", + "winapi", +] + +[[package]] +name = "spin" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c530c2b0d0bf8b69304b39fe2001993e267461948b890cd037d8ad4293fa1a0d" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" + +[[package]] +name = "unicode-segmentation" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" + +[[package]] +name = "vcell" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "volatile-register" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee8f19f9d74293faf70901bc20ad067dc1ad390d2cbf1e3f75f721ffee908b6" +dependencies = [ + "vcell", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/flasher/Cargo.toml b/flasher/Cargo.toml new file mode 100644 index 00000000..02ecf9ea --- /dev/null +++ b/flasher/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "flasher" +version = "0.1.0" +authors = ["Maxime Borges "] +edition = "2018" +categories = ["embedded", "hardware-support"] +keywords = ["embedded", "svc", "idf", "esp-idf", "esp32"] +description = "A tool for flashing OTA app via UART" +license = "PROPRIETARY" +readme = "README.md" + +[dependencies] +argh = "0.1.7" +anyhow = {version = "1", features = ["backtrace"]} +log = "0.4" +thiserror = "1.0.31" +serialport = "4.2" +messages = { path="../messages" } + +[build-dependencies] +anyhow = "1" diff --git a/flasher/src/main.rs b/flasher/src/main.rs new file mode 100644 index 00000000..98d8fc4c --- /dev/null +++ b/flasher/src/main.rs @@ -0,0 +1,186 @@ +use anyhow::Context; +use argh::FromArgs; +use messages::{Message, MessageTypeHost, MessageTypeMcu, UpdateStatus}; +use serialport::{self, SerialPort, TTYPort}; +use thiserror::Error; +use std::{io::{stdin, stdout, Write, Read}, path::PathBuf, fs::File, time::Duration}; + +#[derive(Debug, Error)] +enum Error { + #[error("No serial port found")] + NoSerialPort, + #[error("Binary does not exists at {0}")] + BinDoesntExist(String), + #[error("Writing to UART failed")] + ComWriteFailed, + #[error("Received invalid response from UART")] + ComInvalidResponse, + #[error("Received critical error response from UART")] + ComCriticalError, +} + +#[derive(FromArgs)] +/// Reach new heights. +struct UartArgs { + /// path to the binary file to flash + #[argh(positional)] + bin_path: PathBuf, + + /// UART port to use + #[argh(option)] + uart: Option, + + /// baudrate + #[argh(option, default = "921_600")] + baudrate: u32, +} + +fn main() -> Result<(), anyhow::Error> { + let args: UartArgs = argh::from_env(); + + if !args.bin_path.exists() { + Err(Error::BinDoesntExist(args.bin_path.display().to_string()))? + } + + // Get the serial port name + let uart_port = match args.uart { + // Either from arguments + Some(user_port) => user_port, + // Or from user input + None => { + loop { + let available_ports = serialport::available_ports().unwrap(); + if available_ports.is_empty() { + Err(Error::NoSerialPort)? + } + + // Display available serial ports + println!("Available ports: "); + for (i, p) in available_ports.iter().enumerate() { + print!("{i}: {}", p.port_name); + match &p.port_type { + serialport::SerialPortType::UsbPort(u) => match &u.product { + Some(product) => println!(" ({})", product), + None => println!(), + }, + _ => println!(), + } + } + + // Get user input + let mut s = String::new(); + print!("Enter the index of the desired UART port: "); + + let _ = stdout().flush(); + s.clear(); + stdin().read_line(&mut s).expect("Did not enter a correct string"); + let i: usize = match s.trim().parse() { + Ok(i) => i, + Err(_) => { + println!("Expecting an index\n"); + continue; + }, + }; + if i < available_ports.len() { + break available_ports.get(i).unwrap().port_name.clone(); + } + println!("Index out-of-range\n"); + } + } + }; + + // Prepare serial port + let mut serial_port = serialport::new(&uart_port, args.baudrate) + .timeout(Duration::from_millis(500)) // This seems to be useless for `read()` + .open_native() + .context(format!("opening {uart_port}"))?; + + // Open firmware + let mut file = File::open(args.bin_path)?; + let mut firmware = Vec::new(); + file.read_to_end(&mut firmware)?; + + // Cancel any previous operation + let msg_buffer = Message::new(MessageTypeHost::Cancel).serialize()?; + serial_port.write(msg_buffer.as_slice())?; + + // TODO: implement Cancel ACK on ESP instead of waiting + std::thread::sleep(Duration::from_millis(50)); + + // Start update + let msg_buffer = Message::new(MessageTypeHost::UpdateStart).serialize()?; + serial_port.write(msg_buffer.as_slice())?; + + std::thread::sleep(Duration::from_millis(50)); + + // ACK start update + let mut msg_buffer: Vec = vec![0; 6]; + serial_port.read_exact(msg_buffer.as_mut_slice()).context("reading start update ACK")?; + let rx_msg = Message::::deserialize(msg_buffer.as_slice())?; + + if !matches!(rx_msg.payload.message_type, MessageTypeMcu::UpdateStartStatus(UpdateStatus::Ok)) { + Err(Error::ComInvalidResponse)? + } + + // Split firmware into chunks to send to ESP + let chunks: Vec<&[u8]> = firmware.chunks(110).collect(); + let mut i = 0; + while i < chunks.len() { + const MAX_RETRY: usize = 5; + 'retry: for retry_cnt in 0..MAX_RETRY+1 { + if retry_cnt == MAX_RETRY { + Err(Error::ComWriteFailed)? + } + + println!("Sending chunk {i}"); + let chunk = chunks.get(i).unwrap(); + match send_chunk(&mut serial_port, i, chunk) { + Ok(status) => match status { + UpdateStatus::Ok => break 'retry, + UpdateStatus::Retry(Some(id)) if (id as usize) <= i => { + println!("Retrying segment {}, {}/{}", id, retry_cnt+1, MAX_RETRY); + i = id as usize; + continue 'retry; + }, + _ => Err(Error::ComCriticalError)?, + }, + Err(e) => Err(e)?, + } + + fn send_chunk(serial_port: &mut TTYPort, id: usize, chunk: &[u8]) -> Result { + let msg = Message::new(MessageTypeHost::UpdateSegment(id as u16, chunk)).serialize()?; + serial_port.write(msg.as_slice())?; + + // There is no way of waiting for unknown length of data + // so we wait for a few bytes, and check if there is more in the buffer afterwards + // That's pretty sad. + let mut msg_buffer: Vec = vec![0; 6]; + serial_port.read_exact(&mut msg_buffer[..6])?; + match serial_port.bytes_to_read() { + Ok(bytes_to_read) if bytes_to_read > 0 => { + let mut tmp = vec![0; bytes_to_read as usize]; + serial_port.read_exact(&mut tmp)?; + msg_buffer.extend(tmp); + }, + _ => (), + }; + let rx_msg = Message::::deserialize(msg_buffer.as_mut_slice()) + .context(format!("deserializing {:?}", msg_buffer.as_slice()))?; + + match rx_msg.payload.message_type { + MessageTypeMcu::UpdateSegmentStatus(status) => Ok(status), + _ => Err(Error::ComInvalidResponse)? + } + } + } + + // Go to next chunk if nothing happened + i += 1; + } + + let msg = Message::new(MessageTypeHost::UpdateEnd).serialize()?; + serial_port.write(msg.as_slice())?; + + + Ok(()) +} diff --git a/messages/.gitignore b/messages/.gitignore new file mode 100644 index 00000000..4fffb2f8 --- /dev/null +++ b/messages/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/messages/Cargo.toml b/messages/Cargo.toml new file mode 100644 index 00000000..3fbaa661 --- /dev/null +++ b/messages/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "messages" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +postcard = { git = "https://github.com/jamesmunns/postcard.git", tag = "v1.0.0-alpha.1", features = ["alloc"] } +serde = "1.0" +crc = "3.0" +heapless = "0.7" +anyhow = "1.0" \ No newline at end of file diff --git a/messages/src/lib.rs b/messages/src/lib.rs new file mode 100644 index 00000000..c4c1ee43 --- /dev/null +++ b/messages/src/lib.rs @@ -0,0 +1,231 @@ +use std::fmt::Display; + +use anyhow::{Result, anyhow}; +use postcard::{to_allocvec, from_bytes}; +use serde::{Serialize, Deserialize}; +use crc::{Crc, CRC_16_IBM_3740 as CRC_ALG}; // Also called CRC-16-CCITT-FALSE +extern crate alloc; + +pub const VERSION: u8 = 1; +pub const CRC: Crc = Crc::::new(&CRC_ALG); + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Error { + ChecksumError, +} +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +pub enum UpdateStatus { + Ok, + Retry(Option), + Failed, +} + + +/// Message sent from the MCU to the host +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +pub enum MessageTypeMcu { + /// Send an ADC measurement + Adc(u32), + /// Send status if ready or not to receive a software update + /// Follows the reception of `MessageTypeHost::UpdateStart` + UpdateStartStatus(UpdateStatus), + /// Send last segment status + /// Follows the reception of `MessageTypeHost::UpdateSegment` + UpdateSegmentStatus(UpdateStatus), + /// Send final status of the update + /// Follows the reception of `MessageTypeHost::UpdateEnd` + UpdateEndStatus(UpdateStatus), +} + +/// Message sent from the host to the MCU +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] +pub enum MessageTypeHost<'a> { + /// Ask the MCU to be ready to receive an update + UpdateStart, + /// Send an update segment + UpdateSegment(u16, &'a [u8]), + /// Finish the update process + UpdateEnd, + /// Cancel any current operation + Cancel, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] +pub struct MessagePayload { + version: u8, + pub message_type: T, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] +pub struct Message { + pub payload: MessagePayload, + checksum: u16, +} +impl<'de, T: Serialize + Deserialize<'de>> Message { + /// Create a new message from a `message_type` and compute its CRC + pub fn new(message_type: T) -> Message { + let payload = MessagePayload:: { version: VERSION, message_type }; + // TODO: this is very bad + let payload_bytes = to_allocvec(&payload).unwrap(); + let crc = CRC.checksum(&payload_bytes); + + Message { + payload, + checksum: crc + } + } + + /// Serialize the message to a vector of bytes + pub fn serialize(&self) -> Result> { + to_allocvec(&self).map_err(|e| anyhow!(e)) + } + + /// Deserialize a vector of bytes into a message + pub fn deserialize(bytes: &'de [u8]) -> Result> { + let res: Result> = from_bytes(bytes).map_err(|e| anyhow!(e)); + match &res { + Ok(msg) if !msg.is_crc_valid() => Err(anyhow!(Error::ChecksumError)), + _ => res, + } + } + + /// Check if the CRC is valid + pub fn is_crc_valid(&self) -> bool { + // TODO: this is very bad + let payload_bytes = to_allocvec(&self.payload).unwrap(); + let crc = CRC.checksum(&payload_bytes); + + self.checksum == crc + } +} + +// trait SerDer<'de>: Serialize + Deserialize<'de> { +// /// Serialize the message to a vector of bytes +// fn serialize(&self) -> Result> { +// to_allocvec(&self).map_err(|e| anyhow!(e)) +// } + +// /// Deserialize a vector of bytes into a message +// fn deserialize(bytes: &'de [u8]) -> Result { +// from_bytes(bytes).map_err(|e| anyhow!(e)) +// // let res: Result = from_bytes(bytes).map_err(|e| anyhow!(e)); +// // match &res { +// // Ok(msg) if !msg.is_crc_valid() => Err(anyhow!(Error::ChecksumError)), +// // _ => res, +// // } +// } +// } + + +#[cfg(test)] +mod tests { + mod mcu { + use crate::*; + + #[test] + fn adc() { + let raw = [VERSION, 0x00, 0xB7, 0x26, 0xCA, 0x62]; + let msg = Message::new(MessageTypeMcu::Adc(0x1337)); + let msg_bytes = Message::serialize(&msg).unwrap(); + assert_eq!(msg_bytes, raw); + + let des_msg: Message = Message::deserialize(&msg_bytes).unwrap(); + assert_eq!(msg, des_msg); + + let msg_from_raw: Message = from_bytes(&raw).unwrap(); + assert_eq!(msg, msg_from_raw); + } + + #[test] + fn update_start_failed() { + let raw = [VERSION, 0x01, 0x02, 223, 209, 3]; + let msg = Message::new(MessageTypeMcu::UpdateStartStatus(UpdateStatus::Failed)); + let msg_bytes = Message::serialize(&msg).unwrap(); + assert_eq!(msg_bytes, raw); + + let des_msg: Message = Message::deserialize(&msg_bytes).unwrap(); + assert_eq!(msg, des_msg); + + let msg_from_raw: Message = from_bytes(&raw).unwrap(); + assert_eq!(msg, msg_from_raw); + } + + #[test] + fn update_retry_id() { + let raw = [VERSION, 0x02, 0x01, 0x01, 154, 5, 178, 210, 3]; + let msg = Message::new(MessageTypeMcu::UpdateSegmentStatus(UpdateStatus::Retry(Some(666)))); + let msg_bytes = Message::serialize(&msg).unwrap(); + assert_eq!(msg_bytes, raw); + + let des_msg: Message = Message::deserialize(&msg_bytes).unwrap(); + assert_eq!(msg, des_msg); + + let msg_from_raw: Message = from_bytes(&raw).unwrap(); + assert_eq!(msg, msg_from_raw); + } + + #[test] + fn bad_checksum() { + let raw = [VERSION, 0x00, 0xB7, 0x26, 0x00, 0x00]; + let res_des = Message::::deserialize(&raw); + assert!(res_des.is_err()); + assert_eq!((res_des.unwrap_err().downcast::().unwrap()), Error::ChecksumError); + } + } + + mod host { + use crate::*; + + #[test] + fn update_start() { + let raw = [1, 0, 190, 92]; + let msg = Message::new(MessageTypeHost::UpdateStart); + let msg_bytes = msg.serialize().unwrap(); + println!("{:?}", msg_bytes); + assert_eq!(msg_bytes, raw); + + } + + #[test] + fn cancel() { + let raw = [VERSION, 0x03, 0xDD, 0x3C]; + let msg = Message::new(MessageTypeHost::Cancel); + let msg_bytes = msg.serialize().unwrap(); + assert_eq!(msg_bytes, raw); + + let des_msg: Message = Message::deserialize(&msg_bytes).unwrap(); + assert_eq!(msg, des_msg); + + let msg_from_raw: Message = from_bytes(&raw).unwrap(); + assert_eq!(msg, msg_from_raw); + } + + #[test] + fn update_segment() { + let raw = [VERSION, 0x01, 0x9A, 0x05, 4, 1, 2, 3, 0xFF, 0xBE, 0x84, 0x01]; + let msg = Message::new(MessageTypeHost::UpdateSegment(666, &[1, 2, 3, 0xFF])); + let msg_bytes = msg.serialize().unwrap(); + assert_eq!(msg_bytes, raw); + + let des_msg: Message = Message::deserialize(&msg_bytes).unwrap(); + assert_eq!(msg, des_msg); + + let msg_from_raw: Message = from_bytes(&raw).unwrap(); + assert_eq!(msg, msg_from_raw); + + match des_msg.payload.message_type { + MessageTypeHost::UpdateSegment(cnt, bytes) => { + assert_eq!(cnt, 666); + assert_eq!(bytes, [1, 2, 3, 0xFF]); + }, + _ => assert!(false) + } + } + } +} diff --git a/partitions.csv b/partitions.csv index 924878bc..081f29bb 100644 --- a/partitions.csv +++ b/partitions.csv @@ -1,5 +1,7 @@ # Name, Type, SubType, Offset, Size, Flags # Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap -nvs, data, nvs, , 0x6000, -phy_init, data, phy, , 0x1000, -factory, app, factory, , 3M, \ No newline at end of file +nvs, data, nvs, , 0x6000, +otadata, data, ota, , 0x2000, +phy_init, data, phy, , 0x1000, +ota_0, app, ota_0, , 800k, +ota_1, app, ota_1, , 800k, \ No newline at end of file diff --git a/sdkconfig.defaults b/sdkconfig.defaults index f5b63343..6cc7a753 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -1,5 +1,9 @@ +# OTA Support +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_PARTITION_TABLE_TWO_OTA=y + # Necessary for the LED screen demos -CONFIG_ESP_MAIN_TASK_STACK_SIZE=7000 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=20000 CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 diff --git a/sdkconfig.defaults.esp32 b/sdkconfig.defaults.esp32 index 6fa312f3..15327359 100644 --- a/sdkconfig.defaults.esp32 +++ b/sdkconfig.defaults.esp32 @@ -1,5 +1,5 @@ # Disabled, because somehow causes issues when compiling for ESP32S2, where it is not supported. -# In theory, this `sdkconfig.defaults` file should not be picked up when ESP32S2 is the target, but it somehow does get picked up +# In theory, this `sdkconfig.defaults.esp32` file should not be picked up when ESP32S2 is the target, but it somehow does get picked up #CONFIG_ETH_USE_OPENETH=y #CONFIG_MBEDTLS_HARDWARE_AES=n #CONFIG_MBEDTLS_HARDWARE_SHA=n diff --git a/src/main.rs b/src/main.rs index f984e086..45c69229 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,1400 +1,17 @@ -#![allow(unused_imports)] -#![allow(clippy::single_component_path_imports)] -//#![feature(backtrace)] +mod simple_ota; +mod uart_update; +use crate::simple_ota::serial_ota; +use crate::simple_ota::Error; -#[cfg(all(feature = "qemu", not(esp32)))] -compile_error!("The `qemu` feature can only be built for the `xtensa-esp32-espidf` target."); - -#[cfg(all(feature = "ip101", not(esp32)))] -compile_error!("The `ip101` feature can only be built for the `xtensa-esp32-espidf` target."); - -#[cfg(all(feature = "kaluga", not(esp32s2)))] -compile_error!("The `kaluga` feature can only be built for the `xtensa-esp32s2-espidf` target."); - -#[cfg(all(feature = "ttgo", not(esp32)))] -compile_error!("The `ttgo` feature can only be built for the `xtensa-esp32-espidf` target."); - -#[cfg(all(feature = "heltec", not(esp32)))] -compile_error!("The `heltec` feature can only be built for the `xtensa-esp32-espidf` target."); - -#[cfg(all(feature = "esp32s3_usb_otg", not(esp32s3)))] -compile_error!( - "The `esp32s3_usb_otg` feature can only be built for the `xtensa-esp32s3-espidf` target." -); - -use std::fs; -use std::io::{Read, Write}; -use std::net::{TcpListener, TcpStream}; -use std::path::PathBuf; -use std::sync::{Condvar, Mutex}; -use std::{cell::RefCell, env, sync::atomic::*, sync::Arc, thread, time::*}; - -use anyhow::bail; - -use embedded_svc::mqtt::client::utils::ConnState; -use log::*; - -use url; - -use smol; - -use embedded_hal::adc::OneShot; -use embedded_hal::blocking::delay::DelayMs; -use embedded_hal::digital::v2::OutputPin; - -use embedded_svc::eth; -use embedded_svc::eth::{Eth, TransitionalState}; -use embedded_svc::httpd::registry::*; -use embedded_svc::httpd::*; -use embedded_svc::io; -use embedded_svc::ipv4; -use embedded_svc::mqtt::client::{Client, Connection, MessageImpl, Publish, QoS}; -use embedded_svc::ping::Ping; -use embedded_svc::sys_time::SystemTime; -use embedded_svc::timer::TimerService; -use embedded_svc::timer::*; -use embedded_svc::wifi::*; - -use esp_idf_svc::eth::*; -use esp_idf_svc::eventloop::*; -use esp_idf_svc::eventloop::*; -use esp_idf_svc::httpd as idf; -use esp_idf_svc::httpd::ServerRegistry; -use esp_idf_svc::mqtt::client::*; -use esp_idf_svc::netif::*; -use esp_idf_svc::nvs::*; -use esp_idf_svc::ping; -use esp_idf_svc::sntp; -use esp_idf_svc::sysloop::*; -use esp_idf_svc::systime::EspSystemTime; -use esp_idf_svc::timer::*; -use esp_idf_svc::wifi::*; - -use esp_idf_hal::adc; -use esp_idf_hal::delay; -use esp_idf_hal::gpio; -use esp_idf_hal::i2c; -use esp_idf_hal::prelude::*; -use esp_idf_hal::spi; - -use esp_idf_sys::{self, c_types}; -use esp_idf_sys::{esp, EspError}; - -use display_interface_spi::SPIInterfaceNoCS; - -use embedded_graphics::mono_font::{ascii::FONT_10X20, MonoTextStyle}; -use embedded_graphics::pixelcolor::*; -use embedded_graphics::prelude::*; -use embedded_graphics::primitives::*; -use embedded_graphics::text::*; - -use ili9341; -use ssd1306; -use ssd1306::mode::DisplayConfig; -use st7789; - -use epd_waveshare::{epd4in2::*, graphics::VarDisplay, prelude::*}; - -#[allow(dead_code)] -#[cfg(not(feature = "qemu"))] -const SSID: &str = env!("RUST_ESP32_STD_DEMO_WIFI_SSID"); -#[allow(dead_code)] -#[cfg(not(feature = "qemu"))] -const PASS: &str = env!("RUST_ESP32_STD_DEMO_WIFI_PASS"); - -#[cfg(esp32s2)] -include!(env!("EMBUILD_GENERATED_SYMBOLS_FILE")); - -#[cfg(esp32s2)] -const ULP: &[u8] = include_bytes!(env!("EMBUILD_GENERATED_BIN_FILE")); - -thread_local! { - static TLS: RefCell = RefCell::new(13); -} - -fn main() -> Result<()> { +fn main() -> Result<(), Error> { esp_idf_sys::link_patches(); - - test_print(); - - test_atomics(); - - test_threads(); - - #[cfg(not(esp_idf_version = "4.3"))] - test_fs()?; - - // Bind the log crate to the ESP Logging facilities - esp_idf_svc::log::EspLogger::initialize_default(); - - // Get backtraces from anyhow; only works for Xtensa arch currently - // TODO: No longer working with ESP-IDF 4.3.1+ - //#[cfg(target_arch = "xtensa")] - //env::set_var("RUST_BACKTRACE", "1"); - - #[allow(unused)] - let peripherals = Peripherals::take().unwrap(); - #[allow(unused)] - let pins = peripherals.pins; - - #[allow(unused)] - let netif_stack = Arc::new(EspNetifStack::new()?); - #[allow(unused)] - let sys_loop_stack = Arc::new(EspSysLoopStack::new()?); - #[allow(unused)] - let default_nvs = Arc::new(EspDefaultNvs::new()?); - - #[cfg(feature = "ttgo")] - ttgo_hello_world( - pins.gpio4, - pins.gpio16, - pins.gpio23, - peripherals.spi2, - pins.gpio18, - pins.gpio19, - pins.gpio5, - )?; - - #[cfg(feature = "waveshare_epd")] - waveshare_epd_hello_world( - peripherals.spi2, - pins.gpio13, - pins.gpio14, - pins.gpio15, - pins.gpio25, - pins.gpio27, - pins.gpio26, - )?; - - #[cfg(feature = "kaluga")] - kaluga_hello_world( - pins.gpio6, - pins.gpio13, - pins.gpio16, - peripherals.spi3, - pins.gpio15, - pins.gpio9, - pins.gpio11, - true, - )?; - - #[cfg(feature = "heltec")] - heltec_hello_world(pins.gpio16, peripherals.i2c0, pins.gpio4, pins.gpio15)?; - - #[cfg(feature = "ssd1306g_spi")] - ssd1306g_hello_world_spi( - pins.gpio4, - pins.gpio16, - peripherals.spi3, - pins.gpio18, - pins.gpio23, - pins.gpio5, - )?; - - #[cfg(feature = "ssd1306g")] - let mut led_power = - ssd1306g_hello_world(peripherals.i2c0, pins.gpio14, pins.gpio22, pins.gpio21)?; - - #[cfg(feature = "esp32s3_usb_otg")] - esp32s3_usb_otg_hello_world( - pins.gpio9, - pins.gpio4, - pins.gpio8, - peripherals.spi3, - pins.gpio6, - pins.gpio7, - pins.gpio5, - )?; - - #[allow(clippy::redundant_clone)] - #[cfg(not(feature = "qemu"))] - #[allow(unused_mut)] - let mut wifi = wifi( - netif_stack.clone(), - sys_loop_stack.clone(), - default_nvs.clone(), - )?; - - #[allow(clippy::redundant_clone)] - #[cfg(feature = "qemu")] - let eth = eth_configure(Box::new(EspEth::new_openeth( - netif_stack.clone(), - sys_loop_stack.clone(), - )?))?; - - #[allow(clippy::redundant_clone)] - #[cfg(feature = "ip101")] - let eth = eth_configure(Box::new(EspEth::new_rmii( - netif_stack.clone(), - sys_loop_stack.clone(), - RmiiEthPeripherals { - rmii_rdx0: pins.gpio25, - rmii_rdx1: pins.gpio26, - rmii_crs_dv: pins.gpio27, - rmii_mdc: pins.gpio23, - rmii_txd1: pins.gpio22, - rmii_tx_en: pins.gpio21, - rmii_txd0: pins.gpio19, - rmii_mdio: pins.gpio18, - rmii_ref_clk: pins.gpio0, - rst: Some(pins.gpio5), - }, - RmiiEthChipset::IP101, - None, - )?))?; - - #[cfg(feature = "w5500")] - let eth = eth_configure(Box::new(EspEth::new_spi( - netif_stack.clone(), - sys_loop_stack.clone(), - SpiEthPeripherals { - int_pin: pins.gpio13, - rst_pin: Some(pins.gpio25), - spi_pins: spi::Pins { - sclk: pins.gpio12, - sdo: pins.gpio26, - sdi: Some(pins.gpio27), - cs: Some(pins.gpio14), - }, - spi: peripherals.spi2, - }, - SpiEthChipset::W5500, - 20.MHz().into(), - Some(&[0x02, 0x00, 0x00, 0x12, 0x34, 0x56]), - None, - )?))?; - - test_tcp()?; - - test_tcp_bind()?; - - let _sntp = sntp::EspSntp::new_default()?; - info!("SNTP initialized"); - - let (eventloop, _subscription) = test_eventloop()?; - - let mqtt_client = test_mqtt_client()?; - - let _timer = test_timer(eventloop, mqtt_client)?; - - #[cfg(feature = "experimental")] - experimental::test()?; - - #[cfg(not(feature = "qemu"))] - #[cfg(esp_idf_config_lwip_ipv4_napt)] - enable_napt(&mut wifi)?; - - let mutex = Arc::new((Mutex::new(None), Condvar::new())); - - let httpd = httpd(mutex.clone())?; - - #[cfg(feature = "ssd1306g")] - { - for s in 0..3 { - info!("Powering off the display in {} secs", 3 - s); - thread::sleep(Duration::from_secs(1)); - } - - led_power.set_low()?; - } - - let mut wait = mutex.0.lock().unwrap(); - - #[cfg(esp32)] - let mut hall_sensor = peripherals.hall_sensor; - - #[cfg(esp32)] - let mut a2 = pins.gpio34.into_analog_atten_11db()?; - #[cfg(any(esp32s2, esp32s3))] - let mut a2 = pins.gpio2.into_analog_atten_11db()?; - #[cfg(esp32c3)] - let mut a2 = pins.gpio2.into_analog_atten_11db()?; - - let mut powered_adc1 = adc::PoweredAdc::new( - peripherals.adc1, - adc::config::Config::new().calibration(true), - )?; - - #[allow(unused)] - let cycles = loop { - if let Some(cycles) = *wait { - break cycles; - } else { - wait = mutex - .1 - .wait_timeout(wait, Duration::from_secs(1)) - .unwrap() - .0; - - #[cfg(esp32)] - log::info!( - "Hall sensor reading: {}mV", - powered_adc1.read(&mut hall_sensor).unwrap() - ); - log::info!( - "A2 sensor reading: {}mV", - powered_adc1.read(&mut a2).unwrap() - ); + match serial_ota() { + Ok(v) => v, + Err(e) => { + println!("Error occured during update: {:?}", e); + return Err(e); } }; - for s in 0..3 { - info!("Shutting down in {} secs", 3 - s); - thread::sleep(Duration::from_secs(1)); - } - - drop(httpd); - info!("Httpd stopped"); - - #[cfg(not(feature = "qemu"))] - { - drop(wifi); - info!("Wifi stopped"); - } - - #[cfg(any(feature = "qemu", feature = "w5500", feature = "ip101"))] - { - let _eth_peripherals = eth.release()?; - info!("Eth stopped"); - } - - #[cfg(esp32s2)] - start_ulp(peripherals.ulp, cycles)?; - - Ok(()) -} - -#[allow(clippy::vec_init_then_push)] -fn test_print() { - // Start simple - println!("Hello from Rust!"); - - // Check collections - let mut children = vec![]; - - children.push("foo"); - children.push("bar"); - println!("More complex print {:?}", children); -} - -#[allow(deprecated)] -fn test_atomics() { - let a = AtomicUsize::new(0); - let v1 = a.compare_and_swap(0, 1, Ordering::SeqCst); - let v2 = a.swap(2, Ordering::SeqCst); - - let (r1, r2) = unsafe { - // don't optimize our atomics out - let r1 = core::ptr::read_volatile(&v1); - let r2 = core::ptr::read_volatile(&v2); - - (r1, r2) - }; - - println!("Result: {}, {}", r1, r2); -} - -fn test_threads() { - let mut children = vec![]; - - println!("Rust main thread: {:?}", thread::current()); - - TLS.with(|tls| { - println!("Main TLS before change: {}", *tls.borrow()); - }); - - TLS.with(|tls| *tls.borrow_mut() = 42); - - TLS.with(|tls| { - println!("Main TLS after change: {}", *tls.borrow()); - }); - - for i in 0..5 { - // Spin up another thread - children.push(thread::spawn(move || { - println!("This is thread number {}, {:?}", i, thread::current()); - - TLS.with(|tls| *tls.borrow_mut() = i); - - TLS.with(|tls| { - println!("Inner TLS: {}", *tls.borrow()); - }); - })); - } - - println!( - "About to join the threads. If ESP-IDF was patched successfully, joining will NOT crash" - ); - - for child in children { - // Wait for the thread to finish. Returns a result. - let _ = child.join(); - } - - TLS.with(|tls| { - println!("Main TLS after threads: {}", *tls.borrow()); - }); - - thread::sleep(Duration::from_secs(2)); - - println!("Joins were successful."); -} - -#[cfg(not(esp_idf_version = "4.3"))] -fn test_fs() -> Result<()> { - assert_eq!(fs::canonicalize(PathBuf::from("."))?, PathBuf::from("/")); - assert_eq!( - fs::canonicalize( - PathBuf::from("/") - .join("foo") - .join("bar") - .join(".") - .join("..") - .join("baz") - )?, - PathBuf::from("/foo/baz") - ); - - Ok(()) -} - -fn test_tcp() -> Result<()> { - info!("About to open a TCP connection to 1.1.1.1 port 80"); - - let mut stream = TcpStream::connect("one.one.one.one:80")?; - - let err = stream.try_clone(); - if let Err(err) = err { - info!( - "Duplication of file descriptors does not work (yet) on the ESP-IDF, as expected: {}", - err - ); - } - - stream.write_all("GET / HTTP/1.0\n\n".as_bytes())?; - - let mut result = Vec::new(); - - stream.read_to_end(&mut result)?; - - info!( - "1.1.1.1 returned:\n=================\n{}\n=================\nSince it returned something, all is OK", - std::str::from_utf8(&result)?); - - Ok(()) -} - -fn test_tcp_bind() -> Result<()> { - fn test_tcp_bind_accept() -> Result<()> { - info!("About to bind a simple echo service to port 8080"); - - let listener = TcpListener::bind("0.0.0.0:8080")?; - - for stream in listener.incoming() { - match stream { - Ok(stream) => { - info!("Accepted client"); - - thread::spawn(move || { - test_tcp_bind_handle_client(stream); - }); - } - Err(e) => { - error!("Error: {}", e); - } - } - } - - unreachable!() - } - - fn test_tcp_bind_handle_client(mut stream: TcpStream) { - // read 20 bytes at a time from stream echoing back to stream - loop { - let mut read = [0; 128]; - - match stream.read(&mut read) { - Ok(n) => { - if n == 0 { - // connection was closed - break; - } - stream.write_all(&read[0..n]).unwrap(); - } - Err(err) => { - panic!("{}", err); - } - } - } - } - - thread::spawn(|| test_tcp_bind_accept().unwrap()); - - Ok(()) -} - -fn test_timer( - mut eventloop: EspBackgroundEventLoop, - mut client: EspMqttClient>, -) -> Result { - use embedded_svc::event_bus::Postbox; - - info!("About to schedule a one-shot timer for after 2 seconds"); - let mut once_timer = EspTimerService::new()?.timer(|| { - info!("One-shot timer triggered"); - })?; - - once_timer.after(Duration::from_secs(2))?; - - thread::sleep(Duration::from_secs(3)); - - info!("About to schedule a periodic timer every five seconds"); - let mut periodic_timer = EspTimerService::new()?.timer(move || { - info!("Tick from periodic timer"); - - let now = EspSystemTime {}.now(); - - eventloop.post(&EventLoopMessage::new(now), None).unwrap(); - - client - .publish( - "rust-esp32-std-demo", - QoS::AtMostOnce, - false, - format!("Now is {:?}", now).as_bytes(), - ) - .unwrap(); - })?; - - periodic_timer.every(Duration::from_secs(5))?; - - Ok(periodic_timer) -} - -#[derive(Copy, Clone, Debug)] -struct EventLoopMessage(Duration); - -impl EventLoopMessage { - pub fn new(duration: Duration) -> Self { - Self(duration) - } -} - -impl EspTypedEventSource for EventLoopMessage { - fn source() -> *const c_types::c_char { - b"DEMO-SERVICE\0".as_ptr() as *const _ - } -} - -impl EspTypedEventSerializer for EventLoopMessage { - fn serialize( - event: &EventLoopMessage, - f: impl for<'a> FnOnce(&'a EspEventPostData) -> R, - ) -> R { - f(&unsafe { EspEventPostData::new(Self::source(), Self::event_id(), event) }) - } -} - -impl EspTypedEventDeserializer for EventLoopMessage { - fn deserialize( - data: &EspEventFetchData, - f: &mut impl for<'a> FnMut(&'a EventLoopMessage) -> R, - ) -> R { - f(unsafe { data.as_payload() }) - } -} - -fn test_eventloop() -> Result<(EspBackgroundEventLoop, EspBackgroundSubscription)> { - use embedded_svc::event_bus::EventBus; - - info!("About to start a background event loop"); - let mut eventloop = EspBackgroundEventLoop::new(&Default::default())?; - - info!("About to subscribe to the background event loop"); - let subscription = eventloop.subscribe(|message: &EventLoopMessage| { - info!("Got message from the event loop: {:?}", message.0); - })?; - - Ok((eventloop, subscription)) -} - -fn test_mqtt_client() -> Result>> { - info!("About to start MQTT client"); - - let conf = MqttClientConfiguration { - client_id: Some("rust-esp32-std-demo"), - crt_bundle_attach: Some(esp_idf_sys::esp_crt_bundle_attach), - - ..Default::default() - }; - - let (mut client, mut connection) = - EspMqttClient::new_with_conn("mqtts://broker.emqx.io:8883", &conf)?; - - info!("MQTT client started"); - - // Need to immediately start pumping the connection for messages, or else subscribe() and publish() below will not work - // Note that when using the alternative constructor - `EspMqttClient::new` - you don't need to - // spawn a new thread, as the messages will be pumped with a backpressure into the callback you provide. - // Yet, you still need to efficiently process each message in the callback without blocking for too long. - // - // Note also that if you go to http://tools.emqx.io/ and then connect and send a message to topic - // "rust-esp32-std-demo", the client configured here should receive it. - thread::spawn(move || { - info!("MQTT Listening for messages"); - - while let Some(msg) = connection.next() { - match msg { - Err(e) => info!("MQTT Message ERROR: {}", e), - Ok(msg) => info!("MQTT Message: {:?}", msg), - } - } - - info!("MQTT connection loop exit"); - }); - - client.subscribe("rust-esp32-std-demo", QoS::AtMostOnce)?; - - info!("Subscribed to all topics (rust-esp32-std-demo)"); - - client.publish( - "rust-esp32-std-demo", - QoS::AtMostOnce, - false, - "Hello from rust-esp32-std-demo!".as_bytes(), - )?; - - info!("Published a hello message to topic \"rust-esp32-std-demo\""); - - Ok(client) -} - -#[cfg(feature = "experimental")] -mod experimental { - use super::{thread, TcpListener, TcpStream}; - use log::info; - - use esp_idf_sys::c_types; - - pub fn test() -> anyhow::Result<()> { - #[cfg(not(esp_idf_version = "4.3"))] - test_tcp_bind_async()?; - - test_https_client()?; - - Ok(()) - } - - #[cfg(not(esp_idf_version = "4.3"))] - fn test_tcp_bind_async() -> anyhow::Result<()> { - async fn test_tcp_bind() -> smol::io::Result<()> { - /// Echoes messages from the client back to it. - async fn echo(stream: smol::Async) -> smol::io::Result<()> { - smol::io::copy(&stream, &mut &stream).await?; - Ok(()) - } - - // Create a listener. - let listener = smol::Async::::bind(([0, 0, 0, 0], 8081))?; - - // Accept clients in a loop. - loop { - let (stream, peer_addr) = listener.accept().await?; - info!("Accepted client: {}", peer_addr); - - // Spawn a task that echoes messages from the client back to it. - smol::spawn(echo(stream)).detach(); - } - } - - info!("About to bind a simple echo service to port 8081 using async (smol-rs)!"); - - #[allow(clippy::needless_update)] - { - esp_idf_sys::esp!(unsafe { - esp_idf_sys::esp_vfs_eventfd_register(&esp_idf_sys::esp_vfs_eventfd_config_t { - max_fds: 5, - ..Default::default() - }) - })?; - } - - thread::Builder::new().stack_size(4096).spawn(move || { - smol::block_on(test_tcp_bind()).unwrap(); - })?; - - Ok(()) - } - - fn test_https_client() -> anyhow::Result<()> { - use embedded_svc::http::{self, client::*, status, Headers, Status}; - use embedded_svc::io::Bytes; - use esp_idf_svc::http::client::*; - - let url = String::from("https://google.com"); - - info!("About to fetch content from {}", url); - - let mut client = EspHttpClient::new(&EspHttpClientConfiguration { - crt_bundle_attach: Some(esp_idf_sys::esp_crt_bundle_attach), - - ..Default::default() - })?; - - let response = client.get(&url)?.submit()?; - - let body: Result, _> = Bytes::<_, 64>::new(response.reader()).take(3084).collect(); - - let body = body?; - - info!( - "Body (truncated to 3K):\n{:?}", - String::from_utf8_lossy(&body).into_owned() - ); - - Ok(()) - } -} - -#[cfg(feature = "ttgo")] -fn ttgo_hello_world( - backlight: gpio::Gpio4, - dc: gpio::Gpio16, - rst: gpio::Gpio23, - spi: spi::SPI2, - sclk: gpio::Gpio18, - sdo: gpio::Gpio19, - cs: gpio::Gpio5, -) -> Result<()> { - info!("About to initialize the TTGO ST7789 LED driver"); - - let config = ::default() - .write_only(true) - .baudrate(80.MHz().into()); - - let mut backlight = backlight.into_output()?; - backlight.set_high()?; - - let di = SPIInterfaceNoCS::new( - spi::Master::::new( - spi, - spi::Pins { - sclk, - sdo, - sdi: Option::>::None, - cs: Some(cs), - }, - config, - )?, - dc.into_output()?, - ); - - let mut display = st7789::ST7789::new( - di, - rst.into_output()?, - // SP7789V is designed to drive 240x320 screens, even though the TTGO physical screen is smaller - 240, - 320, - ); - - display - .init(&mut delay::Ets) - .map_err(|e| anyhow::anyhow!("Display error: {:?}", e))?; - display - .set_orientation(st7789::Orientation::Portrait) - .map_err(|e| anyhow::anyhow!("Display error: {:?}", e))?; - - // The TTGO board's screen does not start at offset 0x0, and the physical size is 135x240, instead of 240x320 - let top_left = Point::new(52, 40); - let size = Size::new(135, 240); - - led_draw(&mut display.cropped(&Rectangle::new(top_left, size))) - .map_err(|e| anyhow::anyhow!("Display error: {:?}", e)) -} - -#[cfg(feature = "kaluga")] -fn kaluga_hello_world( - backlight: gpio::Gpio6, - dc: gpio::Gpio13, - rst: gpio::Gpio16, - spi: spi::SPI3, - sclk: gpio::Gpio15, - sdo: gpio::Gpio9, - cs: gpio::Gpio11, - ili9341: bool, -) -> Result<()> { - info!( - "About to initialize the Kaluga {} SPI LED driver", - if ili9341 { "ILI9341" } else { "ST7789" } - ); - - let config = ::default() - .baudrate((if ili9341 { 40 } else { 80 }).MHz().into()); - - let mut backlight = backlight.into_output()?; - backlight.set_high()?; - - let di = SPIInterfaceNoCS::new( - spi::Master::::new( - spi, - spi::Pins { - sclk, - sdo, - sdi: Option::>::None, - cs: Some(cs), - }, - config, - )?, - dc.into_output()?, - ); - - let reset = rst.into_output()?; - - if ili9341 { - let mut display = ili9341::Ili9341::new( - di, - reset, - &mut delay::Ets, - KalugaOrientation::Landscape, - ili9341::DisplaySize240x320, - )?; - - led_draw(&mut display).map_err(|e| anyhow::anyhow!("Display error: {:?}", e)) - } else { - let mut display = st7789::ST7789::new(di, reset, 320, 240); - - display - .init(&mut delay::Ets) - .map_err(|e| anyhow::anyhow!("Display error: {:?}", e))?; - display - .set_orientation(st7789::Orientation::Landscape) - .map_err(|e| anyhow::anyhow!("Display error: {:?}", e))?; - - led_draw(&mut display).map_err(|e| anyhow::anyhow!("Display error: {:?}", e)) - } -} - -#[cfg(feature = "heltec")] -fn heltec_hello_world( - rst: gpio::Gpio16, - i2c: i2c::I2C0, - sda: gpio::Gpio4, - scl: gpio::Gpio15, -) -> Result<()> { - info!("About to initialize the Heltec SSD1306 I2C LED driver"); - - let config = ::default().baudrate(400.kHz().into()); - - let di = ssd1306::I2CDisplayInterface::new(i2c::Master::::new( - i2c, - i2c::MasterPins { sda, scl }, - config, - )?); - - let mut delay = delay::Ets; - let mut reset = rst.into_output()?; - - reset.set_high()?; - delay.delay_ms(1 as u32); - - reset.set_low()?; - delay.delay_ms(10 as u32); - - reset.set_high()?; - - let mut display = ssd1306::Ssd1306::new( - di, - ssd1306::size::DisplaySize128x64, - ssd1306::rotation::DisplayRotation::Rotate0, - ) - .into_buffered_graphics_mode(); - - display - .init() - .map_err(|e| anyhow::anyhow!("Display error: {:?}", e))?; - - led_draw(&mut display).map_err(|e| anyhow::anyhow!("Display error: {:?}", e))?; - - display - .flush() - .map_err(|e| anyhow::anyhow!("Display error: {:?}", e))?; - - Ok(()) -} - -#[cfg(feature = "ssd1306g_spi")] -fn ssd1306g_hello_world_spi( - dc: gpio::Gpio4, - rst: gpio::Gpio16, - spi: spi::SPI3, - sclk: gpio::Gpio18, - sdo: gpio::Gpio23, - cs: gpio::Gpio5, -) -> Result<()> { - info!("About to initialize the SSD1306 SPI LED driver"); - - let config = ::default().baudrate(10.MHz().into()); - - let di = SPIInterfaceNoCS::new( - spi::Master::::new( - spi, - spi::Pins { - sclk, - sdo, - sdi: Option::>::None, - cs: Some(cs), - }, - config, - )?, - dc.into_output()?, - ); - - let mut delay = delay::Ets; - let mut reset = rst.into_output()?; - - reset.set_high()?; - delay.delay_ms(1 as u32); - - reset.set_low()?; - delay.delay_ms(10 as u32); - - reset.set_high()?; - - let mut display = ssd1306::Ssd1306::new( - di, - ssd1306::size::DisplaySize128x64, - ssd1306::rotation::DisplayRotation::Rotate180, - ) - .into_buffered_graphics_mode(); - - display - .init() - .map_err(|e| anyhow::anyhow!("Display error: {:?}", e))?; - - led_draw(&mut display).map_err(|e| anyhow::anyhow!("Display error: {:?}", e))?; - - display - .flush() - .map_err(|e| anyhow::anyhow!("Display error: {:?}", e))?; - - Ok(()) -} - -#[cfg(feature = "ssd1306g")] -fn ssd1306g_hello_world( - i2c: i2c::I2C0, - pwr: gpio::Gpio14, - scl: gpio::Gpio22, - sda: gpio::Gpio21, -) -> Result> { - info!("About to initialize a generic SSD1306 I2C LED driver"); - - let config = ::default().baudrate(400.kHz().into()); - - let di = ssd1306::I2CDisplayInterface::new(i2c::Master::::new( - i2c, - i2c::MasterPins { sda, scl }, - config, - )?); - - let mut delay = delay::Ets; - let mut power = pwr.into_output()?; - - // Powering an OLED display via an output pin allows one to shutdown the display - // when it is no longer needed so as to conserve power - // - // Of course, the I2C driver should also be properly de-initialized etc. - power.set_drive_strength(gpio::DriveStrength::I40mA)?; - power.set_high()?; - delay.delay_ms(10_u32); - - let mut display = ssd1306::Ssd1306::new( - di, - ssd1306::size::DisplaySize128x64, - ssd1306::rotation::DisplayRotation::Rotate0, - ) - .into_buffered_graphics_mode(); - - display - .init() - .map_err(|e| anyhow::anyhow!("Display error: {:?}", e))?; - - led_draw(&mut display).map_err(|e| anyhow::anyhow!("Display error: {:?}", e))?; - - display - .flush() - .map_err(|e| anyhow::anyhow!("Display error: {:?}", e))?; - - Ok(power) -} - -#[cfg(feature = "esp32s3_usb_otg")] -fn esp32s3_usb_otg_hello_world( - backlight: gpio::Gpio9, - dc: gpio::Gpio4, - rst: gpio::Gpio8, - spi: spi::SPI3, - sclk: gpio::Gpio6, - sdo: gpio::Gpio7, - cs: gpio::Gpio5, -) -> Result<()> { - info!("About to initialize the ESP32-S3-USB-OTG SPI LED driver ST7789VW"); - - let config = ::default().baudrate(80.MHz().into()); - - let mut backlight = backlight.into_output()?; - backlight.set_high()?; - - let di = SPIInterfaceNoCS::new( - spi::Master::::new( - spi, - spi::Pins { - sclk, - sdo, - sdi: Option::>::None, - cs: Some(cs), - }, - config, - )?, - dc.into_output()?, - ); - - let reset = rst.into_output()?; - - let mut display = st7789::ST7789::new(di, reset, 240, 240); - - display - .init(&mut delay::Ets) - .map_err(|e| anyhow::anyhow!("Display error: {:?}", e))?; - display - .set_orientation(st7789::Orientation::Landscape) - .map_err(|e| anyhow::anyhow!("Display error: {:?}", e))?; - - led_draw(&mut display).map_err(|e| anyhow::anyhow!("Led draw error: {:?}", e)) -} - -#[allow(dead_code)] -fn led_draw(display: &mut D) -> Result<(), D::Error> -where - D: DrawTarget + Dimensions, - D::Color: From, -{ - display.clear(Rgb565::BLACK.into())?; - - Rectangle::new(display.bounding_box().top_left, display.bounding_box().size) - .into_styled( - PrimitiveStyleBuilder::new() - .fill_color(Rgb565::BLUE.into()) - .stroke_color(Rgb565::YELLOW.into()) - .stroke_width(1) - .build(), - ) - .draw(display)?; - - Text::new( - "Hello Rust!", - Point::new(10, (display.bounding_box().size.height - 10) as i32 / 2), - MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE.into()), - ) - .draw(display)?; - - info!("LED rendering done"); - - Ok(()) -} - -#[allow(unused_variables)] -fn httpd(mutex: Arc<(Mutex>, Condvar)>) -> Result { - let server = idf::ServerRegistry::new() - .at("/") - .get(|_| Ok("Hello from Rust!".into()))? - .at("/foo") - .get(|_| bail!("Boo, something happened!"))? - .at("/bar") - .get(|_| { - Response::new(403) - .status_message("No permissions") - .body("You have no permissions to access this page".into()) - .into() - })? - .at("/panic") - .get(|_| panic!("User requested a panic!"))?; - - #[cfg(esp32s2)] - let server = httpd_ulp_endpoints(server, mutex)?; - - server.start(&Default::default()) -} - -#[cfg(esp32s2)] -fn httpd_ulp_endpoints( - server: ServerRegistry, - mutex: Arc<(Mutex>, Condvar)>, -) -> Result { - server - .at("/ulp") - .get(|_| { - Ok(r#" - - - -
- Connect a LED to ESP32-S2 GPIO Pin 04 and GND.
- Blink it with ULP times - -
- - - "#.into()) - })? - .at("/ulp_start") - .post(move |mut request| { - let body = request.as_bytes()?; - - let cycles = url::form_urlencoded::parse(&body) - .filter(|p| p.0 == "cycles") - .map(|p| str::parse::(&p.1).map_err(Error::msg)) - .next() - .ok_or(anyhow::anyhow!("No parameter cycles"))??; - - let mut wait = mutex.0.lock().unwrap(); - - *wait = Some(cycles); - mutex.1.notify_one(); - - Ok(format!( - r#" - - - - About to sleep now. The ULP chip should blink the LED {} times and then wake me up. Bye! - - - "#, - cycles).to_owned().into()) - }) -} - -#[cfg(esp32s2)] -fn start_ulp(mut ulp: esp_idf_hal::ulp::ULP, cycles: u32) -> Result<()> { - let cycles_var = CYCLES as *mut u32; - - unsafe { - ulp.load(ULP)?; - info!("RiscV ULP binary loaded successfully"); - - info!( - "Default ULP LED blink cycles: {}", - ulp.read_var(cycles_var)? - ); - - ulp.write_var(cycles_var, cycles)?; - info!( - "Sent {} LED blink cycles to the ULP", - ulp.read_var(cycles_var)? - ); - - ulp.start()?; - info!("RiscV ULP started"); - - esp!(esp_idf_sys::esp_sleep_enable_ulp_wakeup())?; - info!("Wakeup from ULP enabled"); - - // Wake up by a timer in 60 seconds - info!("About to get to sleep now. Will wake up automatically either in 1 minute, or once the ULP has done blinking the LED"); - esp_idf_sys::esp_deep_sleep(Duration::from_secs(60).as_micros() as u64); - } - - Ok(()) -} - -#[cfg(not(feature = "qemu"))] -#[allow(dead_code)] -fn wifi( - netif_stack: Arc, - sys_loop_stack: Arc, - default_nvs: Arc, -) -> Result> { - let mut wifi = Box::new(EspWifi::new(netif_stack, sys_loop_stack, default_nvs)?); - - info!("Wifi created, about to scan"); - - let ap_infos = wifi.scan()?; - - let ours = ap_infos.into_iter().find(|a| a.ssid == SSID); - - let channel = if let Some(ours) = ours { - info!( - "Found configured access point {} on channel {}", - SSID, ours.channel - ); - Some(ours.channel) - } else { - info!( - "Configured access point {} not found during scanning, will go with unknown channel", - SSID - ); - None - }; - - wifi.set_configuration(&Configuration::Mixed( - ClientConfiguration { - ssid: SSID.into(), - password: PASS.into(), - channel, - ..Default::default() - }, - AccessPointConfiguration { - ssid: "aptest".into(), - channel: channel.unwrap_or(1), - ..Default::default() - }, - ))?; - - info!("Wifi configuration set, about to get status"); - - wifi.wait_status_with_timeout(Duration::from_secs(20), |status| !status.is_transitional()) - .map_err(|e| anyhow::anyhow!("Unexpected Wifi status: {:?}", e))?; - - let status = wifi.get_status(); - - if let Status( - ClientStatus::Started(ClientConnectionStatus::Connected(ClientIpStatus::Done(ip_settings))), - ApStatus::Started(ApIpStatus::Done), - ) = status - { - info!("Wifi connected"); - - ping(&ip_settings)?; - } else { - bail!("Unexpected Wifi status: {:?}", status); - } - - Ok(wifi) -} - -#[cfg(any(feature = "qemu", feature = "w5500", feature = "ip101"))] -fn eth_configure(mut eth: Box>) -> Result>> { - info!("Eth created"); - - eth.set_configuration(ð::Configuration::Client(Default::default()))?; - - info!("Eth configuration set, about to get status"); - - eth.wait_status_with_timeout(Duration::from_secs(10), |status| !status.is_transitional()) - .map_err(|e| anyhow::anyhow!("Unexpected Eth status: {:?}", e))?; - - let status = eth.get_status(); - - if let eth::Status::Started(eth::ConnectionStatus::Connected(eth::IpStatus::Done(Some( - ip_settings, - )))) = status - { - info!("Eth connected"); - - ping(&ip_settings)?; - } else { - bail!("Unexpected Eth status: {:?}", status); - } - - Ok(eth) -} - -fn ping(ip_settings: &ipv4::ClientSettings) -> Result<()> { - info!("About to do some pings for {:?}", ip_settings); - - let ping_summary = - ping::EspPing::default().ping(ip_settings.subnet.gateway, &Default::default())?; - if ping_summary.transmitted != ping_summary.received { - bail!( - "Pinging gateway {} resulted in timeouts", - ip_settings.subnet.gateway - ); - } - - info!("Pinging done"); - - Ok(()) -} - -#[cfg(not(feature = "qemu"))] -#[cfg(esp_idf_config_lwip_ipv4_napt)] -fn enable_napt(wifi: &mut EspWifi) -> Result<()> { - wifi.with_router_netif_mut(|netif| netif.unwrap().enable_napt(true)); - - info!("NAPT enabled on the WiFi SoftAP!"); - - Ok(()) -} - -// Kaluga needs customized screen orientation commands -// (not a surprise; quite a few ILI9341 boards need these as evidenced in the TFT_eSPI & lvgl ESP32 C drivers) -pub enum KalugaOrientation { - Portrait, - PortraitFlipped, - Landscape, - LandscapeFlipped, -} - -impl ili9341::Mode for KalugaOrientation { - fn mode(&self) -> u8 { - match self { - Self::Portrait => 0, - Self::Landscape => 0x20 | 0x40, - Self::PortraitFlipped => 0x80 | 0x40, - Self::LandscapeFlipped => 0x80 | 0x20, - } - } - - fn is_landscape(&self) -> bool { - matches!(self, Self::Landscape | Self::LandscapeFlipped) - } -} - -#[cfg(feature = "waveshare_epd")] -fn waveshare_epd_hello_world( - spi: spi::SPI2, - sclk: gpio::Gpio13, - sdo: gpio::Gpio14, - cs: gpio::Gpio15, - busy_in: gpio::Gpio25, - dc: gpio::Gpio27, - rst: gpio::Gpio26, -) -> Result<()> { - info!("About to initialize Waveshare 4.2 e-paper display"); - let cs = cs.into_output().unwrap(); - let busy_in = busy_in.into_input().unwrap(); - let dc = dc.into_output().unwrap(); - let rst = rst.into_output().unwrap(); - - let config = ::default().baudrate(26.MHz().into()); - - let mut my_spi = spi::Master::::new( - spi, - spi::Pins { - sclk: sclk, - sdo: sdo, - sdi: Option::>::None, - cs: Option::>::None, - }, - config, - ) - .unwrap(); - // Setup EPD - let mut epd = Epd4in2::new(&mut my_spi, cs, busy_in, dc, rst, &mut delay::Ets).unwrap(); - // Use display graphics from embedded-graphics - let mut buffer = - vec![DEFAULT_BACKGROUND_COLOR.get_byte_value(); WIDTH as usize / 8 * HEIGHT as usize]; - let mut display = VarDisplay::new(WIDTH, HEIGHT, &mut buffer); - - let style = MonoTextStyle::new(&FONT_10X20, BinaryColor::On); - - // Create a text at position (20, 30) and draw it using the previously defined style - Text::new("Hello Rust!", Point::new(20, 30), style).draw(&mut display)?; - - // Display updated frame - epd.update_frame(&mut my_spi, &display.buffer(), &mut delay::Ets)?; - epd.display_frame(&mut my_spi, &mut delay::Ets)?; - Ok(()) } diff --git a/src/simple_ota.rs b/src/simple_ota.rs new file mode 100644 index 00000000..fb3bdfdb --- /dev/null +++ b/src/simple_ota.rs @@ -0,0 +1,137 @@ +use esp_idf_hal::delay; +use esp_idf_hal::gpio; +use esp_idf_hal::prelude::*; +use esp_idf_hal::uart::UartTxDriver; +use esp_idf_hal::uart::*; + +use esp_idf_hal::uart::UartDriver; +use std::sync::mpsc::TryRecvError; +use std::thread; +use std::time::Duration; + +use messages::*; +use std::sync::{ + mpsc::{channel, Receiver, Sender}, + Arc, Mutex, +}; + +use crate::uart_update; + +#[derive(Debug)] +pub enum Error { + StdIo(std::io::Error), + Esp(esp_idf_sys::EspError), + Anyhow(anyhow::Error), + TryRecv(TryRecvError), + Other(String), +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::StdIo(err) + } +} + +impl From for Error { + fn from(err: esp_idf_sys::EspError) -> Self { + Error::Esp(err) + } +} + +impl From for Error { + fn from(err: anyhow::Error) -> Self { + Error::Anyhow(err) + } +} +impl From for Error { + fn from(err: TryRecvError) -> Self { + Error::TryRecv(err) + } +} + +pub fn serial_ota() -> std::result::Result<(), Error> { + thread::Builder::new() + .name("Serial thread".to_string()) + .stack_size(25 * 1024) + .spawn(move || { + let peripherals = Peripherals::take().unwrap(); + + let config = config::Config::new().baudrate(Hertz(115_200)); + let uart = UartDriver::new( + peripherals.uart1, + peripherals.pins.gpio17, + peripherals.pins.gpio16, + Option::::None, + Option::::None, + &config, + ) + .unwrap(); + + let (serial_tx, serial_rx): (Sender>, _) = channel(); + let serial_rx = Arc::new(Mutex::new(serial_rx)); + let serial_tx = Arc::new(Mutex::new(serial_tx)); + let (updater_tx, updater_rx): (Sender>, _) = channel(); + + uart_update::spawn(updater_rx, serial_tx); + + let (uart_tx, uart_rx) = uart.split(); + let mut uart_tx = Arc::new(Mutex::new(uart_tx)); + let uart_rx = Arc::new(Mutex::new(uart_rx)); + const BUF_SIZE: usize = 1024; + let mut buf = [0u8; BUF_SIZE]; + loop { + // Wait until there are bytes available to read + let count: usize = match uart_rx.lock().unwrap().count() { + Ok(count) => count as usize, + Err(e) => { + println!("Error occured: {:?}", e); + 0 + } + }; + + if count == 0 { + if let Err(e) = handle_uart_tx(&mut uart_tx, &serial_rx) {} + thread::yield_now(); + continue; + } + println!("Count: {}", count); + + let read_result = uart_rx + .lock() + .unwrap() + .read(&mut buf[..count], delay::NON_BLOCK); + match read_result { + Ok(len) => { + let copied = Vec::from(&buf[..len as usize]); + updater_tx.send(copied).unwrap(); + } + Err(e) => { + println!("Error during read: {:?}", e); + continue; + } + }; + } + fn handle_uart_tx( + uart_tx: &mut Arc>, + serial_rx: &Arc>>>, + ) -> Result<(), Error> { + let msg = match serial_rx.lock().unwrap().try_recv() { + Ok(v) => v, + Err(e) => return Err(e.into()), + }; + + let data = msg.serialize()?; + match uart_tx.lock().unwrap().write(data.as_slice()) { + Ok(_) => Ok(()), + Err(e) => { + println!("Failed to write: {:?}", e); + Err(e.into()) + } + } + } + })?; + loop { + thread::sleep(Duration::from_secs(10)); + println!("ping"); + } +} diff --git a/src/uart_update.rs b/src/uart_update.rs new file mode 100644 index 00000000..e5e6d006 --- /dev/null +++ b/src/uart_update.rs @@ -0,0 +1,135 @@ +use std::{ + sync::{ + mpsc::{Receiver, Sender}, + Arc, Mutex, + }, + thread, +}; + +use embedded_svc::io::Write; +use esp_idf_svc::ota::EspOta; +use esp_idf_sys::esp_restart; +use messages::{Message, MessageTypeHost, MessageTypeMcu, UpdateStatus}; +use smlang::statemachine; + +// Updater statemachine +statemachine! { + transitions: { + *Init + UpdateStart = WaitingForData, + WaitingForData + SegmentOk = WaitingForData, + + Init | WaitingForData + Cancel = Init, + } +} +pub struct Context; +impl StateMachineContext for Context {} + +/// Spawn a new task that will handles raw messages from UART +pub fn spawn(rx: Receiver>, tx: Arc>>>) { + let builder = thread::Builder::new() + .name("uart_update thread".to_string()) + .stack_size(100 * 1024); + let _handler = builder.spawn(move || { + let mut sm = StateMachine::new(Context); + + let mut ota = match EspOta::new() { + Ok(v) => { + println!("Constructed ota object"); + v + } + Err(e) => { + println!("Could not construct ota object... {:?}", e); + return e; + } + }; + let mut ota_update = None; + + let mut expected_seg_id = 0; + loop { + println!("Running loop on uart_update"); + if let Ok(data) = rx.recv() { + // Deserialize message from UART + let msg = match Message::::deserialize(&data[..]) { + Ok(msg) => msg.payload.message_type, + Err(e) => { + println!("Error occured in deserialize: {:?}", e); + continue; + } + }; + match msg { + MessageTypeHost::UpdateStart if sm.state == States::Init => { + println!("Starting update"); + + println!( + "Current slot: {:?}", + match ota.get_running_slot() { + Ok(val) => val.label.to_string(), + Err(e) => e.to_string(), + } + ); + println!("Updating slot: {:?}", ota.get_update_slot().unwrap().label); + + ota_update = Some(ota.initiate_update().unwrap()); + tx.lock() + .unwrap() + .send(Message::new(MessageTypeMcu::UpdateStartStatus( + UpdateStatus::Ok, + ))) + .unwrap(); + expected_seg_id = 0; + + sm.process_event(Events::UpdateStart).unwrap(); + } + MessageTypeHost::UpdateSegment(id, segment) + if sm.state == States::WaitingForData => + { + if expected_seg_id != id { + tx.lock() + .unwrap() + .send(Message::new(MessageTypeMcu::UpdateSegmentStatus( + UpdateStatus::Retry(Some(expected_seg_id as u16)), + ))) + .unwrap(); + continue; + } + + let ota_update = ota_update.as_mut().unwrap(); + match ota_update.write(segment) { + Ok(_) => (), + Err(e) => { + println!("Received invalid segment: {:?} ({:?})", segment, e); + continue; + } + } + + expected_seg_id = id + 1; + tx.lock() + .unwrap() + .send(Message::new(MessageTypeMcu::UpdateSegmentStatus( + UpdateStatus::Ok, + ))) + .unwrap(); + + sm.process_event(Events::SegmentOk).unwrap(); + } + MessageTypeHost::UpdateEnd if sm.state == States::WaitingForData => { + let ota_update = ota_update.take().unwrap(); + ota_update.complete().unwrap(); + + println!("Restarting the system!"); + unsafe { esp_restart() }; + } + MessageTypeHost::Cancel => match sm.state { + States::Init => (), + States::WaitingForData => { + let ota_update = ota_update.take().unwrap(); + ota_update.abort().unwrap(); + sm.process_event(Events::Cancel).unwrap(); + } + }, + _ => println!("Invalid state"), + }; + } + } + }); +} diff --git a/ulp/rust-esp32-ulp-blink b/ulp/rust-esp32-ulp-blink deleted file mode 100755 index 658c3cb4..00000000 Binary files a/ulp/rust-esp32-ulp-blink and /dev/null differ