diff --git a/.cargo/config b/.cargo/config index 014252e..9fb77ac 100644 --- a/.cargo/config +++ b/.cargo/config @@ -1,11 +1,14 @@ [target.'cfg(all(target_arch = "arm", target_os = "none"))'] -runner = "probe-run --chip STM32H743ZITx --speed 30000" +runner = "probe-rs run --chip STM32H743ZITx --log-file /dev/null" # runner = "gdb-multiarch -q -x openocd.gdb" rustflags = [ "-C", "link-arg=-Tlink.x", "-C", "link-arg=--nmagic", "-C", "target-cpu=cortex-m7", + "-C", "target-feature=+fp-armv8d16", + # fp-armv8d16 is unstable and not a rustc feature but accurate + # fp-armv8 is unstable and a rustc feature but incorrect ] [build] -target = "thumbv7em-none-eabihf" \ No newline at end of file +target = "thumbv7em-none-eabihf" diff --git a/.github/dependabot.yml b/.github/dependabot.yml index cf3221a..9752c41 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,7 @@ version: 2 updates: - package-ecosystem: "cargo" - directory: "/" # Location of package manifests + directory: "/" schedule: - interval: "daily" + interval: "weekly" time: "04:00" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1832f2..2845b00 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,13 @@ name: Continuous Integration - on: + merge_group: push: - + branches: [main] pull_request: branches: [main] + schedule: + # UTC + - cron: '48 4 * * *' env: CARGO_TERM_COLOR: always @@ -33,6 +36,7 @@ jobs: with: command: check args: --verbose + - uses: actions/setup-python@v1 with: python-version: 3.8 @@ -47,17 +51,62 @@ jobs: compile: runs-on: ubuntu-latest + continue-on-error: ${{ matrix.continue-on-error }} strategy: matrix: - toolchain: [stable] + # keep MSRV in sync in ci.yaml and Cargo.toml + toolchain: [stable, '1.66.1'] + features: [''] + continue-on-error: [false] + include: + - toolchain: beta + features: '' + continue-on-error: true + - toolchain: nightly + features: nightly + continue-on-error: true steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: - toolchain: stable + toolchain: ${{ matrix.toolchain }} target: thumbv7em-none-eabihf override: true - uses: actions-rs/cargo@v1 with: command: build - args: --release + args: --release --features "${{ matrix.features }}" + + doc: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: thumbv7em-none-eabihf + override: true + + - uses: Swatinem/rust-cache@v1 + + - name: Install Deadlinks + uses: actions-rs/cargo@v1 + with: + command: install + args: | + cargo-deadlinks + + - name: cargo doc + uses: actions-rs/cargo@v1 + with: + command: doc + args: --no-deps -p miniconf -p idsp -p thermostat-eem + + - name: cargo deadlinks + uses: actions-rs/cargo@v1 + with: + command: deadlinks + # We intentionally ignore fragments, as RTIC may generate fragments for various + # auto-generated code. + args: --dir target/thumbv7em-none-eabihf/doc --ignore-fragments --check-intra-doc-links diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..944df36 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ + +# Change Log + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + +## [Unreleased](https://github.com/quartiq/thermostat-eem/compare/v0.1.0...master) + +### Changed +* A DNS name for the MQTT broker of `mqtt` is now used instead of a hard-coded IP address. +* A static IP can be assigned to Thermostat using the `STATIC_IP` environment variable at build + time. If one is not specified, DHCP is used automatically. +* Miniconf versions have been updated to v0.9.0 +* Application metadata is now published to the `/meta` topic on MQTT connections diff --git a/Cargo.lock b/Cargo.lock index 884988b..8519e39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,24 +32,13 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "asm-delay" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9a69a963b70ddacfcd382524f72a4576f359af9334b3bf48a79566590bb8bfa" -dependencies = [ - "bitrate", - "cortex-m 0.7.7", - "embedded-hal", -] - [[package]] name = "atomic-polyfill" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c041a8d9751a520ee19656232a18971f18946a7900f1520ee4400002244dd89" dependencies = [ - "critical-section", + "critical-section 0.2.7", ] [[package]] @@ -92,16 +81,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] -name = "bitrate" -version = "0.1.1" +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "built" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c147d86912d04bef727828fda769a76ca81629a46d8ba311a8d58a26aa91473d" +checksum = "38d17f4d6e4dc36d1a02fbedc2753a096848e7c1b0772f7654eab8e2c927dd53" +dependencies = [ + "git2", +] [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cast" @@ -109,6 +107,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "jobserver", + "libc", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -136,6 +144,7 @@ checksum = "8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9" dependencies = [ "bare-metal 0.2.5", "bitfield", + "critical-section 1.1.2", "embedded-hal", "volatile-register", ] @@ -200,6 +209,12 @@ dependencies = [ "riscv", ] +[[package]] +name = "critical-section" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" + [[package]] name = "embedded-dma" version = "0.2.0" @@ -219,17 +234,29 @@ dependencies = [ "void", ] +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "embedded-nal" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db9efecb57ab54fa918730f2874d7d37647169c50fa1357fecb81abee840b113" +checksum = "447416d161ba378782c13e82b11b267d6d2104b4913679a7c5640e7e94f96ea7" dependencies = [ "heapless", "nb 1.0.0", "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-time" version = "0.12.1" @@ -241,22 +268,31 @@ dependencies = [ [[package]] name = "enum-iterator" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "706d9e7cf1c7664859d79cd524e4e53ea2b67ea03c98cc2870c5e539695d597e" +checksum = "9fd242f399be1da0a5354aa462d57b4ab2b4ee0683cc552f7c007d2d12d36e94" dependencies = [ "enum-iterator-derive", ] [[package]] name = "enum-iterator-derive" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355f93763ef7b0ae1c43c4d8eccc9d5848d84ad1a1d8ce61c421d1ac85a19d05" +checksum = "03cdc46ec28bd728e67540c528013c6a10eb69a02eb31078a1bda695438cbfb8" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.16", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", ] [[package]] @@ -305,6 +341,19 @@ dependencies = [ "version_check", ] +[[package]] +name = "git2" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf97ba92db08df386e10c8ede66a2a0369bd277090afd8710e19e38de9ec0cd" +dependencies = [ + "bitflags 2.4.2", + "libc", + "libgit2-sys", + "log", + "url", +] + [[package]] name = "hash32" version = "0.2.1" @@ -334,11 +383,21 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idsp" -version = "0.9.2" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f2a7a4a8265bedb4b15abf376c0e9eb2def6290868709894efc27633b04618" +checksum = "8f255ee573949fb629362d10aa3abd0a97a7c4950a3b8890b435b8c7516cf38f" dependencies = [ "num-complex 0.4.2", "num-traits", @@ -361,18 +420,57 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +[[package]] +name = "jobserver" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +dependencies = [ + "libc", +] + [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libgit2-sys" +version = "0.16.1+1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2a2bb3680b094add03bb3732ec520ece34da31a8cd2d633d1389d0f0fb60d0c" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + [[package]] name = "libm" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565" +[[package]] +name = "libz-sys" +version = "1.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037731f5d3aaa87a5675e895b63ddff1a87624bc29f77004ea829809654e48f6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "lock_api" version = "0.4.9" @@ -385,12 +483,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "managed" @@ -406,9 +501,11 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "miniconf" -version = "0.6.3" -source = "git+https://github.com/quartiq/miniconf#8be449dcd9be7c838720c21223dac0841595dd26" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df2d7bdba3acb28460c347b21e1e88d869f2716ebe060eb6a79f7b76b57de72" dependencies = [ + "embedded-io", "heapless", "itoa", "log", @@ -421,19 +518,20 @@ dependencies = [ [[package]] name = "miniconf_derive" -version = "0.6.2" -source = "git+https://github.com/quartiq/miniconf#8be449dcd9be7c838720c21223dac0841595dd26" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89f46d25f40e41f552d76b8eb9e225fe493ebf978a5c3f42b7599e45cfe6b4e3" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.16", ] [[package]] name = "minimq" -version = "0.6.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b222dbc2667cc69a2d9acdf5c24280c49233295eda3a767f6ddfb4040f5cc80c" +checksum = "b561c2c86a3509f7c514f546fb24755753a30fdcf67cce8d5f2f38688483cd31" dependencies = [ "bit_field", "embedded-nal", @@ -468,9 +566,9 @@ checksum = "546c37ac5d9e56f55e73b677106873d9d9f5190605e41a856503623648488cae" [[package]] name = "no-std-net" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bcece43b12349917e096cddfa66107277f123e6c96a5aea78711dc601a47152" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" [[package]] name = "num" @@ -538,9 +636,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", "libm", @@ -548,22 +646,22 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.5.11" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.5.11" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.16", ] [[package]] @@ -582,6 +680,18 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pkg-config" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -608,9 +718,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.52" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] @@ -759,18 +869,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.158" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" dependencies = [ "serde_derive", ] [[package]] name = "serde-json-core" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58ec3c8fe427f45ee3aaa0ebb9f0d9ab8ae9ad05d12047fe7249ae5ea9374ff0" +checksum = "3c9e1ab533c0bc414c34920ec7e5f097101d126ed5eac1a1aac711222e0bbb33" dependencies = [ "heapless", "ryu", @@ -779,13 +889,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.158" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.4", + "syn 2.0.16", ] [[package]] @@ -800,17 +910,6 @@ dependencies = [ "nb 0.1.3", ] -[[package]] -name = "shared-bus-rtic" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb1899470d03c5728db375f63be8f2bbfb93d8c35ec932061f3b593434c2273b" -dependencies = [ - "cortex-m 0.6.7", - "embedded-hal", - "nb 1.0.0", -] - [[package]] name = "smlang" version = "0.6.0" @@ -833,20 +932,22 @@ dependencies = [ [[package]] name = "smoltcp" -version = "0.8.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72165c4af59f5f19c7fb774b88b95660591b612380305b5f4503157341a9f7ee" +checksum = "8d2e3a36ac8fea7b94e666dfa3871063d6e0a5c9d5d4fec9a1a6b7b6760f0229" dependencies = [ - "bitflags", + "bitflags 1.3.2", "byteorder", + "cfg-if", + "heapless", "managed", ] [[package]] name = "smoltcp-nal" -version = "0.2.1" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a63a94229e438f53c83757c51b346a09409064f6d18723e0393e11d829b09640" +checksum = "6dd2ed2f8e7643a170506863ed0f52ad1dc5762abdcff27de825dde14fc8025f" dependencies = [ "embedded-nal", "embedded-time", @@ -885,16 +986,16 @@ dependencies = [ [[package]] name = "stm32h7xx-hal" -version = "0.13.1" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c74a1d83bf1e19db5e504c2cb2d98e7ca5f7ff758ca69f44cf6bfddc90af30dc" +checksum = "e08bcfbdbe4458133f2fd55994a5c4f1b4bf28084f0218e93cdbc19d7c70219f" dependencies = [ "bare-metal 1.0.0", - "bitflags", "cast", "cortex-m 0.7.7", "embedded-dma", "embedded-hal", + "embedded-storage", "fugit", "nb 1.0.0", "paste", @@ -916,9 +1017,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.4" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c622ae390c9302e214c31013517c2061ecb2699935882c60a9b37f82f8625ae" +checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" dependencies = [ "proc-macro2", "quote", @@ -940,7 +1041,7 @@ dependencies = [ name = "thermostat-eem" version = "0.1.0" dependencies = [ - "asm-delay", + "built", "byteorder", "cortex-m 0.7.7", "cortex-m-rt", @@ -959,13 +1060,27 @@ dependencies = [ "rtt-target", "serde", "serde-json-core", - "shared-bus-rtic", "smlang", "smoltcp-nal", "stm32h7xx-hal", "systick-monotonic", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "typenum" version = "1.15.0" @@ -978,12 +1093,38 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e87a2ed6b42ec5e28cc3b94c09982969e9227600b2e3dcbc1db927a84c06bd69" +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + [[package]] name = "unicode-ident" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "varint-rs" version = "2.2.0" @@ -996,6 +1137,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 3f65c95..e477162 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,16 +2,18 @@ name = "thermostat-eem" version = "0.1.0" edition = "2021" -authors = ["Norman Krackow ", "Robert Jördens", "Ryan Summers"] +authors = ["Norman Krackow ", "Robert Jördens ", "Ryan Summers"] description = "Firmware for the Sinara Thermostat-EEM temperature controller." categories = ["embedded", "no-std", "hardware-support", "science"] license = "MIT OR Apache-2.0" -keywords = ["stm32h7", "physics", "adc", "dac", "dsp", "control systems"] +keywords = ["ethernet", "stm32h7", "adc", "dac", "physics"] repository = "https://github.com/quartiq/thermostat-eem" readme = "README.md" exclude = [ ".gitignore" ] +# keep MSRV in sync in ci.yaml and Cargo.toml +rust-version = "1.66.1" [features] default = ["all_differential"] @@ -21,28 +23,28 @@ ai_artiq = [] [dependencies] cortex-m = { version = "0.7" } cortex-m-rt = { version = "0.7", features = ["device"] } -stm32h7xx-hal = { version = "0.13.1", features = ["stm32h743v", "rt", "ethernet"]} +stm32h7xx-hal = { version = "0.15.1", features = ["stm32h743v", "rt", "ethernet"]} log = { version = "0.4", features = ["max_level_trace", "release_max_level_info"] } -panic-probe = { version = "0.3.0", features = ["print-rtt"] } -asm-delay = "0.9.0" -smoltcp-nal = { version = "0.2", features = ["shared-stack"] } +panic-probe = { version = "=0.3.0", features = ["print-rtt"] } +smoltcp-nal = { version = "0.4", features = ["shared-stack"] } cortex-m-rtic = "1.1.4" embedded-time = "0.12.1" -miniconf = "0.6.3" systick-monotonic = "1.0.1" heapless = { version = "0.7", features = ["serde"] } -minimq = "0.6.2" +minimq = "0.8" serde = { version = "1.0.158", features = ["derive"], default-features = false } serde-json-core = "0.5" -num_enum = { version = "0.5.11", default-features = false } +num_enum = { version = "0.7.2", default-features = false } rtt-logger = "0.2" rtt-target = { version = "0.3", features = ["cortex-m"] } num-traits = { version = "0.2", default-features = false, features = ["libm"] } -shared-bus-rtic = { version = "0.2", features = ["cortex-m"] } byteorder = { version = "1", default-features = false } smlang = "0.6.0" -enum-iterator = "1.4.0" -idsp = "0.9.2" +enum-iterator = "1.5.0" +idsp = "0.14.1" + +# Note: Keep in-sync with `py/setup.py` +miniconf = "0.9" [profile.dev] opt-level = 3 @@ -52,5 +54,5 @@ opt-level = 3 lto = true codegen-units = 1 -[patch.crates-io] -miniconf = { git = "https://github.com/quartiq/miniconf" } +[build-dependencies] +built = { version = "0.7", features = ["git2"], default-features = false } diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..7fef7be --- /dev/null +++ b/build.rs @@ -0,0 +1,4 @@ +fn main() { + built::write_built_file().expect("Failed to acquire build-time information"); + println!("cargo:rerun-if-changed=memory.x"); +} diff --git a/py/setup.py b/py/setup.py index a654744..5371ca1 100644 --- a/py/setup.py +++ b/py/setup.py @@ -13,7 +13,8 @@ author="QUARTIQ GmbH", license="MIT", install_requires=[ - "stabilizer@git+https://github.com/quartiq/stabilizer@f4055aa#subdirectory=py", - "miniconf-mqtt@git+https://github.com/quartiq/miniconf@8be449d#subdirectory=py/miniconf-mqtt", + "stabilizer@git+https://github.com/quartiq/stabilizer@d25f2efaaa598eb1a47ceed409f7179d7031f4c0#subdirectory=py", + # Note: Keep in-sync with `Cargo.toml` + "miniconf-mqtt@git+https://github.com/quartiq/miniconf@v0.9.0#subdirectory=py/miniconf-mqtt", ], ) diff --git a/py/thermostat/configure_output_ch.py b/py/thermostat/configure_output_ch.py index 299d28c..94ff18e 100644 --- a/py/thermostat/configure_output_ch.py +++ b/py/thermostat/configure_output_ch.py @@ -23,7 +23,7 @@ def _main(): "--broker", "-b", type=str, - default="10.42.0.1", + default="mqtt", help="The MQTT broker to use to communicate with " "Thermostat-EEM (%(default)s)", ) @@ -152,30 +152,30 @@ async def configure(): interface = await miniconf.Miniconf.create(prefix, args.broker) # Set the filter coefficients. - await interface.command( - f"output_channel/{args.channel}/shutdown", + await interface.set( + f"/output_channel/{args.channel}/shutdown", args.shutdown, ) - await interface.command( - f"output_channel/{args.channel}/hold", + await interface.set( + f"/output_channel/{args.channel}/hold", args.hold, ) - await interface.command( - f"output_channel/{args.channel}/voltage_limit", + await interface.set( + f"/output_channel/{args.channel}/voltage_limit", args.voltage_limit, ) - await interface.command( - f"output_channel/{args.channel}/iir", + await interface.set( + f"/output_channel/{args.channel}/iir", { "ba": coefficients, - "y_offset": args.y_offset + forward_gain * args.x_offset, - "y_min": args.y_min, - "y_max": args.y_max, + "u": args.y_offset + forward_gain * args.x_offset, + "min": args.y_min, + "max": args.y_max, }, ) for i, weight in enumerate(args.input_weights): - await interface.command( - f"output_channel/{args.channel}/weights/{i}", + await interface.set( + f"/output_channel/{args.channel}/weights/{i}", weight, ) diff --git a/src/hardware/adc.rs b/src/hardware/adc.rs index 23838d7..1d38e91 100644 --- a/src/hardware/adc.rs +++ b/src/hardware/adc.rs @@ -197,7 +197,7 @@ impl Adc { Ok(adc) } - /// Setup all ADCs to the specifies [config]. + /// Setup all ADCs to the specifies [AdcConfig]. fn setup(&mut self, delay: &mut impl DelayUs, config: AdcConfig) -> Result<(), Error> { // deassert all CS first for pin in self.cs.iter_mut() { diff --git a/src/hardware/dac.rs b/src/hardware/dac.rs index 256904f..2b00bb5 100644 --- a/src/hardware/dac.rs +++ b/src/hardware/dac.rs @@ -1,15 +1,16 @@ -///! Thermostat DAC driver -///! -///! This file contains the driver for the 4 Thermostat DAC output channels. -///! To convert a 18 bit word into an analog current Thermostat uses a DAC to -///! convert the word into a voltage and a subsequent TEC driver IC that produces -///! a current proportional to the DAC voltage. -///! -///! The 4 channel DAC ICs share an SPI bus and are addressed using individual "sync" -///! signals, similar to a chip select signal. -///! DAC datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/AD5680.pdf -///! TEC driver datasheet: https://datasheets.maximintegrated.com/en/ds/MAX1968-MAX1969.pdf -///! +//! Thermostat DAC driver +//! +//! This file contains the driver for the 4 Thermostat DAC output channels. +//! To convert a 18 bit word into an analog current Thermostat uses a DAC to +//! convert the word into a voltage and a subsequent TEC driver IC that produces +//! a current proportional to the DAC voltage. +//! +//! The 4 channel DAC ICs share an SPI bus and are addressed using individual "sync" +//! signals, similar to a chip select signal. +//! DAC datasheet: `` +//! TEC driver datasheet: `` +//! + use super::hal::{ gpio::{self, gpioc}, hal::blocking::spi::Write, @@ -70,8 +71,8 @@ impl From for u32 { /// DAC gpio pins. /// -/// sync[] - DAC IC adressing signals -/// * specifies Thermostat output channel +/// `sync[n]` - DAC IC adressing signals +/// where `n` specifies Thermostat output channel pub struct DacPins { pub sync: [gpio::ErasedPin; 4], } diff --git a/src/hardware/metadata.rs b/src/hardware/metadata.rs new file mode 100644 index 0000000..d022c72 --- /dev/null +++ b/src/hardware/metadata.rs @@ -0,0 +1,36 @@ +use serde::Serialize; + +mod build_info { + include!(concat!(env!("OUT_DIR"), "/built.rs")); +} + +#[derive(Serialize)] +pub struct ApplicationMetadata { + pub app: &'static str, + pub firmware_version: &'static str, + pub rust_version: &'static str, + pub profile: &'static str, + pub git_dirty: bool, + pub features: &'static str, +} + +impl ApplicationMetadata { + /// Construct the global metadata. + /// + /// # Note + /// This may only be called once. + /// + /// # Returns + /// A reference to the global metadata. + pub fn new() -> &'static ApplicationMetadata { + cortex_m::singleton!(: ApplicationMetadata = ApplicationMetadata { + app: env!("CARGO_BIN_NAME"), + firmware_version: build_info::GIT_VERSION.unwrap_or(build_info::PKG_VERSION), + rust_version: build_info::RUSTC_VERSION, + profile: build_info::PROFILE, + git_dirty: build_info::GIT_DIRTY.unwrap_or(false), + features: build_info::FEATURES_STR, + }) + .unwrap() + } +} diff --git a/src/hardware/mod.rs b/src/hardware/mod.rs index 491628c..7c07ebe 100644 --- a/src/hardware/mod.rs +++ b/src/hardware/mod.rs @@ -13,6 +13,7 @@ pub mod dac; pub mod delay; pub mod fan; pub mod gpio; +pub mod metadata; pub mod pwm; pub mod setup; pub mod system_timer; @@ -25,13 +26,13 @@ const RX_DESRING_CNT: usize = 4; pub type NetworkStack = smoltcp_nal::NetworkStack< 'static, - hal::ethernet::EthernetDMA<'static, TX_DESRING_CNT, RX_DESRING_CNT>, + hal::ethernet::EthernetDMA, system_timer::SystemTimer, >; pub type NetworkManager = smoltcp_nal::shared::NetworkManager< 'static, - hal::ethernet::EthernetDMA<'static, TX_DESRING_CNT, RX_DESRING_CNT>, + hal::ethernet::EthernetDMA, system_timer::SystemTimer, >; diff --git a/src/hardware/pwm.rs b/src/hardware/pwm.rs index 4721d85..8b83a74 100644 --- a/src/hardware/pwm.rs +++ b/src/hardware/pwm.rs @@ -1,8 +1,8 @@ -///! Thermostat TEC driver IC voltage/current limits PWM driver. -///! -///! The Thermostat TEC driver ICs feature current limits controlled by an analog voltage input. -///! This voltage is controlled by the MCU using low-pass filtered PWM outputs. -///! +//! Thermostat TEC driver IC voltage/current limits PWM driver. +//! +//! The Thermostat TEC driver ICs feature current limits controlled by an analog voltage input. +//! This voltage is controlled by the MCU using low-pass filtered PWM outputs. +//! use super::{ dac::{R_SENSE, VREF_TEC}, hal::{ @@ -36,10 +36,10 @@ pub enum Error { /// Pwm pins. /// -/// voltage - voltage limit pin -/// positive_current - positive current limit pin -/// negative_current - negative current limit pin -/// * specifies Thermostat output channel +/// `voltage.` - voltage limit pin +/// `positive_current.` - positive current limit pin +/// `negative_current.` - negative current limit pin +/// * `` specifies Thermostat output channel #[allow(clippy::type_complexity)] pub struct PwmPins { pub voltage: ( diff --git a/src/hardware/setup.rs b/src/hardware/setup.rs index ba91c2f..1458a80 100644 --- a/src/hardware/setup.rs +++ b/src/hardware/setup.rs @@ -1,3 +1,4 @@ +use core::mem::MaybeUninit; use core::sync::atomic::{AtomicBool, Ordering}; use crate::hardware::{ @@ -20,6 +21,7 @@ use super::{ delay, fan::{Fan, FanPins}, gpio::{Gpio, GpioPins}, + metadata::ApplicationMetadata, pwm::{Pwm, PwmPins}, EthernetPhy, NetworkStack, }; @@ -37,15 +39,14 @@ pub struct NetStorage { pub sockets: [smoltcp::iface::SocketStorage<'static>; NUM_SOCKETS + 1], pub tcp_socket_storage: [TcpSocketStorage; NUM_TCP_SOCKETS], pub udp_socket_storage: [UdpSocketStorage; NUM_UDP_SOCKETS], - pub neighbor_cache: [Option<(smoltcp::wire::IpAddress, smoltcp::iface::Neighbor)>; 8], - pub routes_cache: [Option<(smoltcp::wire::IpCidr, smoltcp::iface::Route)>; 8], + pub dns_storage: [Option; 1], } pub struct UdpSocketStorage { rx_storage: [u8; 1024], tx_storage: [u8; 2048], - tx_metadata: [smoltcp::storage::PacketMetadata; 10], - rx_metadata: [smoltcp::storage::PacketMetadata; 10], + tx_metadata: [smoltcp::storage::PacketMetadata; 10], + rx_metadata: [smoltcp::storage::PacketMetadata; 10], } impl UdpSocketStorage { @@ -53,8 +54,8 @@ impl UdpSocketStorage { Self { rx_storage: [0; 1024], tx_storage: [0; 2048], - tx_metadata: [smoltcp::storage::PacketMetadata::::EMPTY; 10], - rx_metadata: [smoltcp::storage::PacketMetadata::::EMPTY; 10], + tx_metadata: [smoltcp::storage::PacketMetadata::EMPTY; 10], + rx_metadata: [smoltcp::storage::PacketMetadata::EMPTY; 10], } } } @@ -81,11 +82,11 @@ impl Default for NetStorage { ip_addrs: [smoltcp::wire::IpCidr::Ipv6( smoltcp::wire::Ipv6Cidr::SOLICITED_NODE_PREFIX, )], - neighbor_cache: [None; 8], - routes_cache: [None; 8], + // Placeholder for the real IP address, which is initialized at runtime. sockets: [smoltcp::iface::SocketStorage::EMPTY; NUM_SOCKETS + 1], tcp_socket_storage: [TcpSocketStorage::new(); NUM_TCP_SOCKETS], udp_socket_storage: [UdpSocketStorage::new(); NUM_UDP_SOCKETS], + dns_storage: [None; 1], } } } @@ -108,12 +109,14 @@ pub struct ThermostatDevices { pub adc_internal: AdcInternal, pub adc_sm: StateMachine, pub adc_channels: [[bool; 4]; 4], + pub metadata: &'static ApplicationMetadata, } #[link_section = ".sram3.eth"] /// Static storage for the ethernet DMA descriptor ring. -static mut DES_RING: ethernet::DesRing<{ super::TX_DESRING_CNT }, { super::RX_DESRING_CNT }> = - ethernet::DesRing::new(); +static mut DES_RING: MaybeUninit< + ethernet::DesRing<{ super::TX_DESRING_CNT }, { super::RX_DESRING_CNT }>, +> = MaybeUninit::uninit(); pub fn setup( mut device: stm32h7xx_hal::stm32::Peripherals, @@ -486,15 +489,17 @@ pub fn setup( (ref_clk, mdio, mdc, crs_dv, rxd0, rxd1, tx_en, txd0, txd1) }; + unsafe { DES_RING.write(ethernet::DesRing::new()) }; + // Configure the ethernet controller - let (eth_dma, eth_mac) = ethernet::new( + let (mut eth_dma, eth_mac) = ethernet::new( device.ETHERNET_MAC, device.ETHERNET_MTL, device.ETHERNET_DMA, ethernet_pins, // Note(unsafe): We only call this function once to take ownership of the // descriptor ring. - unsafe { &mut DES_RING }, + unsafe { DES_RING.assume_init_mut() }, mac_addr, ccdr.peripheral.ETH1MAC, &ccdr.clocks, @@ -507,67 +512,90 @@ pub fn setup( unsafe { ethernet::enable_interrupt() }; + // Configure IP address according to DHCP socket availability + let ip_addrs: smoltcp::wire::IpAddress = option_env!("STATIC_IP") + .unwrap_or("0.0.0.0") + .parse() + .unwrap(); + + let random_seed = { + let mut rng = device.RNG.constrain(ccdr.peripheral.RNG, &ccdr.clocks); + let mut data = [0u8; 8]; + rng.fill(&mut data).unwrap(); + data + }; + // Note(unwrap): The hardware configuration function is only allowed to be called once. // Unwrapping is intended to panic if called again to prevent re-use of global memory. let store = cortex_m::singleton!(: NetStorage = NetStorage::default()).unwrap(); - store.ip_addrs[0] = smoltcp::wire::IpCidr::new( - smoltcp::wire::IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED), - 0, + store.ip_addrs[0] = smoltcp::wire::IpCidr::new(ip_addrs, 24); + + let mut ethernet_config = + smoltcp::iface::Config::new(smoltcp::wire::HardwareAddress::Ethernet(mac_addr)); + + ethernet_config.random_seed = u64::from_be_bytes(random_seed); + + let mut interface = smoltcp::iface::Interface::new( + ethernet_config, + &mut eth_dma, + smoltcp::time::Instant::ZERO, ); - let mut routes = smoltcp::iface::Routes::new(&mut store.routes_cache[..]); - routes + interface + .routes_mut() .add_default_ipv4_route(smoltcp::wire::Ipv4Address::UNSPECIFIED) .unwrap(); - let neighbor_cache = smoltcp::iface::NeighborCache::new(&mut store.neighbor_cache[..]); - - let mut interface = smoltcp::iface::InterfaceBuilder::new(eth_dma, &mut store.sockets[..]) - .hardware_addr(smoltcp::wire::HardwareAddress::Ethernet(mac_addr)) - .neighbor_cache(neighbor_cache) - .ip_addrs(&mut store.ip_addrs[..]) - .routes(routes) - .finalize(); - - interface.add_socket(smoltcp::socket::Dhcpv4Socket::new()); + interface.update_ip_addrs(|ref mut addrs| { + if !ip_addrs.is_unspecified() { + addrs + .push(smoltcp::wire::IpCidr::new(ip_addrs, 24)) + .unwrap(); + } + }); + let mut sockets = smoltcp::iface::SocketSet::new(&mut store.sockets[..]); for storage in store.tcp_socket_storage[..].iter_mut() { let tcp_socket = { - let rx_buffer = smoltcp::socket::TcpSocketBuffer::new(&mut storage.rx_storage[..]); - let tx_buffer = smoltcp::socket::TcpSocketBuffer::new(&mut storage.tx_storage[..]); + let rx_buffer = + smoltcp::socket::tcp::SocketBuffer::new(&mut storage.rx_storage[..]); + let tx_buffer = + smoltcp::socket::tcp::SocketBuffer::new(&mut storage.tx_storage[..]); - smoltcp::socket::TcpSocket::new(rx_buffer, tx_buffer) + smoltcp::socket::tcp::Socket::new(rx_buffer, tx_buffer) }; - interface.add_socket(tcp_socket); + sockets.add(tcp_socket); } + if ip_addrs.is_unspecified() { + sockets.add(smoltcp::socket::dhcpv4::Socket::new()); + } + + sockets.add(smoltcp::socket::dns::Socket::new( + &[], + &mut store.dns_storage[..], + )); + for storage in store.udp_socket_storage[..].iter_mut() { let udp_socket = { - let rx_buffer = smoltcp::socket::UdpSocketBuffer::new( + let rx_buffer = smoltcp::socket::udp::PacketBuffer::new( &mut storage.rx_metadata[..], &mut storage.rx_storage[..], ); - let tx_buffer = smoltcp::socket::UdpSocketBuffer::new( + let tx_buffer = smoltcp::socket::udp::PacketBuffer::new( &mut storage.tx_metadata[..], &mut storage.tx_storage[..], ); - smoltcp::socket::UdpSocket::new(rx_buffer, tx_buffer) + smoltcp::socket::udp::Socket::new(rx_buffer, tx_buffer) }; - interface.add_socket(udp_socket); + sockets.add(udp_socket); } - let random_seed = { - let mut rng = device.RNG.constrain(ccdr.peripheral.RNG, &ccdr.clocks); - let mut data = [0u8; 4]; - rng.fill(&mut data).unwrap(); - data - }; - - let mut stack = smoltcp_nal::NetworkStack::new(interface, clock); + let mut stack = smoltcp_nal::NetworkStack::new(interface, eth_dma, sockets, clock); stack.seed_random_port(&random_seed); @@ -590,5 +618,6 @@ pub fn setup( adc_internal, adc_sm, adc_channels, + metadata: ApplicationMetadata::new(), } } diff --git a/src/hardware/system_timer.rs b/src/hardware/system_timer.rs index aff70db..3ff6c1d 100644 --- a/src/hardware/system_timer.rs +++ b/src/hardware/system_timer.rs @@ -1,8 +1,8 @@ -///! System timer used for non-RTIC compatibility -///! -///! # Design -///! `Clock` is implemented using the RTIC `app::monotonics::now()` default `Monotonic`. -///! That `Monotonic` must tick at 1 kHz. +//! System timer used for non-RTIC compatibility +//! +//! # Design +//! `Clock` is implemented using the RTIC `app::monotonics::now()` default `Monotonic`. +//! That `Monotonic` must tick at 1 kHz. use minimq::embedded_time::{clock::Error, fraction::Fraction, Clock, Instant}; #[derive(Copy, Clone, Debug)] diff --git a/src/main.rs b/src/main.rs index 408f86f..4bc43c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,8 @@ #![no_std] #![no_main] +use core::fmt::Write; + pub mod hardware; pub mod net; pub mod output_channel; @@ -25,12 +27,13 @@ use hardware::{ system_timer::SystemTimer, OutputChannelIdx, }; -use idsp::iir; -use net::{miniconf::Miniconf, serde::Serialize, Alarm, NetworkState, NetworkUsers}; +use miniconf::Tree; +use net::{Alarm, NetworkState, NetworkUsers}; +use serde::Serialize; use statistics::{Buffer, Statistics}; use systick_monotonic::{ExtU64, Systick}; -#[derive(Clone, Debug, Miniconf)] +#[derive(Clone, Debug, Tree)] pub struct Settings { /// Specifies the telemetry output period in seconds. /// @@ -45,12 +48,12 @@ pub struct Settings { /// /// # Path /// `output_channel/` - /// * specifies which channel to configure. := [0, 1, 2, 3] + /// * ` := [0, 1, 2, 3]` specifies which channel to configure. /// /// # Value /// See [output_channel::OutputChannel] - #[miniconf(defer)] - output_channel: miniconf::Array, + #[tree(depth(4))] + output_channel: [output_channel::OutputChannel; 4], /// Alarm settings. /// @@ -59,7 +62,7 @@ pub struct Settings { /// /// # Value /// See [Alarm] - #[miniconf(defer)] + #[tree(depth(3))] alarm: Alarm, } @@ -67,11 +70,8 @@ impl Default for Settings { fn default() -> Self { Self { telemetry_period: 1.0, - output_channel: [Default::default(); 4].into(), - alarm: Alarm { - period: 1.0, - ..Default::default() - }, + output_channel: Default::default(), + alarm: Default::default(), } } } @@ -89,20 +89,20 @@ pub struct Monitor { output_current: [f32; 4], /// Measurement of the output voltages. output_voltage: [f32; 4], - /// See [PoEPower] + /// See [PoePower] poe: PoePower, /// Overtemperature status. overtemp: bool, } /// Thermostat-EEM Telemetry. -#[derive(Serialize, Copy, Clone, Default, Debug)] +#[derive(Serialize, Copy, Clone, Debug, Default)] pub struct Telemetry { /// see [Monitor] monitor: Monitor, - /// [][] array of [Statistics]. 'None' for disabled channels. + /// `[][]` array of [Statistics]. `None` for disabled channels. statistics: [[Option; 4]; 4], - /// Alarm status for each enabled input channel. 'None' for disabled channels. + /// Alarm status for each enabled input channel. `None` for disabled channels. alarm: [[Option; 4]; 4], /// Output current in Amperes for each Thermostat output channel. output_current: [f32; 4], @@ -114,9 +114,10 @@ mod app { #[monotonic(binds = SysTick, default = true)] type Mono = Systick<1_000>; // 1ms resolution + #[shared] struct Shared { - network: NetworkUsers, + network: NetworkUsers, settings: Settings, telemetry: Telemetry, gpio: Gpio, @@ -130,81 +131,75 @@ mod app { dac: Dac, pwm: Pwm, adc_internal: AdcInternal, - iir_state: [iir::Vec5; 4], + iir_state: [[f64; 4]; 4], } #[init] fn init(c: init::Context) -> (Shared, Local, init::Monotonics) { // Initialize monotonic - let systick = c.core.SYST; let clock = SystemTimer::new(|| monotonics::now().ticks()); // setup Thermostat hardware let thermostat = hardware::setup::setup(c.device, clock); - let mono = Systick::new(systick, thermostat.clocks.sysclk().to_Hz()); - - let local = Local { - adc_sm: thermostat.adc_sm, - pwm: thermostat.pwm, - adc_internal: thermostat.adc_internal, - iir_state: Default::default(), - dac: thermostat.dac, - }; + let settings = Settings::default(); - let mut settings = Settings::default(); - - // Initialize enabled temperatures, statistics buffers and alarm. - let mut telemetry = Telemetry::default(); - for phy_i in 0..4 { - for ch_i in 0..4 { - if thermostat.adc_channels[phy_i][ch_i] { - telemetry.alarm[phy_i][ch_i] = Some(false); - telemetry.statistics[phy_i][ch_i] = Some(Default::default()); - settings.alarm.temperature_limits[phy_i][ch_i] = Some([f32::MIN, f32::MAX]); - // Initialize the output weights. - for ch in settings.output_channel.iter_mut() { - ch.weights[phy_i][ch_i] = Some(0.); - } - } - } - } + let mut id = heapless::String::<64>::new(); + write!( + &mut id, + "{}-{}", + thermostat.metadata.app, thermostat.net.mac_address + ) + .unwrap(); let network = NetworkUsers::new( thermostat.net.stack, thermostat.net.phy, clock, - env!("CARGO_BIN_NAME"), - thermostat.net.mac_address, - option_env!("BROKER") - .unwrap_or("10.42.0.1") - .parse() - .unwrap(), + &id, + option_env!("BROKER").unwrap_or("mqtt"), settings.clone(), + thermostat.metadata, ); - settings_update::spawn(settings.clone()).unwrap(); - ethernet_link::spawn().unwrap(); - telemetry_task::spawn().unwrap(); - mqtt_alarm::spawn().unwrap(); + let local = Local { + adc_sm: thermostat.adc_sm, + pwm: thermostat.pwm, + adc_internal: thermostat.adc_internal, + iir_state: Default::default(), + dac: thermostat.dac, + }; let shared = Shared { network, - settings, - telemetry, + settings: settings.clone(), + telemetry: Default::default(), gpio: thermostat.gpio, temperature: Default::default(), statistics_buff: Default::default(), }; - (shared, local, init::Monotonics(mono)) + // Apply initial settings + settings_update::spawn(settings).unwrap(); + ethernet_link::spawn().unwrap(); + telemetry_task::spawn().unwrap(); + mqtt_alarm::spawn().unwrap(); + + ( + shared, + local, + init::Monotonics(Systick::new( + c.core.SYST, + thermostat.clocks.sysclk().to_Hz(), + )), + ) } #[idle(shared=[network])] fn idle(mut c: idle::Context) -> ! { loop { c.shared.network.lock(|net| match net.update() { - NetworkState::SettingsChanged => { + NetworkState::SettingsChanged(_path) => { settings_update::spawn(net.miniconf.settings().clone()).unwrap() } NetworkState::Updated => {} @@ -217,14 +212,16 @@ mod app { fn settings_update(mut c: settings_update::Context, mut settings: Settings) { // Limit y_min and y_max values here. Will be incorporated into miniconf response later. for ch in settings.output_channel.iter_mut() { - ch.iir.y_max = ch - .iir - .y_max - .clamp(-DacCode::MAX_CURRENT as _, DacCode::MAX_CURRENT as _); - ch.iir.y_min = ch - .iir - .y_min - .clamp(-DacCode::MAX_CURRENT as _, DacCode::MAX_CURRENT as _); + ch.iir.set_max( + ch.iir + .max() + .clamp(-DacCode::MAX_CURRENT as _, DacCode::MAX_CURRENT as _), + ); + ch.iir.set_min( + ch.iir + .min() + .clamp(-DacCode::MAX_CURRENT as _, DacCode::MAX_CURRENT as _), + ); } let pwm = c.local.pwm; @@ -271,10 +268,9 @@ mod app { // Finalize temperature telemetry and reset buffer for phy_i in 0..4 { for cfg_i in 0..4 { - let stat = &mut telemetry.statistics[phy_i][cfg_i]; - if stat.is_some() { + if let Some(stat) = &mut telemetry.statistics[phy_i][cfg_i] { c.shared.statistics_buff.lock(|buff| { - *stat = Some(buff[phy_i][cfg_i].into()); + *stat = buff[phy_i][cfg_i].into(); buff[phy_i][cfg_i] = Default::default(); }); } @@ -295,11 +291,11 @@ mod app { let alarm = c.shared.settings.lock(|settings| settings.alarm.clone()); if alarm.armed { let temperatures = c.shared.temperature.lock(|temp| *temp); - let mut alarms: [[Option; 4]; 4] = Default::default(); + let mut alarms = [[None; 4]; 4]; let mut alarm_state = false; for phy_i in 0..4 { for cfg_i in 0..4 { - if let Some(l) = alarm.temperature_limits[phy_i][cfg_i] { + if let Some(l) = &alarm.temperature_limits[phy_i][cfg_i] { let a = !(l[0]..l[1]).contains(&(temperatures[phy_i][cfg_i] as _)); alarms[phy_i][cfg_i] = Some(a); alarm_state |= a; @@ -331,7 +327,7 @@ mod app { fn process_output_channel(mut c: process_output_channel::Context, output_ch: OutputChannelIdx) { let idx = output_ch as usize; let current = (c.shared.settings, c.shared.temperature).lock(|settings, temperature| { - settings.output_channel[idx].update(temperature, &mut c.local.iir_state[idx], false) + settings.output_channel[idx].update(temperature, &mut c.local.iir_state[idx]) }); c.shared .telemetry diff --git a/src/net/mod.rs b/src/net/mod.rs index 41d1f85..41a2437 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -1,10 +1,10 @@ -///! Stabilizer network management module -///! -///! # Design -///! The stabilizer network architecture supports numerous layers to permit transmission of -///! telemetry (via MQTT), configuration of run-time settings (via MQTT + Miniconf), and live data -///! streaming over raw UDP/TCP sockets. This module encompasses the main processing routines -///! related to Stabilizer networking operations. +//! Stabilizer network management module +//! +//! # Design +//! The stabilizer network architecture supports numerous layers to permit transmission of +//! telemetry (via MQTT), configuration of run-time settings (via MQTT + Miniconf), and live data +//! streaming over raw UDP/TCP sockets. This module encompasses the main processing routines +//! related to Stabilizer networking operations. pub use heapless; pub use miniconf; pub use serde; @@ -12,20 +12,33 @@ pub use serde; pub mod network_processor; pub mod telemetry; -use crate::hardware::{system_timer::SystemTimer, EthernetPhy, NetworkManager, NetworkStack}; -use minimq::embedded_nal::IpAddr; +use crate::hardware::{ + metadata::ApplicationMetadata, system_timer::SystemTimer, EthernetPhy, NetworkManager, + NetworkStack, +}; use network_processor::NetworkProcessor; use telemetry::TelemetryClient; use core::fmt::Write; use heapless::String; -use miniconf::Miniconf; +use miniconf::{JsonCoreSlash, Tree}; use serde::Serialize; pub type NetworkReference = smoltcp_nal::shared::NetworkStackProxy<'static, NetworkStack>; -/// The default MQTT broker IP address if unspecified. -pub const DEFAULT_MQTT_BROKER: [u8; 4] = [10, 34, 16, 10]; +pub struct MqttStorage { + telemetry: [u8; 2048], + settings: [u8; 1024], +} + +impl Default for MqttStorage { + fn default() -> Self { + Self { + telemetry: [0u8; 2048], + settings: [0u8; 1024], + } + } +} #[derive(Copy, Clone, PartialEq, Eq)] pub enum UpdateState { @@ -33,22 +46,32 @@ pub enum UpdateState { Updated, } -#[derive(Copy, Clone, PartialEq, Eq)] pub enum NetworkState { - SettingsChanged, + SettingsChanged(String<128>), Updated, NoChange, } /// A structure of Stabilizer's default network users. -pub struct NetworkUsers { - pub miniconf: miniconf::MqttClient, +pub struct NetworkUsers +where + for<'de> S: Default + JsonCoreSlash<'de, Y> + Clone, + T: Serialize, +{ + pub miniconf: miniconf::MqttClient< + 'static, + S, + NetworkReference, + SystemTimer, + miniconf::minimq::broker::NamedBroker, + Y, + >, pub processor: NetworkProcessor, pub telemetry: TelemetryClient, } -impl NetworkUsers +impl NetworkUsers where - S: Default + Miniconf + Clone, + for<'de> S: Default + JsonCoreSlash<'de, Y> + Clone, T: Serialize, { /// Construct Stabilizer's default network users. @@ -57,10 +80,11 @@ where /// * `stack` - The network stack that will be used to share with all network users. /// * `phy` - The ethernet PHY connecting the network. /// * `clock` - System timer clock. - /// * `app` - The name of the application. /// * `mac` - The MAC address of the network. /// * `broker` - The IP address of the MQTT broker to use. + /// * `id` - The MQTT client ID base to use. /// * `settings` - The initial settings value + /// * `metadata` - The metadata associated with the app. /// /// # Returns /// A new struct of network users. @@ -68,35 +92,53 @@ where stack: NetworkStack, phy: EthernetPhy, clock: SystemTimer, - app: &str, - mac: smoltcp_nal::smoltcp::wire::EthernetAddress, - broker: IpAddr, + id: &str, + broker: &str, settings: S, + metadata: &'static ApplicationMetadata, ) -> Self { let stack_manager = cortex_m::singleton!(: NetworkManager = NetworkManager::new(stack)).unwrap(); let processor = NetworkProcessor::new(stack_manager.acquire_stack(), phy); - let prefix = get_device_prefix(app, mac); + let prefix = get_device_prefix(metadata.app, id); + + let store = cortex_m::singleton!(: MqttStorage = MqttStorage::default()).unwrap(); + + let named_broker = + miniconf::minimq::broker::NamedBroker::new(broker, stack_manager.acquire_stack()) + .unwrap(); let settings = miniconf::MqttClient::new( stack_manager.acquire_stack(), - &get_client_id(app, "settings", mac), &prefix, - broker, clock, settings, + miniconf::minimq::ConfigBuilder::new(named_broker, &mut store.settings) + .client_id(&get_client_id(id, "settings")) + .unwrap(), ) .unwrap(); - let telemetry = TelemetryClient::new( - stack_manager.acquire_stack(), - clock, - &get_client_id(app, "tlm", mac), - &prefix, - broker, - ); + let telemetry = { + let named_broker = + miniconf::minimq::broker::NamedBroker::new(broker, stack_manager.acquire_stack()) + .unwrap(); + + let mqtt = minimq::Minimq::new( + stack_manager.acquire_stack(), + clock, + minimq::ConfigBuilder::new(named_broker, &mut store.telemetry) + // The telemetry client doesn't receive any messages except MQTT control packets. + // As such, we don't need much of the buffer for RX. + .rx_buffer(minimq::config::BufferConfig::Maximum(100)) + .client_id(&get_client_id(id, "tlm")) + .unwrap(), + ); + + TelemetryClient::new(mqtt, &prefix, metadata) + }; NetworkUsers { miniconf: settings, @@ -119,8 +161,13 @@ where UpdateState::Updated => NetworkState::Updated, }; - match self.miniconf.update() { - Ok(true) => NetworkState::SettingsChanged, + let mut settings_path: String<128> = String::new(); + match self.miniconf.handled_update(|path, old, new| { + settings_path = path.into(); + *old = new.clone(); + Result::<(), &'static str>::Ok(()) + }) { + Ok(true) => NetworkState::SettingsChanged(settings_path), _ => poll_result, } } @@ -129,19 +176,14 @@ where /// Get an MQTT client ID for a client. /// /// # Args -/// * `app` - The name of the application -/// * `client` - The unique tag of the client -/// * `mac` - The MAC address of the device. +/// * `id` - The base client ID +/// * `mode` - The operating mode of the client. (i.e. tlm, settings) /// /// # Returns /// A client ID that may be used for MQTT client identification. -fn get_client_id( - app: &str, - client: &str, - mac: smoltcp_nal::smoltcp::wire::EthernetAddress, -) -> String<64> { +fn get_client_id(id: &str, mode: &str) -> String<64> { let mut identifier = String::new(); - write!(&mut identifier, "{}-{}-{}", app, mac, client).unwrap(); + write!(&mut identifier, "{id}-{mode}").unwrap(); identifier } @@ -149,35 +191,32 @@ fn get_client_id( /// /// # Args /// * `app` - The name of the application that is executing. -/// * `mac` - The ethernet MAC address of the device. +/// * `id` - The MQTT ID of the device. /// /// # Returns /// The MQTT prefix used for this device. -pub fn get_device_prefix( - app: &str, - mac: smoltcp_nal::smoltcp::wire::EthernetAddress, -) -> String<128> { +pub fn get_device_prefix(app: &str, id: &str) -> String<128> { // Note(unwrap): The mac address + binary name must be short enough to fit into this string. If // they are defined too long, this will panic and the device will fail to boot. let mut prefix: String<128> = String::new(); - write!(&mut prefix, "dt/sinara/{}/{}", app, mac).unwrap(); + write!(&mut prefix, "dt/sinara/{app}/{id}").unwrap(); prefix } /// Miniconf settings for the MQTT alarm. -/// The alarm simply publishes "false" onto its [target] as long as all the channels are -/// within their [temperature_limits] (aka logical OR of all channels). +/// The alarm simply publishes "false" onto its `target` as long as all the channels are +/// within their `temperature_limits`` (aka logical OR of all channels). /// Otherwise it publishes "true" (aka true, there is an alarm). /// -/// The publishing interval is given by [period_ms]. +/// The publishing interval is given by `period_ms`. /// /// The alarm is non-latching. If alarm was "true" for a while and the temperatures come within /// limits again, alarm will be "false" again. -#[derive(Clone, Debug, Miniconf, Default)] +#[derive(Clone, Debug, Tree)] pub struct Alarm { /// Set the alarm to armed (true) or disarmed (false). - /// If the alarm is armed, the device will publish it's alarm state onto the [target]. + /// If the alarm is armed, the device will publish it's alarm state onto the `target`. /// /// # Value /// True to arm, false to disarm. @@ -206,11 +245,22 @@ pub struct Alarm { /// /// # Path /// `temperature_limits//` - /// * specifies which adc to configure. := [0, 1, 2, 3] - /// * specifies which channel of an ADC to configure. Only the enabled channels for the specific ADC are available. + /// * ` := [0, 1, 2, 3]` specifies which adc to configure. + /// * `` specifies which channel of an ADC to configure. Only the enabled channels for the specific ADC are available. /// /// # Value - /// [f32, f32] - #[miniconf(defer)] - pub temperature_limits: miniconf::Array, 4>, 4>, + /// `[f32, f32]` or `None` + #[tree(depth(2))] + pub temperature_limits: [[Option<[f32; 2]>; 4]; 4], +} + +impl Default for Alarm { + fn default() -> Self { + Self { + armed: false, + target: Default::default(), + period: 1.0, + temperature_limits: Default::default(), + } + } } diff --git a/src/net/network_processor.rs b/src/net/network_processor.rs index a5254b3..a557387 100644 --- a/src/net/network_processor.rs +++ b/src/net/network_processor.rs @@ -1,8 +1,8 @@ -///! Task to process network hardware. -///! -///! # Design -///! The network processir is a small taks to regularly process incoming data over ethernet, handle -///! the ethernet PHY state, and reset the network as appropriate. +//! Task to process network hardware. +//! +//! # Design +//! The network processir is a small taks to regularly process incoming data over ethernet, handle +//! the ethernet PHY state, and reset the network as appropriate. use super::{NetworkReference, UpdateState}; use crate::hardware::EthernetPhy; diff --git a/src/net/telemetry.rs b/src/net/telemetry.rs index 2d37b2e..c307ab7 100644 --- a/src/net/telemetry.rs +++ b/src/net/telemetry.rs @@ -1,26 +1,35 @@ -///! Thermostat Telemetry Capabilities -///! -///! # Design -///! Telemetry is reported regularly using an MQTT client. All telemetry is reported in SI units -///! using standard JSON format. -///! -///! In order to report ADC/DAC codes generated during the DSP routines, a telemetry buffer is -///! employed to track the latest codes. Converting these codes to SI units would result in -///! repetitive and unnecessary calculations within the DSP routine, slowing it down and limiting -///! sampling frequency. Instead, the raw codes are stored and the telemetry is generated as -///! required immediately before transmission. This ensures that any slower computation required -///! for unit conversion can be off-loaded to lower priority tasks. -use heapless::{String, Vec}; +//! Thermostat Telemetry Capabilities +//! +//! # Design +//! Telemetry is reported regularly using an MQTT client. All telemetry is reported in SI units +//! using standard JSON format. +//! +//! In order to report ADC/DAC codes generated during the DSP routines, a telemetry buffer is +//! employed to track the latest codes. Converting these codes to SI units would result in +//! repetitive and unnecessary calculations within the DSP routine, slowing it down and limiting +//! sampling frequency. Instead, the raw codes are stored and the telemetry is generated as +//! required immediately before transmission. This ensures that any slower computation required +//! for unit conversion can be off-loaded to lower priority tasks. +use heapless::String; use serde::Serialize; use super::NetworkReference; -use crate::hardware::system_timer::SystemTimer; -use minimq::embedded_nal::IpAddr; +use crate::hardware::{metadata::ApplicationMetadata, system_timer::SystemTimer}; + +/// Default metadata message if formatting errors occur. +const DEFAULT_METADATA: &str = "{\"message\":\"Truncated\"}"; /// The telemetry client for reporting telemetry data over MQTT. pub struct TelemetryClient { - mqtt: minimq::Minimq, - telemetry_topic: String<128>, + mqtt: minimq::Minimq< + 'static, + NetworkReference, + SystemTimer, + minimq::broker::NamedBroker, + >, + prefix: String<128>, + meta_published: bool, + metadata: &'static ApplicationMetadata, _telemetry: core::marker::PhantomData, } @@ -28,30 +37,27 @@ impl TelemetryClient { /// Construct a new telemetry client. /// /// # Args - /// * `stack` - A reference to the (shared) underlying network stack. - /// * `clock` - System timer clock. - /// * `client_id` - The MQTT client ID of the telemetry client. - /// * `prefix` - The device prefix to use for MQTT telemetry reporting. - /// * `broker` - The IP address of the MQTT broker to use. + /// * `mqtt` - The MQTT client + /// * `prefix` - The device prefix to use for MQTT telemetry reporting /// /// # Returns /// A new telemetry client. pub fn new( - stack: NetworkReference, - clock: SystemTimer, - client_id: &str, + mqtt: minimq::Minimq< + 'static, + NetworkReference, + SystemTimer, + minimq::broker::NamedBroker, + >, prefix: &str, - broker: IpAddr, + metadata: &'static ApplicationMetadata, ) -> Self { - let mqtt = minimq::Minimq::new(broker, client_id, stack, clock).unwrap(); - - let mut telemetry_topic: String<128> = String::from(prefix); - telemetry_topic.push_str("/telemetry").unwrap(); - Self { mqtt, - telemetry_topic, - _telemetry: core::marker::PhantomData::default(), + prefix: String::from(prefix), + meta_published: false, + metadata, + _telemetry: core::marker::PhantomData, } } @@ -64,12 +70,14 @@ impl TelemetryClient { /// # Args /// * `telemetry` - The telemetry to report pub fn publish(&mut self, telemetry: &T) { - let telemetry: Vec = serde_json_core::to_vec(telemetry).unwrap(); + let mut topic = self.prefix.clone(); + topic.push_str("/telemetry").unwrap(); + self.mqtt .client() .publish( - minimq::Publication::new(&telemetry) - .topic(&self.telemetry_topic) + minimq::DeferredPublication::new(|buf| serde_json_core::to_slice(&telemetry, buf)) + .topic(&topic) .finish() .unwrap(), ) @@ -77,12 +85,12 @@ impl TelemetryClient { } /// A secondary functionality tugged onto the telemetry client that publishes onto another - /// [alarm_topic]. + /// `alarm_topic`. pub fn publish_alarm(&mut self, alarm_topic: &String<128>, alarm: &bool) { self.mqtt .client() .publish( - minimq::Publication::new(&serde_json_core::to_vec::(alarm).unwrap()) + minimq::DeferredPublication::new(|buf| serde_json_core::to_slice(alarm, buf)) .topic(alarm_topic) .finish() .unwrap(), @@ -98,10 +106,55 @@ impl TelemetryClient { /// should be called regularly. pub fn update(&mut self) { match self.mqtt.poll(|_client, _topic, _message, _properties| {}) { - Err(minimq::Error::Network(smoltcp_nal::NetworkError::NoIpAddress)) => {} + Err(minimq::Error::Network(smoltcp_nal::NetworkError::TcpConnectionFailure( + smoltcp_nal::smoltcp::socket::tcp::ConnectError::Unaddressable, + ))) => {} Err(error) => log::info!("Unexpected error: {:?}", error), _ => {} } + + if !self.mqtt.client().is_connected() { + self.meta_published = false; + return; + } + + // Publish application metadata + if !self.meta_published && self.mqtt.client().can_publish(minimq::QoS::AtMostOnce) { + let Self { + ref mut mqtt, + metadata, + .. + } = self; + + let mut topic = self.prefix.clone(); + topic.push_str("/meta").unwrap(); + + if mqtt + .client() + .publish( + minimq::DeferredPublication::new(|buf| { + serde_json_core::to_slice(&metadata, buf) + }) + .topic(&topic) + .finish() + .unwrap(), + ) + .is_err() + { + // Note(unwrap): We can guarantee that this message will be sent because we checked + // for ability to publish above. + mqtt.client() + .publish( + minimq::Publication::new(DEFAULT_METADATA.as_bytes()) + .topic(&topic) + .finish() + .unwrap(), + ) + .unwrap(); + } + + self.meta_published = true; + } } } diff --git a/src/output_channel.rs b/src/output_channel.rs index cc38bd0..934d0c8 100644 --- a/src/output_channel.rs +++ b/src/output_channel.rs @@ -3,10 +3,10 @@ use crate::hardware::pwm::Pwm; use idsp::iir; -use miniconf::Miniconf; +use miniconf::Tree; use num_traits::Signed; -#[derive(Copy, Clone, Debug, Miniconf)] +#[derive(Copy, Clone, Debug, Tree)] pub struct OutputChannel { /// En-/Disables the TEC driver. This implies "hold". /// @@ -31,8 +31,8 @@ pub struct OutputChannel { /// The y limits will be clamped to the maximum output current of +-3 A. /// /// # Value - /// See [iir::IIR#miniconf] - pub iir: iir::IIR, + /// See [iir::Biquad] + pub iir: iir::Biquad, /// Thermostat input channel weights. Each input temperature of an enabled channel /// is multiplied by its weight and the accumulated output is fed into the IIR. @@ -40,13 +40,13 @@ pub struct OutputChannel { /// /// # Path /// `weights//` - /// * specifies which adc to configure. := [0, 1, 2, 3] - /// * specifies which channel of an ADC to configure. Only the enabled channels for the specific ADC are available. + /// * ` := [0, 1, 2, 3]` specifies which adc to configure. + /// * `` specifies which channel of an ADC to configure. Only the enabled channels for the specific ADC are available. /// /// # Value /// f32 - #[miniconf(defer)] - pub weights: miniconf::Array, 4>, 4>, + #[tree(depth(2))] + pub weights: [[f32; 4]; 4], } impl Default for OutputChannel { @@ -55,39 +55,30 @@ impl Default for OutputChannel { shutdown: true, hold: false, voltage_limit: 0.0, - iir: iir::IIR::default(), - weights: miniconf::Array::default(), + iir: Default::default(), + weights: [[0.0; 4]; 4], } } } -// Global "hold" IIR to apply to a channel iir state [x0,x1,x2,y0,y1] when the output should hold. -const IIR_HOLD: iir::IIR = iir::IIR { - ba: [0., 0., 0., 1., 0.], - y_offset: 0., - y_min: f64::MIN, - y_max: f64::MAX, -}; - impl OutputChannel { /// compute weighted iir input, iir state and return the new output pub fn update( &mut self, channel_temperatures: &[[f64; 4]; 4], - iir_state: &mut iir::Vec5, - hold: bool, + iir_state: &mut [f64; 4], ) -> f32 { let weighted_temperature = channel_temperatures .iter() .flatten() .zip(self.weights.iter().flatten()) // weight is `None` if temperature is invalid (phy cfg absent) - .map(|(t, w)| t * w.unwrap_or(0.) as f64) + .map(|(t, w)| t * *w as f64) .sum(); if self.shutdown || self.hold { - IIR_HOLD.update(iir_state, weighted_temperature, hold) as f32 + iir::Biquad::HOLD.update(iir_state, weighted_temperature) as f32 } else { - self.iir.update(iir_state, weighted_temperature, hold) as f32 + self.iir.update(iir_state, weighted_temperature) as f32 } } @@ -96,33 +87,33 @@ impl OutputChannel { /// - Normalization of the weights /// Returns the current limits. pub fn finalize_settings(&mut self) -> [f32; 2] { - self.iir.y_max = self - .iir - .y_max - .clamp(-Pwm::MAX_CURRENT_LIMIT, Pwm::MAX_CURRENT_LIMIT); - self.iir.y_min = self - .iir - .y_min - .clamp(-Pwm::MAX_CURRENT_LIMIT, Pwm::MAX_CURRENT_LIMIT); + self.iir.set_max( + self.iir + .max() + .clamp(-Pwm::MAX_CURRENT_LIMIT, Pwm::MAX_CURRENT_LIMIT), + ); + self.iir.set_min( + self.iir + .min() + .clamp(-Pwm::MAX_CURRENT_LIMIT, Pwm::MAX_CURRENT_LIMIT), + ); self.voltage_limit = self.voltage_limit.clamp(0.0, Pwm::MAX_VOLTAGE_LIMIT); let divisor: f32 = self .weights .iter() - .map(|w| w.iter().map(|w| w.unwrap_or(0.).abs()).sum::()) + .map(|w| w.iter().map(|w| w.abs()).sum::()) .sum(); // Note: The weights which are not 'None' should always affect an enabled channel and therefore count for normalization. if divisor != 0.0 { - self.weights.iter_mut().flatten().for_each(|w| { - if let Some(w) = w { - *w /= divisor - } - }); + for w in self.weights.iter_mut().flatten() { + *w /= divisor; + } } [ // [Pwm::MAX_CURRENT_LIMIT] + 5% is still below 100% duty cycle for the PWM limits and therefore OK. // Might not be OK for a different shunt resistor or different PWM setup. - (self.iir.y_max + 0.05 * Pwm::MAX_CURRENT_LIMIT).max(0.) as f32, - (self.iir.y_min - 0.05 * Pwm::MAX_CURRENT_LIMIT).min(0.) as f32, + (self.iir.max() + 0.05 * Pwm::MAX_CURRENT_LIMIT).max(0.) as f32, + (self.iir.min() - 0.05 * Pwm::MAX_CURRENT_LIMIT).min(0.) as f32, ] } }