commit 17a8e573c0bcafd31786d27b9631f82728e2086d Author: Sebastian Hugentobler Date: Sun Nov 17 10:27:30 2024 +0100 initial commit diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c722f2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +/.direnv diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ce5c413 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,11 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "mqtt-protocol" +version = "0.1.0" + +[[package]] +name = "serial-comm" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a70ebac --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[workspace] +members = ["serial-comm", "mqtt-protocol"] +resolver = "2" + +[workspace.package] +version = "0.1.0" +edition = "2021" +description = "rp2040 temperature sensor" +license = "GPL-3.0-or-later" + +[profile.release] +strip = true diff --git a/controller/.cargo/config.toml b/controller/.cargo/config.toml new file mode 100644 index 0000000..bd0a37a --- /dev/null +++ b/controller/.cargo/config.toml @@ -0,0 +1,8 @@ +[target.'cfg(all(target_arch = "arm", target_os = "none"))'] +runner = "probe-rs run --chip RP2040" + +[build] +target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+ + +[env] +DEFMT_LOG = "info" diff --git a/controller/Cargo.lock b/controller/Cargo.lock new file mode 100644 index 0000000..4dc7448 --- /dev/null +++ b/controller/Cargo.lock @@ -0,0 +1,1973 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "as-slice" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45403b49e3954a4b8428a0ac21a4b7afadccf92bfd96273f1a58cd4812496ae0" +dependencies = [ + "generic-array 0.12.4", + "generic-array 0.13.3", + "generic-array 0.14.7", + "stable_deref_trait", +] + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + +[[package]] +name = "atomic-polyfill" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ff7eb3f316534d83a8a2c3d1674ace8a5a71198eba31e2e2b597833f699b28" +dependencies = [ + "critical-section", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "atomic-pool" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c5fc22e05ec2884db458bf307dc7b278c9428888d2b6e6fad9c0ae7804f5f6" +dependencies = [ + "as-slice 0.1.5", + "as-slice 0.2.1", + "atomic-polyfill 1.0.3", + "stable_deref_trait", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + +[[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 = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitfield" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719" + +[[package]] +name = "bitfield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "controller" +version = "0.1.0" +dependencies = [ + "cortex-m", + "cortex-m-rt", + "critical-section", + "cyw43", + "cyw43-pio", + "defmt", + "defmt-rtt", + "embassy-embedded-hal", + "embassy-executor", + "embassy-futures", + "embassy-net", + "embassy-net-wiznet", + "embassy-rp", + "embassy-sync 0.6.0", + "embassy-time", + "embassy-usb", + "embedded-hal-async", + "embedded-hal-bus", + "embedded-io-async", + "embedded-storage", + "fixed", + "fixed-macro", + "futures", + "log", + "mqtt-protocol", + "panic-probe", + "pio", + "pio-proc", + "portable-atomic", + "rand", + "serial-comm", + "static_cell", +] + +[[package]] +name = "cortex-m" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9" +dependencies = [ + "bare-metal", + "bitfield 0.13.2", + "embedded-hal 0.2.7", + "volatile-register", +] + +[[package]] +name = "cortex-m-rt" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801d4dec46b34c299ccf6b036717ae0fce602faa4f4fe816d9013b9a7c9f5ba6" +dependencies = [ + "cortex-m-rt-macros", +] + +[[package]] +name = "cortex-m-rt-macros" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37549a379a9e0e6e576fd208ee60394ccb8be963889eebba3ffe0980364f472" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "crc-any" +version = "2.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "774646b687f63643eb0f4bf13dc263cb581c8c9e57973b6ddf78bda3994d88df" +dependencies = [ + "debug-helper", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "cyw43" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04c691efd1af993c7c4cb79eb13da4b901c1e8c6d148280e2a36ae2885f83218" +dependencies = [ + "cortex-m", + "cortex-m-rt", + "defmt", + "embassy-futures", + "embassy-net-driver-channel 0.3.0", + "embassy-sync 0.6.0", + "embassy-time", + "embedded-hal 1.0.0", + "futures", + "heapless 0.8.0", + "num_enum", +] + +[[package]] +name = "cyw43-pio" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22bb644457cfefdd1f0eeea7a3a708a61f9ab223aaf56b7f50589180808d302a" +dependencies = [ + "cyw43", + "defmt", + "embassy-rp", + "fixed", + "pio", + "pio-proc", +] + +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.87", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "debug-helper" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" + +[[package]] +name = "defmt" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a99dd22262668b887121d4672af5a64b238f026099f1a2a1b322066c9ecfe9e0" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54f0216f6c5acb5ae1a47050a6645024e6edafc2ee32d421955eccfef12ef92e" +dependencies = [ + "defmt-parser", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "defmt-parser" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "269924c02afd7f94bc4cecbfa5c379f6ffcf9766b3408fe63d22c728654eccd0" +dependencies = [ + "thiserror", +] + +[[package]] +name = "defmt-rtt" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab697b3dbbc1750b7c8b821aa6f6e7f2480b47a99bc057a2ed7b170ebef0c51" +dependencies = [ + "critical-section", + "defmt", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "embassy-embedded-hal" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5794414bc20e0d750f145bc0e82366b19dd078e9e075e8331fb8dd069a1cb6a2" +dependencies = [ + "defmt", + "embassy-futures", + "embassy-sync 0.6.0", + "embassy-time", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "embedded-storage", + "embedded-storage-async", + "nb 1.1.0", +] + +[[package]] +name = "embassy-executor" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64f84599b0f4296b92a4b6ac2109bc02340094bda47b9766c5f9ec6a318ebf8" +dependencies = [ + "cortex-m", + "critical-section", + "defmt", + "document-features", + "embassy-executor-macros", + "embassy-time-driver", + "embassy-time-queue-driver", +] + +[[package]] +name = "embassy-executor-macros" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3577b1e9446f61381179a330fc5324b01d511624c55f25e3c66c9e3c626dbecf" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "embassy-futures" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f878075b9794c1e4ac788c95b728f26aa6366d32eeb10c7051389f898f7d067" + +[[package]] +name = "embassy-hal-internal" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef3bac31ec146321248a169e9c7b5799f1e0b3829c7a9b324cb4600a7438f59" +dependencies = [ + "cortex-m", + "critical-section", + "defmt", + "num-traits", +] + +[[package]] +name = "embassy-net" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cf91dd36dfd623de32242af711fd294d41159f02130052fc93c5c5ba93febe" +dependencies = [ + "as-slice 0.2.1", + "atomic-pool", + "defmt", + "document-features", + "embassy-net-driver", + "embassy-sync 0.5.0", + "embassy-time", + "embedded-io-async", + "embedded-nal-async", + "futures", + "generic-array 0.14.7", + "heapless 0.8.0", + "managed", + "smoltcp", + "stable_deref_trait", +] + +[[package]] +name = "embassy-net-driver" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524eb3c489760508f71360112bca70f6e53173e6fe48fc5f0efd0f5ab217751d" +dependencies = [ + "defmt", +] + +[[package]] +name = "embassy-net-driver-channel" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "584ab4da7e5612efaa7d55ee76161d9549adf788eab48d49362eddbf322f9933" +dependencies = [ + "embassy-futures", + "embassy-net-driver", + "embassy-sync 0.3.0", +] + +[[package]] +name = "embassy-net-driver-channel" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4818c32afec43e3cae234f324bad9a976c9aa7501022d26ff60a4017a1a006b7" +dependencies = [ + "embassy-futures", + "embassy-net-driver", + "embassy-sync 0.6.0", +] + +[[package]] +name = "embassy-net-wiznet" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "114d6894cb781839c50ccc75df5ec41a0059ec4989f34af83bc87bfe3a4274c4" +dependencies = [ + "defmt", + "embassy-futures", + "embassy-net-driver-channel 0.2.0", + "embassy-time", + "embedded-hal 1.0.0", + "embedded-hal-async", +] + +[[package]] +name = "embassy-rp" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5d0300b0ed5229bf1c25488c64c1ce55024c5153c246f378b1f2e353d5ec9a" +dependencies = [ + "atomic-polyfill 1.0.3", + "cfg-if", + "cortex-m", + "cortex-m-rt", + "critical-section", + "defmt", + "document-features", + "embassy-embedded-hal", + "embassy-futures", + "embassy-hal-internal", + "embassy-sync 0.6.0", + "embassy-time", + "embassy-time-driver", + "embassy-usb-driver", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "embedded-hal-nb", + "embedded-io", + "embedded-io-async", + "embedded-storage", + "embedded-storage-async", + "fixed", + "nb 1.1.0", + "pio", + "pio-proc", + "rand_core", + "rp-pac", + "rp2040-boot2", +] + +[[package]] +name = "embassy-sync" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0525b466ca3ace30b57f2db868a35215dfaecd038d8668cb2db03feb7c069a0" +dependencies = [ + "cfg-if", + "critical-section", + "futures-util", + "heapless 0.7.16", +] + +[[package]] +name = "embassy-sync" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd938f25c0798db4280fcd8026bf4c2f48789aebf8f77b6e5cf8a7693ba114ec" +dependencies = [ + "cfg-if", + "critical-section", + "embedded-io-async", + "futures-util", + "heapless 0.8.0", +] + +[[package]] +name = "embassy-sync" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e0c49ff02ebe324faf3a8653ba91582e2d0a7fdef5bc88f449d5aa1bfcc05c" +dependencies = [ + "cfg-if", + "critical-section", + "defmt", + "embedded-io-async", + "futures-util", + "heapless 0.8.0", +] + +[[package]] +name = "embassy-time" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "158080d48f824fad101d7b2fae2d83ac39e3f7a6fa01811034f7ab8ffc6e7309" +dependencies = [ + "cfg-if", + "critical-section", + "defmt", + "document-features", + "embassy-time-driver", + "embassy-time-queue-driver", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "futures-util", + "heapless 0.8.0", +] + +[[package]] +name = "embassy-time-driver" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0c214077aaa9206958b16411c157961fb7990d4ea628120a78d1a5a28aed24" +dependencies = [ + "document-features", +] + +[[package]] +name = "embassy-time-queue-driver" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1177859559ebf42cd24ae7ba8fe6ee707489b01d0bf471f8827b7b12dcb0bc0" + +[[package]] +name = "embassy-usb" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d0b882133fa684b9d4652351cd7aac5afe8a2c2bf4a7da59f442ff61087cda2" +dependencies = [ + "defmt", + "embassy-futures", + "embassy-net-driver-channel 0.3.0", + "embassy-sync 0.6.0", + "embassy-usb-driver", + "heapless 0.8.0", + "ssmarshal", + "usbd-hid", +] + +[[package]] +name = "embassy-usb-driver" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fc247028eae04174b6635104a35b1ed336aabef4654f5e87a8f32327d231970" +dependencies = [ + "defmt", +] + +[[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 = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" + +[[package]] +name = "embedded-hal-async" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884" +dependencies = [ + "embedded-hal 1.0.0", +] + +[[package]] +name = "embedded-hal-bus" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d3980bf28e8577db59fe2bdb3df868a419469d2cecb363644eea2b6f7797669" +dependencies = [ + "critical-section", + "embedded-hal 1.0.0", + "embedded-hal-async", + "portable-atomic", +] + +[[package]] +name = "embedded-hal-nb" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba4268c14288c828995299e59b12babdbe170f6c6d73731af1b4648142e8605" +dependencies = [ + "embedded-hal 1.0.0", + "nb 1.1.0", +] + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +dependencies = [ + "defmt", +] + +[[package]] +name = "embedded-io-async" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff09972d4073aa8c299395be75161d582e7629cd663171d62af73c8d50dba3f" +dependencies = [ + "defmt", + "embedded-io", +] + +[[package]] +name = "embedded-nal" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a943fad5ed3d3f8a00f1e80f6bba371f1e7f0df28ec38477535eb318dc19cc" +dependencies = [ + "nb 1.1.0", + "no-std-net", +] + +[[package]] +name = "embedded-nal-async" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72229137a4fc12d239b0b7f50f04b30790678da6d782a0f3f1909bf57ec4b759" +dependencies = [ + "embedded-io-async", + "embedded-nal", + "no-std-net", +] + +[[package]] +name = "embedded-storage" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21dea9854beb860f3062d10228ce9b976da520a73474aed3171ec276bc0c032" + +[[package]] +name = "embedded-storage-async" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1763775e2323b7d5f0aa6090657f5e21cfa02ede71f5dc40eead06d64dcd15cc" +dependencies = [ + "embedded-storage", +] + +[[package]] +name = "ena" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c533630cf40e9caa44bd91aadc88a75d75a4c3a12b4cfde353cbed41daa1e1f1" +dependencies = [ + "log", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fixed" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c6e0b89bf864acd20590dbdbad56f69aeb898abfc9443008fd7bd48b2cc85a" +dependencies = [ + "az", + "bytemuck", + "half", + "typenum", +] + +[[package]] +name = "fixed-macro" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0c48af8cb14e02868f449f8a2187bd78af7a08da201fdc78d518ecb1675bc" +dependencies = [ + "fixed", + "fixed-macro-impl", + "fixed-macro-types", +] + +[[package]] +name = "fixed-macro-impl" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c93086f471c0a1b9c5e300ea92f5cd990ac6d3f8edf27616ef624b8fa6402d4b" +dependencies = [ + "fixed", + "paste", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "fixed-macro-types" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "044a61b034a2264a7f65aa0c3cd112a01b4d4ee58baace51fead3f21b993c7e4" +dependencies = [ + "fixed", + "fixed-macro-impl", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-sink", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f797e67af32588215eaaab8327027ee8e71b9dd0b2b26996aedf20c030fce309" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "half" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" + +[[package]] +name = "heapless" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db04bc24a18b9ea980628ecf00e6c0264f3c1426dac36c00cb49b6fbad8b0743" +dependencies = [ + "atomic-polyfill 0.1.11", + "hash32 0.2.1", + "rustc_version 0.4.0", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "defmt", + "hash32 0.3.1", + "stable_deref_trait", +] + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +dependencies = [ + "equivalent", + "hashbrown 0.14.2", +] + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "lalrpop" +version = "0.19.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a1cbf952127589f2851ab2046af368fd20645491bb4b376f04b7f94d7a9837b" +dependencies = [ + "ascii-canvas", + "bit-set", + "diff", + "ena", + "is-terminal", + "itertools", + "lalrpop-util", + "petgraph", + "regex", + "regex-syntax 0.6.29", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", +] + +[[package]] +name = "lalrpop-util" +version = "0.19.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3c48237b9604c5a4702de6b824e02006c3214327564636aef27c1028a8fa0ed" +dependencies = [ + "regex", +] + +[[package]] +name = "libc" +version = "0.2.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" + +[[package]] +name = "linux-raw-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "mqtt-protocol" +version = "0.1.0" + +[[package]] +name = "nb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" +dependencies = [ + "nb 1.1.0", +] + +[[package]] +name = "nb" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "panic-probe" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4047d9235d1423d66cc97da7d07eddb54d4f154d6c13805c6d0793956f4f25b0" +dependencies = [ + "cortex-m", + "defmt", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pio" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e09694b50f89f302ed531c1f2a7569f0be5867aee4ab4f8f729bbeec0078e3" +dependencies = [ + "arrayvec", + "num_enum", + "paste", +] + +[[package]] +name = "pio-parser" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77532c2b8279aef98dfc7207ef15298a5a3d6b6cc76ccc8b65913d69f3a8dd6b" +dependencies = [ + "lalrpop", + "lalrpop-util", + "pio", + "regex-syntax 0.6.29", +] + +[[package]] +name = "pio-proc" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b04dc870fb3a4fd8b3e4ca8c61b53bc8ac4eb78b66805d2b3c2e5c4829e0d7a" +dependencies = [ + "codespan-reporting", + "lalrpop-util", + "pio", + "pio-parser", + "proc-macro-error", + "proc-macro2", + "quote", + "regex-syntax 0.6.29", + "syn 1.0.109", +] + +[[package]] +name = "portable-atomic" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" +dependencies = [ + "critical-section", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall 0.2.16", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rp-pac" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30f6c4c846269293db805e9c77864ff7b923395b480550df44f0868e3765337" +dependencies = [ + "cortex-m", + "cortex-m-rt", +] + +[[package]] +name = "rp2040-boot2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c92f344f63f950ee36cf4080050e4dce850839b9175da38f9d2ffb69b4dbb21" +dependencies = [ + "crc-any", +] + +[[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.20", +] + +[[package]] +name = "rustix" +version = "0.38.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[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.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" + +[[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.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "serial-comm" +version = "0.1.0" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "smallvec" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" + +[[package]] +name = "smoltcp" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a1a996951e50b5971a2c8c0fa05a381480d70a933064245c4a223ddc87ccc97" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "cfg-if", + "defmt", + "heapless 0.8.0", + "managed", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "ssmarshal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e6ad23b128192ed337dfa4f1b8099ced0c2bf30d61e551b65fda5916dbb850" +dependencies = [ + "encode_unicode", + "serde", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_cell" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89b0684884a883431282db1e4343f34afc2ff6996fe1f4a1664519b66e14c1e" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "usb-device" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98816b1accafbb09085168b90f27e93d790b4bfa19d883466b5e53315b5f06a6" +dependencies = [ + "heapless 0.8.0", + "portable-atomic", +] + +[[package]] +name = "usbd-hid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f291ab53d428685cc780f08a2eb9d5d6ff58622db2b36e239a4f715f1e184c" +dependencies = [ + "serde", + "ssmarshal", + "usb-device", + "usbd-hid-macros", +] + +[[package]] +name = "usbd-hid-descriptors" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee54712c5d778d2fb2da43b1ce5a7b5060886ef7b09891baeb4bf36910a3ed" +dependencies = [ + "bitfield 0.14.0", +] + +[[package]] +name = "usbd-hid-macros" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb573c76e7884035ac5e1ab4a81234c187a82b6100140af0ab45757650ccda38" +dependencies = [ + "byteorder", + "hashbrown 0.13.2", + "log", + "proc-macro2", + "quote", + "serde", + "syn 1.0.109", + "usbd-hid-descriptors", +] + +[[package]] +name = "vcell" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de437e2a6208b014ab52972a27e59b33fa2920d3e00fe05026167a1c509d19cc" +dependencies = [ + "vcell", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[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-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] diff --git a/controller/Cargo.toml b/controller/Cargo.toml new file mode 100644 index 0000000..85dfcc0 --- /dev/null +++ b/controller/Cargo.toml @@ -0,0 +1,68 @@ +[workspace] + +[package] +edition = "2021" +name = "controller" +version = "0.1.0" + +[dependencies] +embassy-embedded-hal = { version = "0.2.0",features = ["defmt"] } +embassy-sync = { version = "0.6.0", features = ["defmt"] } +embassy-executor = { version = "0.6.3", features = [ + "arch-cortex-m", + "task-arena-size-98304", + "executor-thread", + "executor-interrupt", + "defmt", + "integrated-timers", +] } + +embassy-time = { version = "0.3.2", features = ["defmt", "defmt-timestamp-uptime"] } +embassy-rp = { version = "0.2.0", features = [ + "defmt", + "unstable-pac", + "time-driver", + "critical-section-impl", +] } +embassy-usb = { version = "0.3.0", features = ["defmt"] } +embassy-net = { version = "0.4.0", features = [ + "defmt", + "tcp", + "udp", + "dhcpv4", + "medium-ethernet", +] } +embassy-net-wiznet = { version = "0.1.0", features = ["defmt"] } +embassy-futures = "0.1.1" +cyw43 = { version = "0.2.0", features = ["defmt", "firmware-logs"] } +cyw43-pio = { version = "0.2.0", features = ["defmt", "overclock"] } +defmt = "0.3.8" +defmt-rtt = "0.4.1" +fixed = "1.28.0" +fixed-macro = "1.2.0" + +cortex-m = { version = "0.7.7", features = ["inline-asm"] } +cortex-m-rt = "0.7.5" +panic-probe = { version = "0.3.2", features = ["print-defmt"] } +futures = { version = "0.3.31", default-features = false, features = [ + "async-await", + "cfg-target-has-atomic", +] } +critical-section = "1.2.0" +portable-atomic = { version = "1.5", features = ["critical-section"] } + +embedded-hal-async = "1.0.0" +embedded-hal-bus = { version = "0.2.0", features = ["async"] } +embedded-io-async = { version = "0.6.1", features = ["defmt-03"] } +embedded-storage = "0.3.1" +static_cell = "2.1.0" +log = "0.4" +pio-proc = "0.2.2" +pio = "0.2.1" +rand = { version = "0.8.5", default-features = false } + +mqtt-protocol = { path = "../mqtt-protocol" } +serial-comm = { path = "../serial-comm" } + +[profile.release] +debug = 2 diff --git a/controller/README.md b/controller/README.md new file mode 100644 index 0000000..8abdb86 --- /dev/null +++ b/controller/README.md @@ -0,0 +1,110 @@ +# Pico with Embassy + +Read temperature values and write them to an MQTT topic. + +The Raspberry Pi Pico is well supported by [embassy](https://embassy.dev), apart +from the bluetooth stack (for [legal]() reasons). As we do not use bluetooth for +communication we can ignore that. + +## Flashing + +There are two ways of getting the code onto the microcontroller. + +### elf2uf2 + +Compile the binary as normal, convert it into a uf2 firmware which is flashable +to the pico with only an USB connection. Disadvantage of this apporach is that +it is more annoying to do and does not lend itself to debugging. + +- Clone the [elf2uf2](https://github.com/rej696/elf2uf2) repository and follow + its instructions to compile the tool +- Compile the controller code with `cargo build --release` +- Convert the resulting binary with something like + `elf2uf2 target/thumbv6m-none-eabi/release/controller ./controller.uf2` +- Hold the bootselect button of the pico when plugging it in +- Copy the uf2 file to the mass storage device + +### CMSIS-DAP + +Use the [CMSIS-DAP](https://arm-software.github.io/CMSIS_5/DAP/html/index.html) +protocol for flashing and debugging. + +The +[raspberry pi debug probe](https://www.raspberrypi.com/documentation/microcontrollers/debug-probe.html) +works well but anything implementing that protocol is fine. + +![Debug Probe Setup](./debug-probe-setup.jpg) _Example setup with a Pico WH +(Pico W works as well, just a bit more annoying)_ + +- Setup debug probe +- Install [probe-rs](https://github.com/probe-rs/probe-rs/) +- Run `cargo run` and it should upload and logs be visible + +## Configuration + +Configuration is done by sending commands across a serial connection. Only one +command is implemented for now: Set-config with two parameters. + +A sample set-config command looks like this (not encoded yet): `SC ssid MyNet`. +It consists of three parts: + +- `SC`: Command prefix, always the same. +- `ssid`: Configuration key. +- `MyNet`: Configuration value. + +The following keys are recognized: + +- `ssid`: Name of the network to connect to. +- `ssid_pw`: Password to connect to the network. +- `mqtt`: URL of the MQTT broker (must not use https). +- `client_id`: ID of the device (used for identifaction in MQTT). + +A message needs to be encoded into its byte representation looking on a high +level as follows: + +``` +||||| +``` + +- `Parameters` is an unsigned byte signifying the amount of parameters in the + message. +- `Parameter-Lengths`: Length of each parameter. +- `Prefix` is always two bytes long and encoded as UTF-8 (meaning it is + basically ASCII). +- `Parameters`: every parameter encodes is own length in its first byte, + followed by the actual data. + +Taking the above ssid example this would lead to this (the prefix counts as its +own parameter): + +``` +0x03 0x02 0x04 0x05 0x53 0x43 0x73 0x73 0x69 0x64 0x4D 0x59 0x4E 0x65 0x74 +``` + +Now the COBS encoding is applied before sending it across the wire: + +``` +0x10 0x03 0x02 0x04 0x05 0x53 0x43 0x73 0x73 0x69 0x64 0x4d 0x79 0x4e 0x65 0x74 +``` + +In this case (as in most) it merely adds two additional bytes (the last 0x00 +byte is implied). + +Sending this this command on linux, assuming the serial connection is accessible +on `/dev/ttyACM1`: + +``` +echo -en "\x10\x03\x02\x04\x05\x53\x43\x73\x73\x69\x64\x4d\x79\x4e\x65\x74\x00" > /dev/ttyACM1 +``` + +If listening to the serial connection, command acknowledgements like `OK` or +`ERROR` can be seen. + +In order to decode a message, apply the above steps in reverse order. + +All configuration is read on startup of the microcontroller. + +## Outlook + +- Pull the flash/config handling into separate library for testability +- Merge different ways of running tasks (join and tasks) diff --git a/controller/build.rs b/controller/build.rs new file mode 100644 index 0000000..3f915f9 --- /dev/null +++ b/controller/build.rs @@ -0,0 +1,36 @@ +//! This build script copies the `memory.x` file from the crate root into +//! a directory where the linker can always find it at build time. +//! For many projects this is optional, as the linker always searches the +//! project root directory -- wherever `Cargo.toml` is. However, if you +//! are using a workspace or have a more complicated build setup, this +//! build script becomes required. Additionally, by requesting that +//! Cargo re-run the build script whenever `memory.x` is changed, +//! updating `memory.x` ensures a rebuild of the application with the +//! new memory settings. + +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + // Put `memory.x` in our output directory and ensure it's + // on the linker search path. + let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); + File::create(out.join("memory.x")) + .unwrap() + .write_all(include_bytes!("memory.x")) + .unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + + // By default, Cargo will re-run a build script whenever + // any file in the project changes. By specifying `memory.x` + // here, we ensure the build script is only re-run when + // `memory.x` is changed. + println!("cargo:rerun-if-changed=memory.x"); + + println!("cargo:rustc-link-arg-bins=--nmagic"); + println!("cargo:rustc-link-arg-bins=-Tlink.x"); + println!("cargo:rustc-link-arg-bins=-Tlink-rp.x"); + println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); +} diff --git a/controller/cyw43-firmware/43439A0.bin b/controller/cyw43-firmware/43439A0.bin new file mode 100644 index 0000000..0173752 Binary files /dev/null and b/controller/cyw43-firmware/43439A0.bin differ diff --git a/controller/cyw43-firmware/43439A0_clm.bin b/controller/cyw43-firmware/43439A0_clm.bin new file mode 100644 index 0000000..1fedd75 Binary files /dev/null and b/controller/cyw43-firmware/43439A0_clm.bin differ diff --git a/controller/cyw43-firmware/LICENSE-permissive-binary-license-1.0.txt b/controller/cyw43-firmware/LICENSE-permissive-binary-license-1.0.txt new file mode 100644 index 0000000..cbb51f9 --- /dev/null +++ b/controller/cyw43-firmware/LICENSE-permissive-binary-license-1.0.txt @@ -0,0 +1,49 @@ +Permissive Binary License + +Version 1.0, July 2019 + +Redistribution. Redistribution and use in binary form, without +modification, are permitted provided that the following conditions are +met: + +1) Redistributions must reproduce the above copyright notice and the + following disclaimer in the documentation and/or other materials + provided with the distribution. + +2) Unless to the extent explicitly permitted by law, no reverse + engineering, decompilation, or disassembly of this software is + permitted. + +3) Redistribution as part of a software development kit must include the + accompanying file named �DEPENDENCIES� and any dependencies listed in + that file. + +4) Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +Limited patent license. The copyright holders (and contributors) grant a +worldwide, non-exclusive, no-charge, royalty-free patent license to +make, have made, use, offer to sell, sell, import, and otherwise +transfer this software, where such license applies only to those patent +claims licensable by the copyright holders (and contributors) that are +necessarily infringed by this software. This patent license shall not +apply to any combinations that include this software. No hardware is +licensed hereunder. + +If you institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the software +itself infringes your patent(s), then your rights granted under this +license shall terminate as of the date such litigation is filed. + +DISCLAIMER. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS." ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/controller/cyw43-firmware/README.md b/controller/cyw43-firmware/README.md new file mode 100644 index 0000000..db3d9c9 --- /dev/null +++ b/controller/cyw43-firmware/README.md @@ -0,0 +1,9 @@ +# WiFi firmware + +Firmware obtained from https://github.com/Infineon/wifi-host-driver/tree/master/WiFi_Host_Driver/resources/firmware/COMPONENT_43439 + +Licensed under the [Infineon Permissive Binary License](./LICENSE-permissive-binary-license-1.0.txt) + +## Changelog + +* 2023-07-28: synced with `ad3bad0` - Update 43439 fw from 7.95.55 ot 7.95.62 diff --git a/controller/debug-probe-setup.jpg b/controller/debug-probe-setup.jpg new file mode 100644 index 0000000..681db17 Binary files /dev/null and b/controller/debug-probe-setup.jpg differ diff --git a/controller/memory.x b/controller/memory.x new file mode 100644 index 0000000..7bc60f8 --- /dev/null +++ b/controller/memory.x @@ -0,0 +1,5 @@ +MEMORY { + BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 + FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100 + RAM : ORIGIN = 0x20000000, LENGTH = 256K +} diff --git a/controller/src/byte_handler.rs b/controller/src/byte_handler.rs new file mode 100644 index 0000000..f7b935c --- /dev/null +++ b/controller/src/byte_handler.rs @@ -0,0 +1,78 @@ +use defmt::{error, info}; +use embassy_rp::usb::{Driver, Instance}; +use embassy_usb::{class::cdc_acm::CdcAcmClass, driver::EndpointError}; +use serial_comm::{ + cobs::SENTINEL, + protocol::{cmd::Cobs, error::ErrorCommand, ok::OkCommand}, +}; + +use crate::{ + cmd_handlers::{handle_cmd, PACKET_SIZE}, + flash::Config, + usb_serial::DeviceClass, +}; + +pub struct Disconnected; + +impl From for Disconnected { + fn from(val: EndpointError) -> Self { + match val { + EndpointError::BufferOverflow => panic!("Buffer overflow"), + EndpointError::Disabled => Disconnected {}, + } + } +} + +#[embassy_executor::task] +pub async fn cmd_task(class: &'static mut DeviceClass, config: &'static mut Config<'static>) -> ! { + loop { + class.wait_connection().await; + info!("Connected"); + let _ = handle_bytes(class, config).await; + info!("Disconnected"); + } +} + +/// Handle incoming bytes over the usb-serial connection. +/// +/// Always buffer data until you encounter the `SENTINEL` value. After that try to decode and +/// execute the received command. +pub async fn handle_bytes<'d, T: Instance + 'd>( + class: &mut CdcAcmClass<'d, Driver<'d, T>>, + config: &mut Config<'d>, +) -> Result<(), Disconnected> { + let mut cmd_buffer: [u8; PACKET_SIZE] = [0; PACKET_SIZE]; + let mut cursor: usize = 0; + + loop { + let mut read_buf: [u8; 64] = [0; 64]; + let n = class.read_packet(&mut read_buf).await?; + let data = &read_buf[..n]; + + if n > 0 { + cmd_buffer[cursor..cursor + n].copy_from_slice(data); + cursor += n; + if data[n - 1] == SENTINEL { + match handle_cmd(cmd_buffer, cursor, config) { + Err(e) => { + error!( + "Failed to handle command: {:?} - {:?}", + e, + cmd_buffer[0..cursor] + ); + let err_cmd = ErrorCommand::new("Failed to handle command"); + let (length, err_cmd): (usize, [u8; 128]) = err_cmd.as_cobs(); + class.write_packet(&err_cmd[0..length]).await?; + } + _ => { + let ok_cmd = OkCommand::new(); + let (length, ok_cmd): (usize, [u8; 128]) = ok_cmd.as_cobs(); + class.write_packet(&ok_cmd[0..length]).await?; + } + }; + + cursor = 0; + } + } + } +} diff --git a/controller/src/cmd_handlers/mod.rs b/controller/src/cmd_handlers/mod.rs new file mode 100644 index 0000000..1ed5511 --- /dev/null +++ b/controller/src/cmd_handlers/mod.rs @@ -0,0 +1,72 @@ +//! Handle specific commands, comming in from usb-serial. + +pub mod set_config; + +use defmt::Format; +use serial_comm::{cobs, protocol::set_config::SetConfig}; + +use crate::flash::{Config, FlashError}; + +#[derive(Debug)] +pub struct Utf8Error(pub core::str::Utf8Error); + +impl Format for Utf8Error { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "Utf8Error"); + } +} + +#[derive(Debug, Format)] +pub enum CmdError { + Utf8Error(Utf8Error), + FlashError(FlashError), + NoSuchCmd, +} + +impl From for CmdError { + fn from(err: core::str::Utf8Error) -> Self { + CmdError::Utf8Error(Utf8Error(err)) + } +} + +impl From for CmdError { + fn from(err: FlashError) -> Self { + CmdError::FlashError(err) + } +} + +// arbitrarily constrained, we know we don't need more for now +pub const PACKET_SIZE: usize = 256; + +type CmdHandler = fn([u8; PACKET_SIZE], &mut Config) -> Result<(), CmdError>; +static CMD_HANDLERS: &[(&str, CmdHandler)] = &[(SetConfig::PREFIX, set_config::handle)]; + +/// Try decoding a command buffer and run it. +/// +/// On failure return an ErrorCommand across the serial connection, otherwise an OkCommand. +pub fn handle_cmd( + cmd_buffer: [u8; PACKET_SIZE], + length: usize, + config: &mut Config<'_>, +) -> Result<(), CmdError> { + let (length, decoded_cmd): (usize, [u8; PACKET_SIZE]) = cobs::decode(cmd_buffer, length); + + if length > 1 { + let header_length = (decoded_cmd[0] + 1) as usize; + let cmd_prefix = core::str::from_utf8(&decoded_cmd[header_length..header_length + 2])?; + + let mut idx = 0; + while idx < CMD_HANDLERS.len() && CMD_HANDLERS[idx].0 != cmd_prefix { + idx += 1; + } + + if idx == CMD_HANDLERS.len() { + return Err(CmdError::NoSuchCmd); + } + + let handler = CMD_HANDLERS[idx].1; + handler(decoded_cmd, config)?; + } + + Ok(()) +} diff --git a/controller/src/cmd_handlers/set_config.rs b/controller/src/cmd_handlers/set_config.rs new file mode 100644 index 0000000..07e4ed7 --- /dev/null +++ b/controller/src/cmd_handlers/set_config.rs @@ -0,0 +1,17 @@ +use defmt::info; +use serial_comm::protocol::{cmd::Cmd, set_config::SetConfig}; + +use crate::{cmd_handlers::CmdError, flash::Config}; + +use super::PACKET_SIZE; + +/// Handle a SetConfig command by writing the received key-value pair to flash storage. +pub fn handle(data: [u8; PACKET_SIZE], config: &mut Config<'_>) -> Result<(), CmdError> { + let mut cmd_buffer: [u8; PACKET_SIZE] = [0; PACKET_SIZE]; + + let set_config = SetConfig::from_bytes::(data, &mut cmd_buffer)?; + config.write_config(set_config.key, set_config.value)?; + info!("set {} to {}", set_config.key, set_config.value); + + Ok(()) +} diff --git a/controller/src/flash.rs b/controller/src/flash.rs new file mode 100644 index 0000000..85e4d38 --- /dev/null +++ b/controller/src/flash.rs @@ -0,0 +1,124 @@ +use defmt::{error, Format}; +use embassy_rp::{ + flash::{self, Async, Flash, ERASE_SIZE}, + peripherals::{DMA_CH0, FLASH}, +}; + +use crate::cmd_handlers::Utf8Error; + +const ADDR_OFFSET: u32 = 0x100000; +const FLASH_SIZE: usize = 2 * 1024 * 1024; + +pub const ENTRY_SIZE: u32 = ERASE_SIZE as u32; +const SSID_CONFIG: u32 = ADDR_OFFSET; +const SSID_PW_CONFIG: u32 = ADDR_OFFSET + ENTRY_SIZE; +const MQTT_CONFIG: u32 = ADDR_OFFSET + 2 * ENTRY_SIZE; +const CLIENT_ID_CONFIG: u32 = ADDR_OFFSET + 3 * ENTRY_SIZE; + +pub const SSID_KEY: &str = "ssid"; +pub const SSID_PW_KEY: &str = "ssid_pw"; +pub const MQTT_KEY: &str = "mqtt"; +pub const CLIENT_ID_KEY: &str = "client_id"; + +#[derive(Debug, Format)] +pub enum FlashError { + Internal(flash::Error), + Utf8Error(Utf8Error), + NoSuchKey, + ValueTooLong, + NoValue, +} + +impl From for FlashError { + fn from(err: flash::Error) -> Self { + FlashError::Internal(err) + } +} + +impl From for FlashError { + fn from(err: core::str::Utf8Error) -> Self { + FlashError::Utf8Error(Utf8Error(err)) + } +} + +/// Write key-value data to flash storage for persistence and read it from there again. +pub struct Config<'a> { + flash: Flash<'a, FLASH, Async, FLASH_SIZE>, +} + +impl<'a> Config<'a> { + /// Initialize the flash module on the RP2040. + pub fn init(flash: FLASH, dma_ch0: DMA_CH0) -> Self { + let flash = embassy_rp::flash::Flash::<_, Async, FLASH_SIZE>::new(flash, dma_ch0); + + Self { flash } + } + + /// Get the position on flash for a configuration key. + fn get_config_cursor(&self, key: &str) -> Option { + match key { + SSID_KEY => Some(SSID_CONFIG), + SSID_PW_KEY => Some(SSID_PW_CONFIG), + MQTT_KEY => Some(MQTT_CONFIG), + CLIENT_ID_KEY => Some(CLIENT_ID_CONFIG), + _ => None, + } + } + + /// Write a key-value pair to flash storage. + /// + /// The key must be one of the predefined ones and the value length can not be longer than + /// `ENTRY_SIZE`. + /// + /// Before writing, the whole sector must be erased, otherwise garbage writes can occur. For + /// that reason `ENTRY_SIZE` is set to the ERASE_SIZE of the RP2040 (4096 bits), otherwise we + /// would get misaligned erasures. + /// + /// Each key-vaue pair is encoded as [,], value_length being one byte + /// long. + pub fn write_config(&mut self, key: &str, val: &str) -> Result<(), FlashError> { + let entry_cursor = self.get_config_cursor(key).ok_or(FlashError::NoSuchKey)?; + let val_data = val.as_bytes(); + + if val_data.len() > ENTRY_SIZE as usize { + error!( + "data of length {} for {} is longer than the max entry size of {}", + val_data.len(), + key, + ENTRY_SIZE + ); + return Err(FlashError::ValueTooLong); + } + + self.flash + .blocking_erase(entry_cursor, entry_cursor + ERASE_SIZE as u32)?; + self.flash + .blocking_write(entry_cursor, &[val_data.len() as u8])?; // value length + self.flash.blocking_write(entry_cursor + 1, val_data)?; + + Ok(()) + } + + /// Read the value of a key from flash storage. + pub fn read_config<'b>( + &mut self, + key: &str, + buffer: &'b mut [u8; ENTRY_SIZE as usize], + ) -> Result<&'b str, FlashError> { + let entry_cursor = self.get_config_cursor(key).ok_or(FlashError::NoSuchKey)?; + + let mut length_buffer: [u8; 1] = [0; 1]; + self.flash.blocking_read(entry_cursor, &mut length_buffer)?; + let length = length_buffer[0] as usize; + + if length < 1 || length > ENTRY_SIZE as usize { + error!("there is no valid value for {}", key); + return Err(FlashError::NoValue); + } + + self.flash.blocking_read(entry_cursor + 1, buffer)?; + let val = core::str::from_utf8(&buffer[0..length])?; + + Ok(val) + } +} diff --git a/controller/src/main.rs b/controller/src/main.rs new file mode 100644 index 0000000..348d6fc --- /dev/null +++ b/controller/src/main.rs @@ -0,0 +1,101 @@ +//! Read the temperature at regular intervals and publish that data via MQTT: +//! Configure the microcontroller with commands sent across an usb-serial connection. + +#![no_std] +#![no_main] + +use core::str::FromStr; + +use byte_handler::cmd_task; +use cyw43::Control; +use defmt::{error, info}; +use embassy_executor::Spawner; +use embassy_net::Stack; +use embassy_time::Timer; +use flash::{Config, FlashError}; +use static_cell::StaticCell; +use temperature::{temperature_task, Temperature}; +use usb_serial::{init_usb, usb_task}; +use wifi::connect; +use {defmt_rtt as _, panic_probe as _}; + +use crate::{mqtt::BUFFER_SIZE, wifi::init_wifi}; + +mod byte_handler; +mod cmd_handlers; +mod flash; +mod mqtt; +mod temperature; +mod usb_serial; +mod wifi; + +async fn connect_wifi<'a>( + config: &mut Config<'static>, + wifi: &mut Control<'a>, + net_stack: &'a Stack>, +) -> Result<(), FlashError> { + let mut ssid_buffer = [0; flash::ENTRY_SIZE as usize]; + let ssid = config.read_config(flash::SSID_KEY, &mut ssid_buffer)?; + let mut ssid_pw_buffer = [0; flash::ENTRY_SIZE as usize]; + let ssid_pw = config.read_config(flash::SSID_PW_KEY, &mut ssid_pw_buffer)?; + + if !ssid.is_empty() { + connect(wifi, net_stack, ssid, ssid_pw).await; + } + Ok(()) +} + +static CONFIG: StaticCell = StaticCell::new(); + +static RX_BUFFER: StaticCell<[u8; BUFFER_SIZE]> = StaticCell::new(); +static TX_BUFFER: StaticCell<[u8; BUFFER_SIZE]> = StaticCell::new(); + +static CLIENT_ID_BUFFER: StaticCell<[u8; flash::ENTRY_SIZE as usize]> = StaticCell::new(); +static MQTT_IP_BUFFER: StaticCell<[u8; flash::ENTRY_SIZE as usize]> = StaticCell::new(); + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + let p = embassy_rp::init(Default::default()); + + Timer::after_millis(100).await; + + let config = CONFIG.init(Config::init(p.FLASH, p.DMA_CH0)); + + let (mut wifi, net_stack) = init_wifi( + spawner, p.PIN_23, p.PIN_24, p.PIN_25, p.PIN_29, p.PIO0, p.DMA_CH1, + ) + .await; + match connect_wifi(config, &mut wifi, net_stack).await { + Ok(_) => info!("Connected to WiFi."), + Err(_) => error!("Failed to connect to WiFi"), + } + + let rx_buffer = RX_BUFFER.init([0u8; BUFFER_SIZE]); + let tx_buffer = TX_BUFFER.init([0u8; BUFFER_SIZE]); + + let client_id_buffer = CLIENT_ID_BUFFER.init([0u8; flash::ENTRY_SIZE as usize]); + let client_id = config + .read_config(flash::CLIENT_ID_KEY, client_id_buffer) + .unwrap_or("tomato"); + + let mqtt_ip_buffer = MQTT_IP_BUFFER.init([0u8; flash::ENTRY_SIZE as usize]); + let mqtt_ip = config + .read_config(flash::MQTT_KEY, mqtt_ip_buffer) + .unwrap_or("127.0.0.1"); + + let mqtt = mqtt::Client::new(client_id, net_stack, rx_buffer, tx_buffer); + let host_addr = embassy_net::IpAddress::from_str(mqtt_ip) + .unwrap_or(embassy_net::IpAddress::v4(127, 0, 1, 1)); + let temps = Temperature::new(p.ADC, p.ADC_TEMP_SENSOR); + + let (usb, class) = init_usb(p.USB); + spawner + .spawn(usb_task(usb)) + .expect("failed to run usb task"); + spawner + .spawn(cmd_task(class, config)) + .expect("failed initializing usb command handler"); + spawner + .spawn(temperature_task(client_id, host_addr, mqtt, temps)) + .expect("failed starting temperature task"); +} diff --git a/controller/src/mqtt.rs b/controller/src/mqtt.rs new file mode 100644 index 0000000..42f6dba --- /dev/null +++ b/controller/src/mqtt.rs @@ -0,0 +1,73 @@ +use defmt::Format; +use embassy_net::{ + tcp::{ConnectError, Error, TcpSocket}, + IpAddress, Stack, +}; +use embassy_time::Duration; +use mqtt_protocol::{connect::Connect, publish::Publish}; + +pub const BUFFER_SIZE: usize = 4096; +const TCP_TIMEOUT_SECS: u64 = 5; + +#[derive(Debug, Format)] +pub enum MqttError { + TcpError(Error), + ConnectError(ConnectError), +} + +impl From for MqttError { + fn from(err: ConnectError) -> Self { + MqttError::ConnectError(err) + } +} + +impl From for MqttError { + fn from(err: Error) -> Self { + MqttError::TcpError(err) + } +} + +/// Very simple MQTT client, capable of sending simple publish packets over unencrypted TCP/IP. +pub struct Client<'a> { + pub client_id: &'a str, + socket: TcpSocket<'a>, +} + +impl<'a> Client<'a> { + /// Initialize the socket for sending MQTT data and set the socket timeout to `TCP_TIMEOUT_SECS`. + /// + /// - `client_id`: Identification of this client. + /// - `net_stack`: network stack for sending data. + /// - `rx_buffer`: byte buffer for receiving data. + /// - `tx_buffer`: byte buffer for sending data. + pub fn new( + client_id: &'a str, + net_stack: &'a Stack>, + rx_buffer: &'static mut [u8; BUFFER_SIZE], + tx_buffer: &'static mut [u8; BUFFER_SIZE], + ) -> Self { + let mut socket = embassy_net::tcp::TcpSocket::new(net_stack, rx_buffer, tx_buffer); + socket.set_timeout(Some(Duration::from_secs(TCP_TIMEOUT_SECS))); + + Self { client_id, socket } + } + + //// Publish the payload `payload` to the MQTT topic `topic` on the broker running at `address`:1883. + pub async fn publish( + &mut self, + address: IpAddress, + topic: &str, + payload: &[u8], + ) -> Result<(), MqttError> { + self.socket.connect((address, 1883)).await?; + let connect = Connect::new(self.client_id); + self.socket.write(&connect.data[0..connect.length]).await?; + + let publish = Publish::new(topic, payload); + self.socket.write(&publish.data[0..publish.length]).await?; + + self.socket.close(); + + Ok(()) + } +} diff --git a/controller/src/temperature.rs b/controller/src/temperature.rs new file mode 100644 index 0000000..ba37862 --- /dev/null +++ b/controller/src/temperature.rs @@ -0,0 +1,86 @@ +use defmt::{error, info}; +use embassy_net::IpAddress; +use embassy_rp::{ + adc::{Adc, Async, Channel, Config, Error, InterruptHandler}, + bind_interrupts, + peripherals::{ADC, ADC_TEMP_SENSOR}, +}; +use embassy_time::Timer; +use mqtt_protocol::publish::MAX_TOPIC_LENGTH; + +use crate::mqtt; + +bind_interrupts!(struct Irqs { + ADC_IRQ_FIFO => InterruptHandler; +}); + +#[embassy_executor::task] +pub async fn temperature_task( + client_id: &'static str, + host_addr: IpAddress, + mut mqtt: mqtt::Client<'static>, + mut temps: Temperature<'static>, +) -> ! { + const TOPIC_PREFIX: &str = "temps/"; + let mut topic_buffer = [0u8; MAX_TOPIC_LENGTH - TOPIC_PREFIX.len()]; + topic_buffer[..TOPIC_PREFIX.len()].copy_from_slice(TOPIC_PREFIX.as_bytes()); + + let client_id_bytes = client_id.as_bytes(); + topic_buffer[TOPIC_PREFIX.len()..TOPIC_PREFIX.len() + client_id_bytes.len()] + .copy_from_slice(client_id_bytes); + + let topic_full = + core::str::from_utf8(&topic_buffer[..TOPIC_PREFIX.len() + client_id_bytes.len()]) + .unwrap_or("temps/tomato"); + + loop { + match temps.read().await { + Ok(temp) => { + info!("publishing temperature data..."); + if let Err(e) = mqtt + .publish(host_addr, topic_full, &temp.to_le_bytes()) + .await + { + error!("failed to publish temperature data: {}", e); + } + } + Err(e) => error!("failed to read temperature: {}", e), + } + + Timer::after_secs(10).await; + } +} + +/// Convert the raw temperature sensor reading into degrees celsius. +/// +/// Taken from the [embassy examples](https://github.com/embassy-rs/embassy/blob/b6fc682117a41e8e63a9632e06da5a17f46d9ab0/examples/rp/src/bin/adc.rs#L43). +fn convert_to_celsius(raw_temp: u16) -> f32 { + // According to chapter 4.9.5. Temperature Sensor in RP2040 datasheet + let temp = 27.0 - (raw_temp as f32 * 3.3 / 4096.0 - 0.706) / 0.001721; + let sign = if temp < 0.0 { -1.0 } else { 1.0 }; + let rounded_temp_x10: i16 = ((temp * 10.0) + 0.5 * sign) as i16; + (rounded_temp_x10 as f32) / 10.0 +} + +pub struct Temperature<'a> { + adc: Adc<'a, Async>, + ts: Channel<'a>, +} + +impl<'a> Temperature<'a> { + /// Initialize the temperature sensor. + pub fn new(adc_pin: ADC, adc_temp_pin: ADC_TEMP_SENSOR) -> Self { + let adc = Adc::new(adc_pin, Irqs, Config::default()); + let ts = Channel::new_temp_sensor(adc_temp_pin); + + Self { adc, ts } + } + + /// Read from the temperature sensor and return the value in degree celsius. + pub async fn read(&mut self) -> Result { + let temp = self.adc.read(&mut self.ts).await?; + let temp = convert_to_celsius(temp); + + Ok(temp) + } +} diff --git a/controller/src/usb_serial.rs b/controller/src/usb_serial.rs new file mode 100644 index 0000000..46eaf98 --- /dev/null +++ b/controller/src/usb_serial.rs @@ -0,0 +1,74 @@ +use embassy_rp::{ + bind_interrupts, + peripherals::USB, + usb::{Driver, InterruptHandler}, +}; +use embassy_usb::{ + class::cdc_acm::{CdcAcmClass, State}, + Builder, Config, UsbDevice, +}; +use static_cell::StaticCell; + +bind_interrupts!(struct Irqs { + USBCTRL_IRQ => InterruptHandler; +}); + +pub type SerialDevice = UsbDevice<'static, Driver<'static, USB>>; +pub type DeviceClass = CdcAcmClass<'static, Driver<'static, USB>>; + +const DESCRIPTOR_SIZE: usize = 256; +type Descriptor = [u8; DESCRIPTOR_SIZE]; + +const BUFFER_SIZE: usize = 64; +type ControlBuffer = [u8; BUFFER_SIZE]; + +static DEVICE_DESCRIPTOR: StaticCell = StaticCell::new(); +static CONFIG_DESCRIPTOR: StaticCell = StaticCell::new(); +static BOS_DESCRIPTOR: StaticCell = StaticCell::new(); +static CONTROL_BUFFER: StaticCell = StaticCell::new(); +static STATE: StaticCell = StaticCell::new(); +static CLASS: StaticCell = StaticCell::new(); + +#[embassy_executor::task] +pub async fn usb_task(mut usb: SerialDevice) -> ! { + usb.run().await +} + +/// Initialize the USB serial interface and return the device and CdcAcm class (Abstract Control Model). +pub fn init_usb(usb: USB) -> (SerialDevice, &'static mut DeviceClass) { + let driver = Driver::new(usb, Irqs); + + let mut config = Config::new(0xc0de, 0xcafe); + config.manufacturer = Some("Embassy"); + config.product = Some("USB-serial"); + config.serial_number = Some("123456789"); + config.max_power = 100; + config.max_packet_size_0 = 64; + + // Required for windows 7 compatibility. + // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/1.9.1/kconfig/CONFIG_CDC_ACM_IAD.html#help + config.device_class = 0xEF; + config.device_sub_class = 0x02; + config.device_protocol = 0x01; + config.composite_with_iads = true; + + let device_descriptor = DEVICE_DESCRIPTOR.init([0u8; DESCRIPTOR_SIZE]); + let config_descriptor = CONFIG_DESCRIPTOR.init([0u8; DESCRIPTOR_SIZE]); + let bos_descriptor = BOS_DESCRIPTOR.init([0u8; DESCRIPTOR_SIZE]); + let control_buf = CONTROL_BUFFER.init([0u8; BUFFER_SIZE]); + + let state = STATE.init(State::new()); + let mut builder = Builder::new( + driver, + config, + device_descriptor, + config_descriptor, + bos_descriptor, + control_buf, + ); + + let class = CLASS.init(CdcAcmClass::new(&mut builder, state, 64)); + let usb = builder.build(); + + (usb, &mut *class) +} diff --git a/controller/src/wifi.rs b/controller/src/wifi.rs new file mode 100644 index 0000000..7a34580 --- /dev/null +++ b/controller/src/wifi.rs @@ -0,0 +1,128 @@ +use cyw43::{Control, ControlError, State}; +use cyw43_pio::PioSpi; +use defmt::{error, info, unwrap}; +use embassy_executor::Spawner; +use embassy_net::{Config, Stack, StackResources}; +use embassy_rp::{ + bind_interrupts, + clocks::RoscRng, + gpio::{Level, Output}, + peripherals::{DMA_CH1, PIN_23, PIN_24, PIN_25, PIN_29, PIO0}, + pio::{InterruptHandler, Pio}, +}; +use embassy_time::Timer; +use rand::RngCore; +use static_cell::StaticCell; + +const MAX_WIFI_TRIES: u8 = 3; +const MAX_DHCP_TRIES: u8 = 20; + +bind_interrupts!(struct Irqs { + PIO0_IRQ_0 => InterruptHandler; +}); + +#[embassy_executor::task] +async fn wifi_task( + runner: cyw43::Runner<'static, Output<'static>, PioSpi<'static, PIO0, 0, DMA_CH1>>, +) -> ! { + runner.run().await +} + +#[embassy_executor::task] +async fn net_task(stack: &'static Stack>) -> ! { + stack.run().await +} + +static STATE: StaticCell = StaticCell::new(); +static STACK: StaticCell>> = StaticCell::new(); +const STACK_RESOURCES_COUNT: usize = 2; +static STACK_RESOURCES: StaticCell> = StaticCell::new(); + +// Initialise the wwifi chip of the RP2040 and return the control interface as well as the network +// stack. +pub async fn init_wifi( + spawner: Spawner, + pin_23: PIN_23, + pin_24: PIN_24, + pin_25: PIN_25, + pin_29: PIN_29, + pio0: PIO0, + dma_ch1: DMA_CH1, +) -> (Control<'static>, &'static Stack>) { + let fw = include_bytes!("../cyw43-firmware/43439A0.bin"); + let clm = include_bytes!("../cyw43-firmware/43439A0_clm.bin"); + + let pwr = Output::new(pin_23, Level::Low); + let cs = Output::new(pin_25, Level::High); + let mut pio = Pio::new(pio0, Irqs); + let spi = PioSpi::new( + &mut pio.common, + pio.sm0, + pio.irq0, + cs, + pin_24, + pin_29, + dma_ch1, + ); + + let state = STATE.init(State::new()); + let (net_device, mut control, runner) = cyw43::new(state, pwr, spi, fw).await; + + unwrap!(spawner.spawn(wifi_task(runner))); + + control.init(clm).await; + control + .set_power_management(cyw43::PowerManagementMode::PowerSave) + .await; + + let config = Config::dhcpv4(Default::default()); + + let mut rng = RoscRng; + let seed = rng.next_u64(); + + // Init network stack + let resources = STACK_RESOURCES.init(StackResources::::new()); + let stack = &*STACK.init(Stack::new(net_device, config, resources, seed)); + + unwrap!(spawner.spawn(net_task(stack))); + + (control, stack) +} + +/// Connect to a WiFi network using WPA2 and try to get an IP from DHCP (IPv4). +pub async fn connect<'a>( + wifi: &mut Control<'a>, + net_stack: &Stack>, + ssid: &str, + pw: &str, +) { + info!("trying to connect to {}...", ssid); + + let mut idx = 0; + let mut status: Result<(), ControlError> = Err(ControlError { status: 0 }); + + while idx < MAX_WIFI_TRIES && status.is_err() { + status = wifi.join_wpa2(ssid, pw).await; + Timer::after_millis(100).await; + + idx += 1; + } + + if let Err(e) = status { + error!("WiFi connection failed: {}", e.status); + } else { + info!("waiting for DHCP..."); + + idx = 0; + while idx < MAX_DHCP_TRIES && !net_stack.is_config_up() { + Timer::after_millis(1000).await; + idx += 1; + } + + if net_stack.is_config_up() { + info!("DHCP is up!"); + } else { + error!("Failed to get an IP from DHCP"); + } + } +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..06f1ef6 --- /dev/null +++ b/flake.lock @@ -0,0 +1,114 @@ +{ + "nodes": { + "fenix": { + "inputs": { + "nixpkgs": "nixpkgs", + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1731738660, + "narHash": "sha256-tIXhc9lX1b030v812yVJanSR37OnpTb/OY5rU3TbShA=", + "owner": "nix-community", + "repo": "fenix", + "rev": "e10ba121773f754a30d31b6163919a3e404a434f", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1731319897, + "narHash": "sha256-PbABj4tnbWFMfBp6OcUK5iGy1QY+/Z96ZcLpooIbuEI=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "dc460ec76cbff0e66e269457d7b728432263166c", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1731319897, + "narHash": "sha256-PbABj4tnbWFMfBp6OcUK5iGy1QY+/Z96ZcLpooIbuEI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "dc460ec76cbff0e66e269457d7b728432263166c", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "fenix": "fenix", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs_2" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1731693936, + "narHash": "sha256-uHUUS1WPyW6ohp5Bt3dAZczUlQ22vOn7YZF8vaPKIEw=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "1b90e979aeee8d1db7fe14603a00834052505497", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..2f5ce10 --- /dev/null +++ b/flake.nix @@ -0,0 +1,37 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + fenix.url = "github:nix-community/fenix"; + }; + + outputs = + { + nixpkgs, + fenix, + flake-utils, + ... + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = import nixpkgs { inherit system; }; + fx = fenix.packages.${system}; + rust = fx.combine [ + fx.stable.toolchain + fx.targets.thumbv6m-none-eabi.stable.rust-std + ]; + buildInputs = [ + rust + pkgs.cargo-deny + pkgs.rust-analyzer + pkgs.probe-rs + ]; + in + { + devShells.default = pkgs.mkShell { + buildInputs = buildInputs; + }; + } + ); +} diff --git a/mqtt-protocol/Cargo.toml b/mqtt-protocol/Cargo.toml new file mode 100644 index 0000000..65a0ac1 --- /dev/null +++ b/mqtt-protocol/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "mqtt-protocol" +version.workspace = true +edition.workspace = true +description.workspace = true +license.workspace = true + +[dependencies] diff --git a/mqtt-protocol/src/connect.rs b/mqtt-protocol/src/connect.rs new file mode 100644 index 0000000..0330ef9 --- /dev/null +++ b/mqtt-protocol/src/connect.rs @@ -0,0 +1,128 @@ +use crate::fixed_header::FixedHeader; + +// [MQTT 5.0: 3.1.3.1](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc385349242) +const ALLOWED_CLIENT_ID_CHARS: &str = + "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; +// [MQTT 5.0: 3.1.3.1](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc385349242) +const MAX_CLIENT_ID_SIZE: usize = 23; + +// [MQTT 5.0: 3.1.1](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_CONNECT_Fixed_Header) +const FIXED_CONNECT_TYPE: u8 = 0x01; +// [MQTT 5.0: 3.1.1](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_CONNECT_Fixed_Header) +const FIXED_CONNECT_FLAGS: u8 = 0x00; + +// "MQTT" [MQTT 5.0: 3.1.2.1](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc385349225) +const PROTOCOL_NAME: [u8; 6] = [0x00, 0x04, 0x4d, 0x51, 0x54, 0x54]; +// "version 5" [MQTT 5.0: 3.1.2.2](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc385349227) +const PROTOCOL_VERSION: [u8; 1] = [0x05]; +// no user name, no will qos, no will, no clean start [MQTT 5.0: 3.1.2.3 - 3.1.2.9](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc385349229) +const CONNECT_FLAGS: [u8; 1] = [0x00]; +// turn off keep alive mechanism [MQTT 5.0: 3.1.2.10](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Keep_Alive_1) +const KEEP_ALIVE: [u8; 2] = [0x00, 0x00]; +// no properties [MQTT 5.0: 3.1.2.11](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc511988523) +const PROPERTIES: [u8; 1] = [0x00]; + +// calculate position in the data array (those that are constant) +const PROTOCOL_NAME_IDX: usize = 0; +const PROTOCOL_VERSION_IDX: usize = PROTOCOL_NAME_IDX + PROTOCOL_NAME.len(); +const CONNECT_FLAGS_IDX: usize = PROTOCOL_VERSION_IDX + PROTOCOL_VERSION.len(); +const KEEP_ALIVE_IDX: usize = CONNECT_FLAGS_IDX + CONNECT_FLAGS.len(); +const PROPERTIES_IDX: usize = KEEP_ALIVE_IDX + KEEP_ALIVE.len(); +const PAYLOAD_IDX: usize = PROPERTIES_IDX + PROPERTIES.len(); + +// max size the variable header + payload of a connect packet +const MAX_PACKET_LENGTH: usize = PROTOCOL_NAME.len() + + PROTOCOL_VERSION.len() + + CONNECT_FLAGS.len() + + KEEP_ALIVE.len() + + PROPERTIES.len() + + MAX_CLIENT_ID_SIZE + + 2; // two bytes for the client id length + +// max size a whole connect packet can be +const MAX_CONNECT_LENGTH: usize = MAX_PACKET_LENGTH + 5; // 5 is the max length of the fixed header + +pub struct Connect { + // byte representation of the connect packet + pub data: [u8; MAX_CONNECT_LENGTH], + // actual length of the whole connect packet + pub length: usize, +} + +impl Connect { + /// Create an MQTT connect packet. + /// + /// See [MQTT 5.0: 3.1](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_CONNECT_%E2%80%93_Connection) + /// for the byte structure. + /// + /// `client_id` is how the client identifies itself. According to + /// [MQTT 5.0: 3.1.3.1]((https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc385349242) + /// this is not strictly necessary but a server does not have to accept this. + /// + /// The same holds for the limitation of 23 bytes for the `client_id`. + pub fn new(client_id: &str) -> Self { + assert!(client_id + .chars() + .all(|x| ALLOWED_CLIENT_ID_CHARS.contains(x))); + + let client_id_data = client_id.as_bytes(); + assert!(!client_id_data.is_empty() && client_id_data.len() <= MAX_CLIENT_ID_SIZE); + + let client_id_length = client_id_data.len() as u16; + let high_byte = ((client_id_length & 0xFF00) >> 8) as u8; + let low_byte = (client_id_length & 0x00FF) as u8; + + let mut payload_data = [0u8; MAX_CLIENT_ID_SIZE + 2]; + payload_data[0] = high_byte; + payload_data[1] = low_byte; + payload_data[2..2 + client_id_data.len()].copy_from_slice(client_id_data); + + let packet_length = MAX_PACKET_LENGTH - MAX_CLIENT_ID_SIZE + client_id_data.len(); + let fixed_header = FixedHeader::new( + FIXED_CONNECT_TYPE, + FIXED_CONNECT_FLAGS, + packet_length as u32, + ); + + let mut data = [0u8; MAX_CONNECT_LENGTH]; + data[0] = fixed_header.type_flags; + data[1..1 + fixed_header.length] + .copy_from_slice(&fixed_header.remaining_length[0..fixed_header.length]); + let fixed_offset = fixed_header.length + 1; + + data[fixed_offset + PROTOCOL_NAME_IDX..fixed_offset + PROTOCOL_VERSION_IDX] + .copy_from_slice(&PROTOCOL_NAME); + data[fixed_offset + PROTOCOL_VERSION_IDX..fixed_offset + CONNECT_FLAGS_IDX] + .copy_from_slice(&PROTOCOL_VERSION); + data[fixed_offset + CONNECT_FLAGS_IDX..fixed_offset + KEEP_ALIVE_IDX] + .copy_from_slice(&CONNECT_FLAGS); + data[fixed_offset + KEEP_ALIVE_IDX..fixed_offset + PROPERTIES_IDX] + .copy_from_slice(&KEEP_ALIVE); + data[fixed_offset + PROPERTIES_IDX..fixed_offset + PAYLOAD_IDX] + .copy_from_slice(&PROPERTIES); + data[fixed_offset + PAYLOAD_IDX..fixed_offset + PAYLOAD_IDX + client_id_data.len() + 2] + .copy_from_slice(&payload_data[0..client_id_data.len() + 2]); + + Self { + data, + length: packet_length + fixed_header.length + 1, + } + } +} + +#[cfg(test)] +mod tests { + use super::Connect; + + #[test] + fn test_encode_decode_length_1_byte() { + let connect = Connect::new("Tomato"); + assert_eq!( + connect.data[0..connect.length], + [ + 0x10, 0x13, 0x00, 0x04, 0x4d, 0x51, 0x54, 0x54, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x06, 0x54, 0x6f, 0x6d, 0x61, 0x74, 0x6f + ] + ) + } +} diff --git a/mqtt-protocol/src/fixed_header.rs b/mqtt-protocol/src/fixed_header.rs new file mode 100644 index 0000000..733a156 --- /dev/null +++ b/mqtt-protocol/src/fixed_header.rs @@ -0,0 +1,28 @@ +use super::variable_length::encode_length; + +pub struct FixedHeader { + pub type_flags: u8, + // length of variable header + payload, encoded as a variable byte integer + // ([MQTT 5.0: 1.5.5](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc473619950)) + pub remaining_length: [u8; 4], + // how many bytes in the remaining length field are relevant (left to right) + pub length: usize, +} + +impl FixedHeader { + /// Create a fixed header ([MQTT 5.0: 2.1.1](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc511988498)) for an MQTT packet. + /// + /// - `packet_type`: [MQTT 5.0: 2.1.2](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc353481061) + /// - `flags`: [MQTT 5.0: 2.1.3](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc353481062) + /// - `remaining_length`: Length of the variable header + payload. + pub fn new(packet_type: u8, flags: u8, remaining_length: u32) -> Self { + let mut remaining_length_data = [0u8; 4]; + let length = encode_length(remaining_length, &mut remaining_length_data); + + Self { + type_flags: packet_type << 4 | (flags & 0x0F), + remaining_length: remaining_length_data, + length: length.into(), + } + } +} diff --git a/mqtt-protocol/src/lib.rs b/mqtt-protocol/src/lib.rs new file mode 100644 index 0000000..1069309 --- /dev/null +++ b/mqtt-protocol/src/lib.rs @@ -0,0 +1,10 @@ +//! Implement just enough of the MQTT protocol to be able to publish data with +//! QOS 0. +//! All formats are taken from [MQTT Version 5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html). + +#![cfg_attr(not(test), no_std)] + +pub mod connect; +pub mod fixed_header; +pub mod publish; +pub mod variable_length; diff --git a/mqtt-protocol/src/publish.rs b/mqtt-protocol/src/publish.rs new file mode 100644 index 0000000..3b33dec --- /dev/null +++ b/mqtt-protocol/src/publish.rs @@ -0,0 +1,80 @@ +use crate::fixed_header::FixedHeader; + +// [MQTT 5.0: 3.3.1](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc359155681) +const FIXED_PUBLISH_TYPE: u8 = 0x03; +// no flags set [MQTT 5.0: 3.3.1](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc359155681) +const FIXED_PUBLISH_FLAGS: u8 = 0x00; + +// no properties [MQTT 5.0: 3.3.2.3](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc511988586) +const PROPERTIES: [u8; 1] = [0x00]; + +// technically this is 65535 (see https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_UTF-8_Encoded_String) +// but we do not need that here +pub const MAX_TOPIC_LENGTH: usize = 254; + +// this is again arbitrarilly constricted, technically it could be as long as the variable length +// encoding allows (minus fixed and variable header) +const MAX_PAYLOAD_LENGTH: usize = 128; +// 7 is 5 + 2 which is max length of the fixed header and the two bytes used to encode the topic +// length +const MAX_PUBLISH_LENGTH: usize = MAX_TOPIC_LENGTH + 7 + MAX_PAYLOAD_LENGTH; + +/// Encode a string to an MQTT length encoded string +/// [MQTT 5.0: 1.5.4](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc462729066) +fn encode_mqtt_str(topic: &str, encode_buffer: &mut [u8; MAX_TOPIC_LENGTH]) -> usize { + let topic_data = topic.as_bytes(); + assert!(topic_data.len() <= MAX_TOPIC_LENGTH); + + encode_buffer[0] = 0; // no need for the high bits as our max topic length is too small + encode_buffer[1] = topic_data.len() as u8; + encode_buffer[2..2 + topic_data.len()].copy_from_slice(topic_data); + + topic_data.len() + 2 +} + +pub struct Publish { + // byte representation of the publish packet + pub data: [u8; MAX_PUBLISH_LENGTH], + // actual length of the whole publish packet + pub length: usize, +} + +impl Publish { + /// Create an MQTT publish packet. + /// + /// See [MQTT 5.0: 3.3](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc384800410) + /// for the byte structure. + /// + /// `topic`: defines the topic to publish to + /// `payload` value to publish as is + pub fn new(topic: &str, payload: &[u8]) -> Self { + let mut topic_data = [0u8; MAX_TOPIC_LENGTH]; + let topic_length = encode_mqtt_str(topic, &mut topic_data); + + let packet_length = PROPERTIES.len() + topic_length + 2 + payload.len(); + let fixed_header = FixedHeader::new( + FIXED_PUBLISH_TYPE, + FIXED_PUBLISH_FLAGS, + packet_length as u32, + ); + + let mut data = [0u8; MAX_PUBLISH_LENGTH]; + data[0] = fixed_header.type_flags; + data[1..1 + fixed_header.length] + .copy_from_slice(&fixed_header.remaining_length[0..fixed_header.length]); + let fixed_offset = fixed_header.length + 1; + + let topic_idx = fixed_offset; + let properties_idx = topic_idx + topic_length; + let payload_idx = properties_idx + PROPERTIES.len(); + + data[topic_idx..properties_idx].copy_from_slice(&topic_data[0..topic_length]); + data[properties_idx..payload_idx].copy_from_slice(&PROPERTIES); + data[payload_idx..payload_idx + payload.len()].copy_from_slice(payload); + + Self { + data, + length: packet_length + fixed_header.length + 1, + } + } +} diff --git a/mqtt-protocol/src/variable_length.rs b/mqtt-protocol/src/variable_length.rs new file mode 100644 index 0000000..806e31d --- /dev/null +++ b/mqtt-protocol/src/variable_length.rs @@ -0,0 +1,94 @@ +/// Encode an integer as a variable byte integer +/// [MQTT 5.0: 1.5.5](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc473619950) +pub fn encode_length(length: u32, encoded_length: &mut [u8; 4]) -> u8 { + let mut length = length; + + let mut idx = 0; + while idx < encoded_length.len() && length > 0 { + let mut encoded_byte = (length % 128) as u8; + length /= 128; + + if length > 0 { + encoded_byte |= 0x80; // Set the highest bit + } + encoded_length[idx] = encoded_byte; + + idx += 1; + } + + idx as u8 +} + +/// Decode a variable byte integer as an integer. +/// [MQTT 5.0: 1.5.5](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc473619950) +pub fn decode_length(bytes: [u8; 4]) -> u32 { + let mut length = 0u32; + let mut multiplier = 1u32; + + let mut idx = 0; + let mut byte = bytes[idx]; + + while idx < bytes.len() && byte & 0x80 != 0 { + length += ((byte & 0x7F) as u32) * multiplier; + multiplier *= 128; + + idx += 1; + byte = bytes[idx] + } + length += ((byte & 0x7F) as u32) * multiplier; + + length +} + +#[cfg(test)] +mod tests { + use crate::variable_length::{decode_length, encode_length}; + + #[test] + fn test_encode_decode_length_1_byte() { + let mut length = [0u8; 4]; + let byte_length = encode_length(16, &mut length); + + assert_eq!(byte_length, 1); + assert_eq!(length, [0x10, 0x00, 0x00, 0x00]); + + let length = decode_length(length); + assert_eq!(length, 16); + } + + #[test] + fn test_encode_decode_length_2_bytes() { + let mut length = [0u8; 4]; + let byte_length = encode_length(568, &mut length); + + assert_eq!(byte_length, 2); + assert_eq!(length, [0xb8, 0x04, 0x00, 0x00]); + + let length = decode_length(length); + assert_eq!(length, 568); + } + + #[test] + fn test_encode_decode_length_3_bytes() { + let mut length = [0u8; 4]; + let byte_length = encode_length(85734, &mut length); + + assert_eq!(byte_length, 3); + assert_eq!(length, [0xe6, 0x9d, 0x05, 0x00]); + + let length = decode_length(length); + assert_eq!(length, 85734); + } + + #[test] + fn test_encode_length_4_bytes() { + let mut length = [0u8; 4]; + let byte_length = encode_length(8573471, &mut length); + + assert_eq!(byte_length, 4); + assert_eq!(length, [0x9f, 0xa4, 0x8b, 0x04]); + + let length = decode_length(length); + assert_eq!(length, 8573471); + } +} diff --git a/serial-comm/Cargo.toml b/serial-comm/Cargo.toml new file mode 100644 index 0000000..7ded2d0 --- /dev/null +++ b/serial-comm/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "serial-comm" +version.workspace = true +edition.workspace = true +description.workspace = true +license.workspace = true + +[dependencies] diff --git a/serial-comm/src/cobs.rs b/serial-comm/src/cobs.rs new file mode 100644 index 0000000..bc38ee4 --- /dev/null +++ b/serial-comm/src/cobs.rs @@ -0,0 +1,150 @@ +// sentinel value for backage boundaries +// could be anything but 0x00 is used as a default and makes sense +pub const SENTINEL: u8 = 0x00; +// max size of non SENTINEL values before a code split occurs +const SPLIT_BOUNDARY: u8 = 0xff; + +/// Encode a byte array of size `INPUT` with an actual data length of`length` +/// to an output array of size `OUTPUT`, using the COBS algorithm. +/// +/// Return a tuple of the structure (, ) +pub fn encode( + input: [u8; INPUT], + length: usize, +) -> (usize, [u8; OUTPUT]) { + let mut output: [u8; OUTPUT] = [0; OUTPUT]; + + let mut encode_idx = 1; + let mut code_idx = 0; + let mut code = 1; + + for byte in input.iter().take(length) { + if *byte == SENTINEL { + output[code_idx] = code; + code = 1; + code_idx = encode_idx; + encode_idx += 1; + } else { + output[encode_idx] = *byte; + encode_idx += 1; + code += 1; + + if code == SPLIT_BOUNDARY { + output[code_idx] = code; + code = 1; + code_idx = encode_idx; + + if encode_idx < length { + encode_idx += 1; + } + } + } + } + + if code_idx < OUTPUT { + output[code_idx] = code; + } + + (encode_idx, output) +} + +/// Decode a COBS encoded byte array of length `INPUT` and an actual data length of `length` to an +/// array of length `OUTPUT`. +/// +/// Return a tuple of the structure (, ) +pub fn decode( + input: [u8; INPUT], + length: usize, +) -> (usize, [u8; OUTPUT]) { + let mut output: [u8; OUTPUT] = [0; OUTPUT]; + + let mut out_idx = 0; + let mut idx = 0; + + while idx < length { + let code = input[idx]; + idx += 1; + + for _ in 1..code { + if idx < length { + output[out_idx] = input[idx]; + out_idx += 1; + idx += 1; + } + } + + if code != SPLIT_BOUNDARY && idx < length { + output[out_idx] = SENTINEL; + out_idx += 1; + } + } + + (out_idx, output) +} + +#[cfg(test)] +mod tests { + use crate::cobs::{decode, encode}; + + #[test] + fn test_paper_example() { + let input = [ + 0x45, 0x00, 0x00, 0x2c, 0x4c, 0x79, 0x00, 0x00, 0x40, 0x06, 0x4f, 0x37, + ]; + let output = [ + 0x02, 0x45, 0x01, 0x04, 0x2c, 0x4c, 0x79, 0x01, 0x05, 0x40, 0x06, 0x4f, 0x37, + ]; + assert_eq!(encode(input, 12), (13, output)); + assert_eq!(decode(output, 13), (12, input)); + } + + #[test] + fn test_empty_input() { + let input = []; + let output = [0x01]; + assert_eq!(encode(input, 0), (1, output)); + assert_eq!(decode(output, 1), (0, input)); + } + + #[test] + fn test_all_zeros() { + let input = [0x00, 0x00, 0x00]; + let output = [0x01, 0x01, 0x01, 0x01]; + assert_eq!(encode(input, 3), (4, output)); + assert_eq!(decode(output, 4), (3, input)); + } + + #[test] + fn test_no_zeros() { + let input = [0x01, 0x02, 0x03]; + let output = [0x04, 0x01, 0x02, 0x03]; + assert_eq!(encode(input, 3), (4, output)); + assert_eq!(decode(output, 4), (3, input)); + } + + #[test] + fn test_no_zeros_in_254_bytes() { + let input: [u8; 254] = [0x01; 254]; + + // expected output will have the first byte as 0xFF followed by the 255 non-zero bytes. + let mut output: [u8; 255] = [0x01; 255]; + output[0] = 0xff; + + assert_eq!(encode(input, 254), (255, output)); + assert_eq!(decode(output, 255), (254, input)); + } + + #[test] + fn test_no_zeros_in_254_bytes_and_additional_block() { + let mut input: [u8; 259] = [0x01; 259]; + input[254..259].copy_from_slice(&[0x02, 0x00, 0x03, 0x0c, 0x01]); + + // expected output will have the first byte as 0xFF followed by the 254 non-zero bytes. + let mut output: [u8; 261] = [0x01; 261]; + output[0] = 0xff; + output[255..261].copy_from_slice(&[0x02, 0x02, 0x04, 0x03, 0x0c, 0x01]); + + assert_eq!(encode(input, 259), (261, output)); + assert_eq!(decode(output, 261), (259, input)); + } +} diff --git a/serial-comm/src/lib.rs b/serial-comm/src/lib.rs new file mode 100644 index 0000000..0bb9a86 --- /dev/null +++ b/serial-comm/src/lib.rs @@ -0,0 +1,8 @@ +//! Simple communication protocol over a serial connection, leveraging the COBS algorithm for byte +//! stuffing +//! (S. Cheshire and M. Baker, "Consistent overhead byte stuffing," in IEEE/ACM Transactions on Networking, vol. 7, no. 2, pp. 159-172, April 1999, doi: 10.1109/90.769765). + +#![cfg_attr(not(test), no_std)] + +pub mod cobs; +pub mod protocol; diff --git a/serial-comm/src/protocol/cmd.rs b/serial-comm/src/protocol/cmd.rs new file mode 100644 index 0000000..2038bd8 --- /dev/null +++ b/serial-comm/src/protocol/cmd.rs @@ -0,0 +1,50 @@ +use core::str::Utf8Error; + +const MAX_PARAMS: usize = 8; + +pub trait Cobs<'a> { + /// Encode a command with the COBS algorithm. + /// + /// Return a tuple of the structure (, ) + fn as_cobs(&self) -> (usize, [u8; OUTPUT]); +} + +pub trait Cmd<'a> { + fn from_bytes( + input: [u8; INPUT], + buffer: &'a mut [u8; INPUT], + ) -> Result + where + Self: Sized; + + /// Read parameters from a byte array (not COBS encoded). + /// + /// Every parameter is interpreted as an UTF-8 string. + /// + /// Return a string array of size `MAX_PARAMS`. + fn params_from_bytes( + input: [u8; INPUT], + buffer: &mut [u8; INPUT], + ) -> Result<[&str; MAX_PARAMS], Utf8Error> { + buffer.copy_from_slice(&input); + + let param_count = buffer[0] as usize; + assert_eq!(param_count, PARAMS); + assert!(param_count <= MAX_PARAMS); + + let param_offset = param_count + 1; + let mut param_idx = 0; + let mut params: [&str; MAX_PARAMS] = [""; MAX_PARAMS]; + for i in 1..=param_count { + let param_length = buffer[i] as usize; + let param_start = param_idx + param_offset; + let param = &buffer[param_start..param_start + param_length]; + let param = core::str::from_utf8(param)?; + params[i - 1] = param; + + param_idx += param_length; + } + + Ok(params) + } +} diff --git a/serial-comm/src/protocol/error.rs b/serial-comm/src/protocol/error.rs new file mode 100644 index 0000000..55eecb8 --- /dev/null +++ b/serial-comm/src/protocol/error.rs @@ -0,0 +1,83 @@ +use core::str::Utf8Error; + +use crate::cobs; + +use super::cmd::{Cmd, Cobs}; + +/// The error command carries a message. +pub struct ErrorCommand<'a> { + pub prefix: &'a str, + pub msg: &'a str, +} + +impl<'a> ErrorCommand<'a> { + const PARAMS: u8 = 2; + const HEADER_LENGTH: usize = Self::PARAMS as usize + 1; + + pub fn new(msg: &'a str) -> Self { + Self { prefix: "ER", msg } + } +} + +impl<'a> Cobs<'a> for ErrorCommand<'a> { + fn as_cobs(&self) -> (usize, [u8; OUTPUT]) { + let prefix_bytes = self.prefix.as_bytes(); + let msg_bytes = self.msg.as_bytes(); + + let prefix_end = prefix_bytes.len() + Self::HEADER_LENGTH; + let msg_end = prefix_bytes.len() + msg_bytes.len() + Self::HEADER_LENGTH; + + let mut output: [u8; OUTPUT] = [0; OUTPUT]; + + output[0] = ErrorCommand::PARAMS; + output[1] = prefix_bytes.len() as u8; + output[2] = msg_bytes.len() as u8; + output[ErrorCommand::HEADER_LENGTH..prefix_end].copy_from_slice(prefix_bytes); + output[prefix_end..msg_end].copy_from_slice(msg_bytes); + + cobs::encode(output, msg_end) + } +} + +impl<'a> Cmd<'a> for ErrorCommand<'a> { + fn from_bytes( + input: [u8; INPUT], + buffer: &'a mut [u8; INPUT], + ) -> Result { + let params = as Cmd>::params_from_bytes::< + INPUT, + { ErrorCommand::PARAMS as usize }, + >(input, buffer)?; + + Ok(Self { + prefix: params[0], + msg: params[1], + }) + } +} + +#[cfg(test)] +mod tests { + use super::ErrorCommand; + use crate::cobs; + use crate::protocol::cmd::{Cmd, Cobs}; + + #[test] + fn test_ok_cmd() { + let cmd = ErrorCommand::new("error"); + let (length, cmd_cobs) = cmd.as_cobs::<64>(); + + assert_eq!(length, 11); + assert_eq!( + &cmd_cobs[0..11], + &[0x0b, 0x02, 0x02, 0x05, 0x45, 0x52, 0x65, 0x72, 0x72, 0x6f, 0x72] + ); + + let (_, decoded): (usize, [u8; 64]) = cobs::decode(cmd_cobs, 11); + + let mut cmd_buffer: [u8; 64] = [0; 64]; + let cmd = ErrorCommand::from_bytes::<64>(decoded, &mut cmd_buffer).unwrap(); + assert_eq!(cmd.prefix, "ER"); + assert_eq!(cmd.msg, "error"); + } +} diff --git a/serial-comm/src/protocol/mod.rs b/serial-comm/src/protocol/mod.rs new file mode 100644 index 0000000..4422344 --- /dev/null +++ b/serial-comm/src/protocol/mod.rs @@ -0,0 +1,6 @@ +//! Specific commands for the protocol. + +pub mod cmd; +pub mod error; +pub mod ok; +pub mod set_config; diff --git a/serial-comm/src/protocol/ok.rs b/serial-comm/src/protocol/ok.rs new file mode 100644 index 0000000..d3a9a9e --- /dev/null +++ b/serial-comm/src/protocol/ok.rs @@ -0,0 +1,76 @@ +use core::str::Utf8Error; + +use crate::cobs; + +use super::cmd::{Cmd, Cobs}; + +// The Ok command has no parameters, it should merely signify something went ok. +pub struct OkCommand<'a> { + pub prefix: &'a str, +} + +impl<'a> OkCommand<'a> { + const PARAMS: u8 = 1; + const HEADER_LENGTH: usize = Self::PARAMS as usize + 1; + + pub fn new() -> Self { + Self { prefix: "OK" } + } +} + +impl<'a> Default for OkCommand<'a> { + fn default() -> Self { + Self::new() + } +} + +impl<'a> Cobs<'a> for OkCommand<'a> { + fn as_cobs(&self) -> (usize, [u8; OUTPUT]) { + let prefix_bytes = self.prefix.as_bytes(); + let prefix_end = prefix_bytes.len() + Self::HEADER_LENGTH; + + let mut output: [u8; OUTPUT] = [0; OUTPUT]; + + output[0] = OkCommand::PARAMS; + output[1] = prefix_bytes.len() as u8; + output[OkCommand::HEADER_LENGTH..prefix_end].copy_from_slice(prefix_bytes); + + cobs::encode(output, prefix_end) + } +} + +impl<'a> Cmd<'a> for OkCommand<'a> { + fn from_bytes( + input: [u8; INPUT], + buffer: &'a mut [u8; INPUT], + ) -> Result { + let params = as Cmd>::params_from_bytes::< + INPUT, + { OkCommand::PARAMS as usize }, + >(input, buffer)?; + + Ok(Self { prefix: params[0] }) + } +} + +#[cfg(test)] +mod tests { + use super::OkCommand; + use crate::cobs; + use crate::protocol::cmd::{Cmd, Cobs}; + + #[test] + fn test_ok_cmd() { + let cmd = OkCommand::new(); + let (length, cmd_cobs) = cmd.as_cobs::<64>(); + + assert_eq!(length, 5); + assert_eq!(&cmd_cobs[0..5], &[0x05, 0x01, 0x02, 0x4f, 0x4b]); + + let (_, decoded): (usize, [u8; 64]) = cobs::decode(cmd_cobs, 5); + + let mut cmd_buffer: [u8; 64] = [0; 64]; + let cmd = OkCommand::from_bytes::<64>(decoded, &mut cmd_buffer).unwrap(); + assert_eq!(cmd.prefix, "OK"); + } +} diff --git a/serial-comm/src/protocol/set_config.rs b/serial-comm/src/protocol/set_config.rs new file mode 100644 index 0000000..7fd7a42 --- /dev/null +++ b/serial-comm/src/protocol/set_config.rs @@ -0,0 +1,92 @@ +use core::str::Utf8Error; + +use crate::cobs; + +use super::cmd::{Cmd, Cobs}; + +#[derive(Debug)] +/// The SetConfig command sets the value of a key. +pub struct SetConfig<'a> { + pub key: &'a str, + pub value: &'a str, +} + +impl<'a> SetConfig<'a> { + pub const PREFIX: &'a str = "SC"; + const PARAMS: u8 = 3; + const HEADER_LENGTH: usize = Self::PARAMS as usize + 1; + + pub fn new(key: &'a str, value: &'a str) -> Self { + Self { key, value } + } +} + +impl<'a> Cobs<'a> for SetConfig<'a> { + /// Encode a SetConfig command with the COBS algorithm. + /// + /// Return a tuple of the structure (, ) + fn as_cobs(&self) -> (usize, [u8; OUTPUT]) { + let prefix_bytes = Self::PREFIX.as_bytes(); + let key_bytes = self.key.as_bytes(); + let val_bytes = self.value.as_bytes(); + + let prefix_end = prefix_bytes.len() + Self::HEADER_LENGTH; + let key_end = prefix_end + key_bytes.len(); + let val_end = key_end + val_bytes.len(); + + let mut output: [u8; OUTPUT] = [0; OUTPUT]; + + output[0] = Self::PARAMS; + output[1] = prefix_bytes.len() as u8; + output[2] = key_bytes.len() as u8; + output[3] = val_bytes.len() as u8; + output[Self::HEADER_LENGTH..prefix_end].copy_from_slice(prefix_bytes); + output[prefix_end..key_end].copy_from_slice(key_bytes); + output[key_end..val_end].copy_from_slice(val_bytes); + + cobs::encode(output, val_end) + } +} + +impl<'a> Cmd<'a> for SetConfig<'a> { + fn from_bytes( + input: [u8; INPUT], + buffer: &'a mut [u8; INPUT], + ) -> Result { + let params = as Cmd>::params_from_bytes::< + INPUT, + { SetConfig::PARAMS as usize }, + >(input, buffer)?; + + Ok(Self { + key: params[1], + value: params[2], + }) + } +} + +#[cfg(test)] +mod tests { + use crate::cobs; + use crate::protocol::cmd::{Cmd, Cobs}; + use crate::protocol::set_config::SetConfig; + + #[test] + fn test_config_cmd() { + let cmd = SetConfig::new("ssid", "pw"); + let (length, cmd_cobs) = cmd.as_cobs::<64>(); + + assert_eq!(length, 13); + assert_eq!( + &cmd_cobs[0..13], + &[0x0d, 0x03, 0x02, 0x04, 0x02, 0x53, 0x43, 0x73, 0x73, 0x69, 0x64, 0x70, 0x77] + ); + + let (_, decoded): (usize, [u8; 64]) = cobs::decode(cmd_cobs, 13); + + let mut cmd_buffer: [u8; 64] = [0; 64]; + let cmd = SetConfig::from_bytes::<64>(decoded, &mut cmd_buffer).unwrap(); + assert_eq!(cmd.key, "ssid"); + assert_eq!(cmd.value, "pw"); + } +}