commit f764ef0581c622626574581f551227744455c502
parent 3e10c5cd7da55cb8bc3542d973f8334a8262b562
Author: Bradley Gannon <bradley@bradleygannon.com>
Date: Fri, 29 May 2026 18:39:04 -0400
Split Goertzel and demod functions into separate crate to allow host-side testing
Diffstat:
25 files changed, 1892 insertions(+), 1445 deletions(-)
diff --git a/.cargo/config.toml b/.cargo/config.toml
@@ -1,9 +0,0 @@
-[build]
-target = "avr-none"
-rustflags = ["-C", "target-cpu=atmega328p"]
-
-[target.'cfg(target_arch = "avr")']
-runner = "ravedude"
-
-[unstable]
-build-std = ["core"]
diff --git a/.gitignore b/.gitignore
@@ -1 +1 @@
-/target
+**/target
diff --git a/Cargo.lock b/Cargo.lock
@@ -1,1080 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 4
-
-[[package]]
-name = "aho-corasick"
-version = "1.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
-dependencies = [
- "memchr",
-]
-
-[[package]]
-name = "anyhow"
-version = "1.0.101"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
-
-[[package]]
-name = "arduino-hal"
-version = "0.1.0"
-source = "git+https://github.com/rahix/avr-hal?rev=e5c8f37fe48419956e722490a82b9ca9b9fc61a2#e5c8f37fe48419956e722490a82b9ca9b9fc61a2"
-dependencies = [
- "atmega-hal",
- "avr-device",
- "avr-hal-generic",
- "cfg-if",
- "embedded-hal 1.0.0",
- "ufmt",
-]
-
-[[package]]
-name = "arduino-tnc"
-version = "0.1.0"
-dependencies = [
- "arduino-hal",
- "avr-device",
- "crc",
- "embedded-hal 1.0.0",
- "heapless",
- "nb 1.1.0",
- "panic-halt",
- "trig-const",
- "ufmt",
-]
-
-[[package]]
-name = "arraydeque"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
-
-[[package]]
-name = "atdf2svd"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "179e5f106191a4d02437d8f66ed06f852e1a85774db29f934251d33cfec219fa"
-dependencies = [
- "cfg-if",
- "log",
- "svd-encoder",
- "svd-rs",
- "xmltree 0.10.3",
-]
-
-[[package]]
-name = "atmega-hal"
-version = "0.1.0"
-source = "git+https://github.com/rahix/avr-hal?rev=e5c8f37fe48419956e722490a82b9ca9b9fc61a2#e5c8f37fe48419956e722490a82b9ca9b9fc61a2"
-dependencies = [
- "avr-device",
- "avr-hal-generic",
-]
-
-[[package]]
-name = "avr-device"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0584fb409e2ff23423979617c6b7ab34628727c8ef8026b665d41f41114060b9"
-dependencies = [
- "anyhow",
- "atdf2svd",
- "avr-device-macros",
- "bare-metal",
- "cfg-if",
- "critical-section",
- "prettyplease",
- "svd-rs",
- "svd2rust",
- "svdtools",
- "syn 2.0.110",
- "vcell",
- "yaml-rust2",
-]
-
-[[package]]
-name = "avr-device-macros"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "129341bd5b981a038dd2ae0963550bfcb67ad56c64b53aa462cf1a0764405e97"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "avr-hal-generic"
-version = "0.1.0"
-source = "git+https://github.com/rahix/avr-hal?rev=e5c8f37fe48419956e722490a82b9ca9b9fc61a2#e5c8f37fe48419956e722490a82b9ca9b9fc61a2"
-dependencies = [
- "avr-device",
- "embedded-hal 0.2.7",
- "embedded-hal 1.0.0",
- "embedded-hal-bus",
- "embedded-storage",
- "nb 1.1.0",
- "paste",
- "ufmt",
- "unwrap-infallible",
-]
-
-[[package]]
-name = "bare-metal"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8fe8f5a8a398345e52358e18ff07cc17a568fbca5c6f73873d3a62056309603"
-
-[[package]]
-name = "bstr"
-version = "1.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
-dependencies = [
- "memchr",
- "serde",
-]
-
-[[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.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
-
-[[package]]
-name = "commands"
-version = "0.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "688226b9769bbf11a9d82a94fb4adda15793a6023d33a8ca7695dbafec6f4123"
-
-[[package]]
-name = "convert_case"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
-dependencies = [
- "unicode-segmentation",
-]
-
-[[package]]
-name = "crc"
-version = "3.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
-dependencies = [
- "crc-catalog",
-]
-
-[[package]]
-name = "crc-catalog"
-version = "2.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
-
-[[package]]
-name = "critical-section"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
-
-[[package]]
-name = "displaydoc"
-version = "0.2.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.110",
-]
-
-[[package]]
-name = "either"
-version = "1.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
-
-[[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-bus"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57b4e6ede84339ebdb418cd986e6320a34b017cdf99b5cc3efceec6450b06886"
-dependencies = [
- "critical-section",
- "embedded-hal 1.0.0",
-]
-
-[[package]]
-name = "embedded-storage"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "723dce4e9f25b6e6c5f35628e144794e5b459216ed7da97b7c4b66cdb3fa82ca"
-
-[[package]]
-name = "encoding_rs"
-version = "0.8.35"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
-name = "equivalent"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
-
-[[package]]
-name = "foldhash"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
-
-[[package]]
-name = "form_urlencoded"
-version = "1.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
-dependencies = [
- "percent-encoding",
-]
-
-[[package]]
-name = "globset"
-version = "0.4.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
-dependencies = [
- "aho-corasick",
- "bstr",
- "log",
- "regex-automata",
- "regex-syntax",
-]
-
-[[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.15.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
-dependencies = [
- "foldhash",
-]
-
-[[package]]
-name = "hashbrown"
-version = "0.16.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
-
-[[package]]
-name = "hashlink"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
-dependencies = [
- "hashbrown 0.15.5",
-]
-
-[[package]]
-name = "heapless"
-version = "0.9.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed"
-dependencies = [
- "hash32",
- "stable_deref_trait",
-]
-
-[[package]]
-name = "icu_collections"
-version = "2.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
-dependencies = [
- "displaydoc",
- "potential_utf",
- "yoke",
- "zerofrom",
- "zerovec",
-]
-
-[[package]]
-name = "icu_locale_core"
-version = "2.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
-dependencies = [
- "displaydoc",
- "litemap",
- "tinystr",
- "writeable",
- "zerovec",
-]
-
-[[package]]
-name = "icu_normalizer"
-version = "2.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
-dependencies = [
- "icu_collections",
- "icu_normalizer_data",
- "icu_properties",
- "icu_provider",
- "smallvec",
- "zerovec",
-]
-
-[[package]]
-name = "icu_normalizer_data"
-version = "2.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
-
-[[package]]
-name = "icu_properties"
-version = "2.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
-dependencies = [
- "icu_collections",
- "icu_locale_core",
- "icu_properties_data",
- "icu_provider",
- "zerotrie",
- "zerovec",
-]
-
-[[package]]
-name = "icu_properties_data"
-version = "2.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
-
-[[package]]
-name = "icu_provider"
-version = "2.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
-dependencies = [
- "displaydoc",
- "icu_locale_core",
- "writeable",
- "yoke",
- "zerofrom",
- "zerotrie",
- "zerovec",
-]
-
-[[package]]
-name = "idna"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
-dependencies = [
- "idna_adapter",
- "smallvec",
- "utf8_iter",
-]
-
-[[package]]
-name = "idna_adapter"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
-dependencies = [
- "icu_normalizer",
- "icu_properties",
-]
-
-[[package]]
-name = "indexmap"
-version = "2.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
-dependencies = [
- "equivalent",
- "hashbrown 0.16.1",
-]
-
-[[package]]
-name = "inflections"
-version = "1.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a"
-
-[[package]]
-name = "itertools"
-version = "0.14.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
-dependencies = [
- "either",
-]
-
-[[package]]
-name = "litemap"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
-
-[[package]]
-name = "log"
-version = "0.4.29"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
-
-[[package]]
-name = "memchr"
-version = "2.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
-
-[[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 = "normpath"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf23ab2b905654b4cb177e30b629937b3868311d4e1cba859f899c041046e69b"
-dependencies = [
- "windows-sys",
-]
-
-[[package]]
-name = "once_cell"
-version = "1.21.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
-
-[[package]]
-name = "panic-halt"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a513e167849a384b7f9b746e517604398518590a9142f4846a32e3c2a4de7b11"
-
-[[package]]
-name = "paste"
-version = "1.0.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
-
-[[package]]
-name = "percent-encoding"
-version = "2.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
-
-[[package]]
-name = "phf"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
-dependencies = [
- "phf_macros",
- "phf_shared",
-]
-
-[[package]]
-name = "phf_generator"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
-dependencies = [
- "phf_shared",
- "rand",
-]
-
-[[package]]
-name = "phf_macros"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
-dependencies = [
- "phf_generator",
- "phf_shared",
- "proc-macro2",
- "quote",
- "syn 2.0.110",
-]
-
-[[package]]
-name = "phf_shared"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
-dependencies = [
- "siphasher",
-]
-
-[[package]]
-name = "potential_utf"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
-dependencies = [
- "zerovec",
-]
-
-[[package]]
-name = "prettyplease"
-version = "0.2.37"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
-dependencies = [
- "proc-macro2",
- "syn 2.0.110",
-]
-
-[[package]]
-name = "proc-macro2"
-version = "1.0.106"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "quick-xml"
-version = "0.37.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
-dependencies = [
- "memchr",
- "serde",
-]
-
-[[package]]
-name = "quote"
-version = "1.0.44"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
-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 = "regex"
-version = "1.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-automata",
- "regex-syntax",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.4.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-syntax",
-]
-
-[[package]]
-name = "regex-syntax"
-version = "0.8.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
-
-[[package]]
-name = "roxmltree"
-version = "0.20.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
-
-[[package]]
-name = "serde"
-version = "1.0.228"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
-dependencies = [
- "serde_core",
- "serde_derive",
-]
-
-[[package]]
-name = "serde_core"
-version = "1.0.228"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
-dependencies = [
- "serde_derive",
-]
-
-[[package]]
-name = "serde_derive"
-version = "1.0.228"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.110",
-]
-
-[[package]]
-name = "siphasher"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
-
-[[package]]
-name = "smallvec"
-version = "1.15.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
-
-[[package]]
-name = "stable_deref_trait"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
-
-[[package]]
-name = "svd-encoder"
-version = "0.14.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0961dac31cc1ce3e673918792e826d6bdf5e4005b197acb996b793cfc732f83"
-dependencies = [
- "convert_case",
- "svd-rs",
- "thiserror 1.0.69",
- "xmltree 0.11.0",
-]
-
-[[package]]
-name = "svd-parser"
-version = "0.14.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a41fe17e46dee363a7d3b20e878bbf6d6b4e839ae010ff07ef0e05d41d201811"
-dependencies = [
- "anyhow",
- "roxmltree",
- "svd-rs",
- "thiserror 1.0.69",
-]
-
-[[package]]
-name = "svd-rs"
-version = "0.14.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c9b88ee2e82f09623ff76965587dc15a2e7150a3126854899c86e94dab777458"
-dependencies = [
- "once_cell",
- "regex",
- "serde",
- "thiserror 1.0.69",
-]
-
-[[package]]
-name = "svd2rust"
-version = "0.37.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1eaf6a040bfbc04e7b946a6a373b24aab09e1feb875bb0e20c929946c4b7bfc4"
-dependencies = [
- "anyhow",
- "inflections",
- "log",
- "proc-macro2",
- "quote",
- "regex",
- "svd-parser",
- "svd-rs",
- "syn 2.0.110",
- "thiserror 2.0.18",
- "url",
-]
-
-[[package]]
-name = "svdtools"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6cb2e48b200a352e10c4c50056b3f0a2da8e571c317d5af47c41ba672b2e9c87"
-dependencies = [
- "anyhow",
- "commands",
- "globset",
- "hashlink",
- "itertools",
- "log",
- "normpath",
- "once_cell",
- "phf",
- "quick-xml",
- "regex",
- "serde",
- "svd-encoder",
- "svd-parser",
- "svd-rs",
- "thiserror 1.0.69",
- "yaml-rust2",
-]
-
-[[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.110"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "synstructure"
-version = "0.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.110",
-]
-
-[[package]]
-name = "thiserror"
-version = "1.0.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
-dependencies = [
- "thiserror-impl 1.0.69",
-]
-
-[[package]]
-name = "thiserror"
-version = "2.0.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
-dependencies = [
- "thiserror-impl 2.0.18",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "1.0.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.110",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "2.0.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.110",
-]
-
-[[package]]
-name = "tinystr"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
-dependencies = [
- "displaydoc",
- "zerovec",
-]
-
-[[package]]
-name = "trig-const"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffec2fe2ed1e10dac46d8530afb395cae635fa02ef03d7fff1c211e94f9059fc"
-
-[[package]]
-name = "ufmt"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a64846ec02b57e9108d6469d98d1648782ad6bb150a95a9baac26900bbeab9d"
-dependencies = [
- "ufmt-macros",
- "ufmt-write",
-]
-
-[[package]]
-name = "ufmt-macros"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d337d3be617449165cb4633c8dece429afd83f84051024079f97ad32a9663716"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "ufmt-write"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e87a2ed6b42ec5e28cc3b94c09982969e9227600b2e3dcbc1db927a84c06bd69"
-
-[[package]]
-name = "unicode-ident"
-version = "1.0.22"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
-
-[[package]]
-name = "unicode-segmentation"
-version = "1.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
-
-[[package]]
-name = "unwrap-infallible"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "151ac09978d3c2862c4e39b557f4eceee2cc72150bc4cb4f16abf061b6e381fb"
-
-[[package]]
-name = "url"
-version = "2.5.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
-dependencies = [
- "form_urlencoded",
- "idna",
- "percent-encoding",
- "serde",
- "serde_derive",
-]
-
-[[package]]
-name = "utf8_iter"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
-
-[[package]]
-name = "vcell"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002"
-
-[[package]]
-name = "void"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
-
-[[package]]
-name = "windows-link"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
-
-[[package]]
-name = "windows-sys"
-version = "0.61.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
-dependencies = [
- "windows-link",
-]
-
-[[package]]
-name = "writeable"
-version = "0.6.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
-
-[[package]]
-name = "xml-rs"
-version = "0.8.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
-
-[[package]]
-name = "xmltree"
-version = "0.10.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb"
-dependencies = [
- "xml-rs",
-]
-
-[[package]]
-name = "xmltree"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b619f8c85654798007fb10afa5125590b43b088c225a25fc2fec100a9fad0fc6"
-dependencies = [
- "indexmap",
- "xml-rs",
-]
-
-[[package]]
-name = "yaml-rust2"
-version = "0.10.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9"
-dependencies = [
- "arraydeque",
- "encoding_rs",
- "hashlink",
-]
-
-[[package]]
-name = "yoke"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
-dependencies = [
- "stable_deref_trait",
- "yoke-derive",
- "zerofrom",
-]
-
-[[package]]
-name = "yoke-derive"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.110",
- "synstructure",
-]
-
-[[package]]
-name = "zerofrom"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
-dependencies = [
- "zerofrom-derive",
-]
-
-[[package]]
-name = "zerofrom-derive"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.110",
- "synstructure",
-]
-
-[[package]]
-name = "zerotrie"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
-dependencies = [
- "displaydoc",
- "yoke",
- "zerofrom",
-]
-
-[[package]]
-name = "zerovec"
-version = "0.11.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
-dependencies = [
- "yoke",
- "zerofrom",
- "zerovec-derive",
-]
-
-[[package]]
-name = "zerovec-derive"
-version = "0.11.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.110",
-]
diff --git a/Cargo.toml b/Cargo.toml
@@ -1,39 +0,0 @@
-[package]
-name = "arduino-tnc"
-version = "0.1.0"
-authors = ["Bradley Gannon <bradley@bradleygannon.com>"]
-edition = "2021"
-license = "MIT OR Apache-2.0"
-
-
-[[bin]]
-name = "arduino-tnc"
-test = false
-bench = false
-
-[dependencies]
-avr-device = "0.8.1"
-crc = "3.4.0"
-embedded-hal = "1.0"
-heapless = "0.9.2"
-nb = "1.1.0"
-panic-halt = "1.0.0"
-trig-const = "0.4.0"
-ufmt = "0.2.0"
-
-[dependencies.arduino-hal]
-git = "https://github.com/rahix/avr-hal"
-rev = "e5c8f37fe48419956e722490a82b9ca9b9fc61a2"
-features = ["arduino-nano"]
-
-[profile.dev]
-panic = "abort"
-lto = true
-opt-level = "s"
-
-[profile.release]
-panic = "abort"
-codegen-units = 1
-debug = true
-lto = true
-opt-level = "s"
diff --git a/Ravedude.toml b/Ravedude.toml
@@ -1,4 +0,0 @@
-[general]
-board = "nano"
-open-console = true
-serial-baudrate = 57600
diff --git a/firmware/.cargo/config.toml b/firmware/.cargo/config.toml
@@ -0,0 +1,12 @@
+[build]
+target = "avr-none"
+
+[unstable]
+build-std = ["core"]
+
+[target.'cfg(target_arch = "avr")']
+runner = "ravedude"
+rustflags = ["-C", "target-cpu=atmega328p"]
+
+[alias]
+flash = "run --release --"
diff --git a/firmware/Cargo.lock b/firmware/Cargo.lock
@@ -0,0 +1,1086 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "arduino-hal"
+version = "0.1.0"
+source = "git+https://github.com/rahix/avr-hal?rev=e5c8f37fe48419956e722490a82b9ca9b9fc61a2#e5c8f37fe48419956e722490a82b9ca9b9fc61a2"
+dependencies = [
+ "atmega-hal",
+ "avr-device",
+ "avr-hal-generic",
+ "cfg-if",
+ "embedded-hal 1.0.0",
+ "ufmt",
+]
+
+[[package]]
+name = "arduino-tnc"
+version = "0.1.0"
+dependencies = [
+ "arduino-hal",
+ "avr-device",
+ "crc",
+ "embedded-hal 1.0.0",
+ "heapless",
+ "nb 1.1.0",
+ "panic-halt",
+ "tnc-core",
+ "trig-const",
+ "ufmt",
+]
+
+[[package]]
+name = "arraydeque"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
+
+[[package]]
+name = "atdf2svd"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "179e5f106191a4d02437d8f66ed06f852e1a85774db29f934251d33cfec219fa"
+dependencies = [
+ "cfg-if",
+ "log",
+ "svd-encoder",
+ "svd-rs",
+ "xmltree 0.10.3",
+]
+
+[[package]]
+name = "atmega-hal"
+version = "0.1.0"
+source = "git+https://github.com/rahix/avr-hal?rev=e5c8f37fe48419956e722490a82b9ca9b9fc61a2#e5c8f37fe48419956e722490a82b9ca9b9fc61a2"
+dependencies = [
+ "avr-device",
+ "avr-hal-generic",
+]
+
+[[package]]
+name = "avr-device"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0584fb409e2ff23423979617c6b7ab34628727c8ef8026b665d41f41114060b9"
+dependencies = [
+ "anyhow",
+ "atdf2svd",
+ "avr-device-macros",
+ "bare-metal",
+ "cfg-if",
+ "critical-section",
+ "prettyplease",
+ "svd-rs",
+ "svd2rust",
+ "svdtools",
+ "syn 2.0.110",
+ "vcell",
+ "yaml-rust2",
+]
+
+[[package]]
+name = "avr-device-macros"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "129341bd5b981a038dd2ae0963550bfcb67ad56c64b53aa462cf1a0764405e97"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "avr-hal-generic"
+version = "0.1.0"
+source = "git+https://github.com/rahix/avr-hal?rev=e5c8f37fe48419956e722490a82b9ca9b9fc61a2#e5c8f37fe48419956e722490a82b9ca9b9fc61a2"
+dependencies = [
+ "avr-device",
+ "embedded-hal 0.2.7",
+ "embedded-hal 1.0.0",
+ "embedded-hal-bus",
+ "embedded-storage",
+ "nb 1.1.0",
+ "paste",
+ "ufmt",
+ "unwrap-infallible",
+]
+
+[[package]]
+name = "bare-metal"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fe8f5a8a398345e52358e18ff07cc17a568fbca5c6f73873d3a62056309603"
+
+[[package]]
+name = "bstr"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "commands"
+version = "0.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "688226b9769bbf11a9d82a94fb4adda15793a6023d33a8ca7695dbafec6f4123"
+
+[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "crc"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
+
+[[package]]
+name = "critical-section"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
+
+[[package]]
+name = "displaydoc"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+]
+
+[[package]]
+name = "either"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
+
+[[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-bus"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57b4e6ede84339ebdb418cd986e6320a34b017cdf99b5cc3efceec6450b06886"
+dependencies = [
+ "critical-section",
+ "embedded-hal 1.0.0",
+]
+
+[[package]]
+name = "embedded-storage"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "723dce4e9f25b6e6c5f35628e144794e5b459216ed7da97b7c4b66cdb3fa82ca"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "globset"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "log",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[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.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "hashlink"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+dependencies = [
+ "hashbrown 0.15.5",
+]
+
+[[package]]
+name = "heapless"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4"
+dependencies = [
+ "hash32",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+]
+
+[[package]]
+name = "inflections"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a"
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "litemap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
+[[package]]
+name = "log"
+version = "0.4.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
+
+[[package]]
+name = "memchr"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
+
+[[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 = "normpath"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9985ef7269fa99f3b12437bb698381da2428743ab90f20393f399fa14cab21a"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "panic-halt"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a513e167849a384b7f9b746e517604398518590a9142f4846a32e3c2a4de7b11"
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "phf"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
+dependencies = [
+ "phf_shared",
+ "rand",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.110",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.37.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
+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 = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "roxmltree"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+]
+
+[[package]]
+name = "siphasher"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "svd-encoder"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0961dac31cc1ce3e673918792e826d6bdf5e4005b197acb996b793cfc732f83"
+dependencies = [
+ "convert_case",
+ "svd-rs",
+ "thiserror 1.0.69",
+ "xmltree 0.11.0",
+]
+
+[[package]]
+name = "svd-parser"
+version = "0.14.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a41fe17e46dee363a7d3b20e878bbf6d6b4e839ae010ff07ef0e05d41d201811"
+dependencies = [
+ "anyhow",
+ "roxmltree",
+ "svd-rs",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "svd-rs"
+version = "0.14.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9b88ee2e82f09623ff76965587dc15a2e7150a3126854899c86e94dab777458"
+dependencies = [
+ "once_cell",
+ "regex",
+ "serde",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "svd2rust"
+version = "0.37.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1eaf6a040bfbc04e7b946a6a373b24aab09e1feb875bb0e20c929946c4b7bfc4"
+dependencies = [
+ "anyhow",
+ "inflections",
+ "log",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "svd-parser",
+ "svd-rs",
+ "syn 2.0.110",
+ "thiserror 2.0.18",
+ "url",
+]
+
+[[package]]
+name = "svdtools"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cb2e48b200a352e10c4c50056b3f0a2da8e571c317d5af47c41ba672b2e9c87"
+dependencies = [
+ "anyhow",
+ "commands",
+ "globset",
+ "hashlink",
+ "itertools",
+ "log",
+ "normpath",
+ "once_cell",
+ "phf",
+ "quick-xml",
+ "regex",
+ "serde",
+ "svd-encoder",
+ "svd-parser",
+ "svd-rs",
+ "thiserror 1.0.69",
+ "yaml-rust2",
+]
+
+[[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.110"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl 2.0.18",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tnc-core"
+version = "0.1.0"
+
+[[package]]
+name = "trig-const"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffec2fe2ed1e10dac46d8530afb395cae635fa02ef03d7fff1c211e94f9059fc"
+
+[[package]]
+name = "ufmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a64846ec02b57e9108d6469d98d1648782ad6bb150a95a9baac26900bbeab9d"
+dependencies = [
+ "ufmt-macros",
+ "ufmt-write",
+]
+
+[[package]]
+name = "ufmt-macros"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d337d3be617449165cb4633c8dece429afd83f84051024079f97ad32a9663716"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "ufmt-write"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e87a2ed6b42ec5e28cc3b94c09982969e9227600b2e3dcbc1db927a84c06bd69"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
+
+[[package]]
+name = "unwrap-infallible"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "151ac09978d3c2862c4e39b557f4eceee2cc72150bc4cb4f16abf061b6e381fb"
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+ "serde_derive",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "vcell"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002"
+
+[[package]]
+name = "void"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "xml-rs"
+version = "0.8.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
+
+[[package]]
+name = "xmltree"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb"
+dependencies = [
+ "xml-rs",
+]
+
+[[package]]
+name = "xmltree"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b619f8c85654798007fb10afa5125590b43b088c225a25fc2fec100a9fad0fc6"
+dependencies = [
+ "indexmap",
+ "xml-rs",
+]
+
+[[package]]
+name = "yaml-rust2"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9"
+dependencies = [
+ "arraydeque",
+ "encoding_rs",
+ "hashlink",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+ "synstructure",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+ "synstructure",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+]
diff --git a/firmware/Cargo.toml b/firmware/Cargo.toml
@@ -0,0 +1,41 @@
+[package]
+name = "arduino-tnc"
+version = "0.1.0"
+authors = ["Bradley Gannon <bradley@bradleygannon.com>"]
+edition = "2021"
+license = "MIT OR Apache-2.0"
+
+[[bin]]
+bench = false
+name = "arduino-tnc"
+test = false
+
+[dependencies]
+avr-device = "0.8.1"
+crc = "3.4.0"
+embedded-hal = "1.0"
+heapless = "0.9.2"
+nb = "1.1.0"
+panic-halt = "1.0.0"
+trig-const = "0.4.0"
+ufmt = "0.2.0"
+
+[dependencies.arduino-hal]
+git = "https://github.com/rahix/avr-hal"
+rev = "e5c8f37fe48419956e722490a82b9ca9b9fc61a2"
+features = ["arduino-nano"]
+
+[dependencies.tnc-core]
+path = "../tnc-core"
+
+[profile.dev]
+opt-level = "s"
+lto = true
+panic = "abort"
+
+[profile.release]
+opt-level = "s"
+debug = true
+lto = true
+panic = "abort"
+codegen-units = 1
diff --git a/firmware/Ravedude.toml b/firmware/Ravedude.toml
@@ -0,0 +1,4 @@
+[general]
+board = "nano"
+open-console = true
+serial-baudrate = 115200
diff --git a/rust-toolchain.toml b/firmware/rust-toolchain.toml
diff --git a/src/kiss.rs b/firmware/src/kiss.rs
diff --git a/firmware/src/main.rs b/firmware/src/main.rs
@@ -0,0 +1,182 @@
+#![no_std]
+#![no_main]
+#![feature(abi_avr_interrupt)]
+
+mod kiss;
+mod rx;
+mod tx;
+
+use core::cell::RefCell;
+use core::hint::spin_loop;
+
+use arduino_hal::clock::MHz16;
+use arduino_hal::hal::usart::{Event, Usart0};
+use arduino_hal::{pac, Adc};
+use avr_device::generic::Periph;
+use avr_device::interrupt::{self, Mutex};
+use kiss::{KissAction, KissTncState};
+use panic_halt as _;
+use rx::Receiver;
+use tx::Transmitter;
+
+type TC0 = Periph<pac::tc0::RegisterBlock, 53>;
+type TC1 = Periph<pac::tc1::RegisterBlock, 54>;
+
+struct Tnc {
+ state: TncState,
+ serial: Option<Usart0<MHz16>>,
+ receiver: Receiver,
+ transmitter: Transmitter,
+ kiss_state: KissTncState,
+ sample_timer: Option<TC0>,
+ symbol_timer: Option<TC1>,
+}
+
+impl Tnc {
+ // 52 * 8 = 416 cycles; at 16 MHz this gives a period of 26 µs (about 38460 Hz)
+ const SAMPLE_TIMER_COMPARE: u8 = 52;
+ // this should theoretically be 13_333 cycles, but in practice we have to go a little faster to
+ // account for overhead; this value seems to work well
+ const SYMBOL_TIMER_COMPARE: u16 = 13_300;
+
+ const fn default() -> Tnc {
+ Tnc {
+ state: TncState::Receiving,
+ serial: None,
+ receiver: Receiver::default(),
+ transmitter: Transmitter::default(),
+ kiss_state: KissTncState::Idle,
+ sample_timer: None,
+ symbol_timer: None,
+ }
+ }
+ pub(crate) fn handle_usart_byte(&mut self, byte: u8) {
+ if let Some(action) = self.kiss_state.update(byte) {
+ match action {
+ KissAction::PushTx(b) => {
+ let _ = self.transmitter.push_message_byte(b);
+ }
+ KissAction::ClearTx => self.transmitter.clear_message(),
+ KissAction::SendTx => {
+ if self.transmitter.has_pending_bytes() {
+ self.state = TncState::Transmitting;
+ self.transmitter.start();
+ }
+ }
+ KissAction::_SetTxDelay { .. } => todo!(),
+ KissAction::_SetPersistence { .. } => todo!(),
+ KissAction::_SetSlotTime { .. } => todo!(),
+ KissAction::_SetTxTail { .. } => todo!(),
+ KissAction::_SetFullDuplex { .. } => todo!(),
+ KissAction::_SetHardware { .. } => todo!(),
+ KissAction::_Return => todo!(),
+ }
+ };
+ }
+}
+
+enum TncState {
+ Receiving,
+ Transmitting,
+}
+
+static TNC: Mutex<RefCell<Tnc>> = Mutex::new(RefCell::new(Tnc::default()));
+
+#[avr_device::interrupt(atmega328p)]
+fn TIMER0_COMPA() {
+ interrupt::free(|cs| {
+ let mut tnc = TNC.borrow(cs).borrow_mut();
+ match tnc.state {
+ TncState::Receiving => {
+ tnc.receiver.sample_update();
+ if tnc.receiver.is_at_symbol_transition() {
+ if let Some(ref mut symbol_timer) = tnc.symbol_timer {
+ symbol_timer
+ .tcnt1()
+ .write(|w| w.set(Tnc::SYMBOL_TIMER_COMPARE / 2));
+ }
+ }
+ if let Some(byte) = tnc.receiver.get_output() {
+ if let Some(ref mut serial) = tnc.serial {
+ let _ = ufmt::uwriteln!(serial, "0x{:02x}", byte);
+ }
+ }
+ }
+ TncState::Transmitting => {
+ tnc.transmitter.sample_update();
+ if tnc.transmitter.is_done() {
+ tnc.state = TncState::Receiving;
+ }
+ }
+ }
+ });
+}
+
+#[avr_device::interrupt(atmega328p)]
+fn TIMER1_COMPA() {
+ interrupt::free(|cs| {
+ let mut tnc = TNC.borrow(cs).borrow_mut();
+ match tnc.state {
+ TncState::Receiving => tnc.receiver.symbol_update(),
+ TncState::Transmitting => tnc.transmitter.symbol_update(),
+ }
+ });
+}
+
+#[avr_device::interrupt(atmega328p)]
+fn USART_RX() {
+ interrupt::free(|cs| {
+ let mut tnc = TNC.borrow(cs).borrow_mut();
+ if let Some(ref mut serial) = tnc.serial {
+ let byte = serial.read_byte();
+ tnc.handle_usart_byte(byte);
+ }
+ });
+}
+
+#[arduino_hal::entry]
+fn main() -> ! {
+ let dp = arduino_hal::Peripherals::take().unwrap();
+ let pins = arduino_hal::pins!(dp);
+
+ let mut serial = arduino_hal::default_serial!(dp, pins, 115200);
+ serial.listen(Event::RxComplete);
+
+ let sample_timer = dp.TC0;
+ sample_timer.tccr0a().write(|w| w.wgm0().ctc());
+ sample_timer.tccr0b().write(|w| w.cs0().prescale_8());
+ sample_timer
+ .ocr0a()
+ .write(|w| w.set(Tnc::SAMPLE_TIMER_COMPARE));
+ sample_timer.timsk0().write(|w| w.ocie0a().set_bit());
+
+ let symbol_timer = dp.TC1;
+ symbol_timer
+ .tccr1b()
+ .write(|w| w.wgm1().set(0b01).cs1().direct());
+ symbol_timer
+ .ocr1a()
+ .write(|w| w.set(Tnc::SYMBOL_TIMER_COMPARE));
+ symbol_timer.timsk1().write(|w| w.ocie1a().set_bit());
+
+ interrupt::free(|cs| {
+ let mut tnc = TNC.borrow(cs).borrow_mut();
+ tnc.sample_timer = Some(sample_timer);
+ tnc.symbol_timer = Some(symbol_timer);
+ tnc.receiver
+ .initialize(Adc::new(dp.ADC, Default::default()), pins.a0);
+ tnc.transmitter.initialize(
+ pins.d2.into_output(),
+ pins.d3.into_output(),
+ pins.d4.into_output(),
+ pins.d5.into_output(),
+ pins.d6.into_output(),
+ );
+ });
+
+ unsafe { avr_device::interrupt::enable() };
+
+ loop {
+ spin_loop();
+ }
+}
diff --git a/firmware/src/rx.rs b/firmware/src/rx.rs
@@ -0,0 +1,44 @@
+use arduino_hal::{
+ hal::port::PC0,
+ port::{
+ mode::{Analog, Floating, Input},
+ Pin,
+ },
+ Adc,
+};
+use tnc_core::goertzel::Bell202Demodulator;
+
+pub(crate) struct Receiver {
+ adc: Option<Adc>,
+ input: Option<Pin<Analog, PC0>>,
+ demod: Bell202Demodulator,
+}
+
+impl Receiver {
+ pub(crate) const fn default() -> Receiver {
+ Receiver {
+ adc: None,
+ input: None,
+ demod: Bell202Demodulator::new(),
+ }
+ }
+ pub(crate) fn initialize(&mut self, mut adc: Adc, input: Pin<Input<Floating>, PC0>) {
+ self.input = Some(input.into_analog_input(&mut adc));
+ self.adc = Some(adc);
+ }
+ pub(crate) fn is_at_symbol_transition(&self) -> bool {
+ self.demod.get_output().is_some()
+ }
+ pub(crate) fn get_output(&self) -> Option<u8> {
+ self.demod.get_output()
+ }
+ pub(crate) fn sample_update(&mut self) {
+ if let (Some(ref input), Some(ref mut adc)) = (&self.input, &mut self.adc) {
+ let new_sample = input.analog_read(adc) as f32 / 512. - 0.5;
+ self.demod.sample_update(new_sample);
+ }
+ }
+ pub(crate) fn symbol_update(&mut self) {
+ self.demod.symbol_update();
+ }
+}
diff --git a/src/tx.rs b/firmware/src/tx.rs
diff --git a/flake.lock b/flake.lock
@@ -46,11 +46,11 @@
},
"stablePkgs": {
"locked": {
- "lastModified": 1779467186,
- "narHash": "sha256-nOesoDCiXcUftqbRBMz9tt4blI5PvljMWbm3kuCA+0s=",
+ "lastModified": 1779796641,
+ "narHash": "sha256-ZsIrKmhp4vbBXoXXmR/tBXA/UCsAQiJL9vsgZEduhVY=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "b77b3de8775677f84492abe84635f87b0e153f0f",
+ "rev": "25f538306313eae3927264466c70d7001dcea1df",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
@@ -26,7 +26,7 @@
inherit system;
};
rust = fenix.fromToolchainFile {
- file = ./rust-toolchain.toml;
+ file = ./firmware/rust-toolchain.toml;
sha256 = "sha256-DnyK5MS+xYySA+csnnMogu2gtEfyiy10W0ATmAvmjGg=";
};
libInputs = [ stable.stdenv.cc.cc.lib ];
diff --git a/src/main.rs b/src/main.rs
@@ -1,182 +0,0 @@
-#![no_std]
-#![no_main]
-#![feature(abi_avr_interrupt)]
-
-mod kiss;
-mod rx;
-mod tx;
-
-use core::cell::RefCell;
-use core::hint::spin_loop;
-
-use arduino_hal::clock::MHz16;
-use arduino_hal::hal::usart::{Event, Usart0};
-use arduino_hal::{pac, Adc};
-use avr_device::generic::Periph;
-use avr_device::interrupt::{self, Mutex};
-use kiss::{KissAction, KissTncState};
-use panic_halt as _;
-use rx::Receiver;
-use tx::Transmitter;
-
-type TC0 = Periph<pac::tc0::RegisterBlock, 53>;
-type TC1 = Periph<pac::tc1::RegisterBlock, 54>;
-
-struct Tnc {
- state: TncState,
- serial: Option<Usart0<MHz16>>,
- receiver: Receiver,
- transmitter: Transmitter,
- kiss_state: KissTncState,
- sample_timer: Option<TC0>,
- symbol_timer: Option<TC1>,
-}
-
-impl Tnc {
- // 52 * 8 = 416 cycles; at 16 MHz this gives a period of 26 µs (about 38460 Hz)
- const SAMPLE_TIMER_COMPARE: u8 = 52;
- // this should theoretically be 13_333 cycles, but in practice we have to go a little faster to
- // account for overhead; this value seems to work well
- const SYMBOL_TIMER_COMPARE: u16 = 13_300;
-
- const fn default() -> Tnc {
- Tnc {
- state: TncState::Receiving,
- serial: None,
- receiver: Receiver::default(),
- transmitter: Transmitter::default(),
- kiss_state: KissTncState::Idle,
- sample_timer: None,
- symbol_timer: None,
- }
- }
- pub(crate) fn handle_usart_byte(&mut self, byte: u8) {
- if let Some(action) = self.kiss_state.update(byte) {
- match action {
- KissAction::PushTx(b) => {
- let _ = self.transmitter.push_message_byte(b);
- }
- KissAction::ClearTx => self.transmitter.clear_message(),
- KissAction::SendTx => {
- if self.transmitter.has_pending_bytes() {
- self.state = TncState::Transmitting;
- self.transmitter.start();
- }
- }
- KissAction::_SetTxDelay { .. } => todo!(),
- KissAction::_SetPersistence { .. } => todo!(),
- KissAction::_SetSlotTime { .. } => todo!(),
- KissAction::_SetTxTail { .. } => todo!(),
- KissAction::_SetFullDuplex { .. } => todo!(),
- KissAction::_SetHardware { .. } => todo!(),
- KissAction::_Return => todo!(),
- }
- };
- }
-}
-
-enum TncState {
- Receiving,
- Transmitting,
-}
-
-static TNC: Mutex<RefCell<Tnc>> = Mutex::new(RefCell::new(Tnc::default()));
-
-#[avr_device::interrupt(atmega328p)]
-fn TIMER0_COMPA() {
- interrupt::free(|cs| {
- let mut tnc = TNC.borrow(cs).borrow_mut();
- match tnc.state {
- TncState::Receiving => {
- tnc.receiver.sample_update();
- if tnc.receiver.is_at_symbol_transition() {
- if let Some(ref mut symbol_timer) = tnc.symbol_timer {
- symbol_timer
- .tcnt1()
- .write(|w| w.set(Tnc::SYMBOL_TIMER_COMPARE / 2));
- }
- }
- if let Some(byte) = tnc.receiver.get_output() {
- if let Some(ref mut serial) = tnc.serial {
- let _ = ufmt::uwriteln!(serial, "0x{:02x}", byte);
- }
- }
- }
- TncState::Transmitting => {
- tnc.transmitter.sample_update();
- if tnc.transmitter.is_done() {
- tnc.state = TncState::Receiving;
- }
- }
- }
- });
-}
-
-#[avr_device::interrupt(atmega328p)]
-fn TIMER1_COMPA() {
- interrupt::free(|cs| {
- let mut tnc = TNC.borrow(cs).borrow_mut();
- match tnc.state {
- TncState::Receiving => tnc.receiver.symbol_update(),
- TncState::Transmitting => tnc.transmitter.symbol_update(),
- }
- });
-}
-
-#[avr_device::interrupt(atmega328p)]
-fn USART_RX() {
- interrupt::free(|cs| {
- let mut tnc = TNC.borrow(cs).borrow_mut();
- if let Some(ref mut serial) = tnc.serial {
- let byte = serial.read_byte();
- tnc.handle_usart_byte(byte);
- }
- });
-}
-
-#[arduino_hal::entry]
-fn main() -> ! {
- let dp = arduino_hal::Peripherals::take().unwrap();
- let pins = arduino_hal::pins!(dp);
-
- let mut serial = arduino_hal::default_serial!(dp, pins, 57600);
- serial.listen(Event::RxComplete);
-
- let sample_timer = dp.TC0;
- sample_timer.tccr0a().write(|w| w.wgm0().ctc());
- sample_timer.tccr0b().write(|w| w.cs0().prescale_8());
- sample_timer
- .ocr0a()
- .write(|w| w.set(Tnc::SAMPLE_TIMER_COMPARE));
- sample_timer.timsk0().write(|w| w.ocie0a().set_bit());
-
- let symbol_timer = dp.TC1;
- symbol_timer
- .tccr1b()
- .write(|w| w.wgm1().set(0b01).cs1().direct());
- symbol_timer
- .ocr1a()
- .write(|w| w.set(Tnc::SYMBOL_TIMER_COMPARE));
- symbol_timer.timsk1().write(|w| w.ocie1a().set_bit());
-
- interrupt::free(|cs| {
- let mut tnc = TNC.borrow(cs).borrow_mut();
- tnc.sample_timer = Some(sample_timer);
- tnc.symbol_timer = Some(symbol_timer);
- tnc.receiver
- .initialize(Adc::new(dp.ADC, Default::default()), pins.a0);
- tnc.transmitter.initialize(
- pins.d2.into_output(),
- pins.d3.into_output(),
- pins.d4.into_output(),
- pins.d5.into_output(),
- pins.d6.into_output(),
- );
- });
-
- unsafe { avr_device::interrupt::enable() };
-
- loop {
- spin_loop();
- }
-}
diff --git a/src/rx.rs b/src/rx.rs
@@ -1,126 +0,0 @@
-use core::f32::consts::TAU;
-
-use arduino_hal::{
- hal::port::PC0,
- port::{
- mode::{Analog, Floating, Input},
- Pin,
- },
- Adc,
-};
-use trig_const::cos;
-
-#[derive(PartialEq, Clone, Copy)]
-enum Symbol {
- Mark,
- Space,
-}
-
-struct GoertzelFilter {
- coeff: f32,
- s0: f32,
- s1: f32,
- s2: f32,
- energy: f32,
-}
-
-impl GoertzelFilter {
- const fn new(frequency: f32, sample_rate: f32) -> Self {
- GoertzelFilter {
- coeff: 2. * cos((TAU * frequency / sample_rate) as f64) as f32,
- s0: 0.,
- s1: 0.,
- s2: 0.,
- energy: 0.,
- }
- }
- fn update(&mut self, x: f32) {
- self.s0 = x + self.coeff * self.s1 - self.s2;
- self.s2 = self.s1;
- self.s1 = self.s0;
- self.energy = self.s1 * self.s1 + self.s2 * self.s2 - self.coeff * self.s1 * self.s2;
- }
- fn energy(&self) -> f32 {
- self.energy
- }
-}
-
-pub(crate) struct Receiver {
- adc: Option<Adc>,
- input: Option<Pin<Analog, PC0>>,
- mark: GoertzelFilter,
- space: GoertzelFilter,
- symbol_estimate: Symbol,
- previous_symbol_estimate: Symbol,
- symbol_phase: u8,
- byte: u8,
- byte_phase: u8,
- output: Option<u8>,
-}
-
-impl Receiver {
- const MARK_HZ: f32 = 1200.;
- const SPACE_HZ: f32 = 2200.;
- const SAMPLE_RATE_HZ: f32 = 38400.;
- const SYMBOL_RATE_HZ: f32 = 1200.;
- const SAMPLES_PER_SYMBOL: u8 = (Self::SAMPLE_RATE_HZ / Self::SYMBOL_RATE_HZ) as u8;
-
- pub(crate) const fn default() -> Receiver {
- Receiver {
- adc: None,
- input: None,
- mark: GoertzelFilter::new(Receiver::MARK_HZ, Receiver::SAMPLE_RATE_HZ),
- space: GoertzelFilter::new(Receiver::SPACE_HZ, Receiver::SAMPLE_RATE_HZ),
- symbol_estimate: Symbol::Space,
- previous_symbol_estimate: Symbol::Space,
- symbol_phase: 0,
- byte: 0,
- byte_phase: 0,
- output: None,
- }
- }
- pub(crate) fn initialize(&mut self, mut adc: Adc, input: Pin<Input<Floating>, PC0>) {
- self.input = Some(input.into_analog_input(&mut adc));
- self.adc = Some(adc);
- }
- pub(crate) fn is_at_symbol_transition(&self) -> bool {
- self.symbol_phase == 0
- }
- pub(crate) fn get_output(&self) -> Option<u8> {
- self.output
- }
- pub(crate) fn sample_update(&mut self) {
- if let (Some(ref input), Some(ref mut adc)) = (&self.input, &mut self.adc) {
- let new_sample = input.analog_read(adc) as f32 / 512. - 0.5;
- self.mark.update(new_sample);
- self.space.update(new_sample);
- let new_symbol_estimate = if self.mark.energy() > self.space.energy() {
- Symbol::Mark
- } else {
- Symbol::Space
- };
- if new_symbol_estimate != self.symbol_estimate {
- self.symbol_phase = 0;
- } else {
- self.symbol_phase = self.symbol_phase.saturating_add(1) % Self::SAMPLES_PER_SYMBOL;
- }
- self.symbol_estimate = new_symbol_estimate;
- }
- }
- pub(crate) fn symbol_update(&mut self) {
- self.byte <<= 1;
- if self.symbol_estimate == self.previous_symbol_estimate {
- self.byte += 1;
- }
- self.byte_phase = (self.byte_phase + 1) % 8;
- if self.byte == 0x7e {
- self.byte_phase = 0;
- }
- if self.byte_phase == 0 {
- self.output = Some(self.byte);
- } else {
- self.output = None;
- }
- self.previous_symbol_estimate = self.symbol_estimate;
- }
-}
diff --git a/tnc-core/.cargo/config.toml b/tnc-core/.cargo/config.toml
@@ -0,0 +1,5 @@
+# Override the workspace root's AVR target + build-std for this package.
+# x86_64 has a pre-built core, so build-std from the parent is effectively
+# a no-op for this crate.
+[build]
+target = "x86_64-unknown-linux-gnu"
diff --git a/tnc-core/Cargo.lock b/tnc-core/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "tnc-core"
+version = "0.1.0"
diff --git a/tnc-core/Cargo.toml b/tnc-core/Cargo.toml
@@ -0,0 +1,4 @@
+[package]
+name = "tnc-core"
+version = "0.1.0"
+edition = "2021"
diff --git a/tnc-core/src/goertzel.rs b/tnc-core/src/goertzel.rs
@@ -0,0 +1,155 @@
+//! Block-based Goertzel demodulation for Bell 202 AFSK (mark @ 1200 Hz, space @ 2200 Hz).
+
+/// A single Goertzel resonator.
+#[derive(Clone, Copy, Debug)]
+pub struct GoertzelFilter {
+ /// 2cos(2πf / sample_rate)
+ pub coeff: f32,
+ pub s0: f32,
+ pub s1: f32,
+ pub s2: f32,
+ pub energy: f32,
+}
+
+impl GoertzelFilter {
+ /// Create a new Goertzel filter with a pre-computed coefficient.
+ ///
+ /// The coefficient should be `2cos(2πf / sample_rate)`. We can't easily compute the
+ /// coefficient inside this method because it's `const`.
+ pub const fn new_with_coeff(coeff: f32) -> Self {
+ Self {
+ coeff,
+ s0: 0.0,
+ s1: 0.0,
+ s2: 0.0,
+ energy: 0.0,
+ }
+ }
+
+ pub const fn mark() -> Self {
+ Self::new_with_coeff(1.961_69)
+ }
+
+ pub const fn space() -> Self {
+ Self::new_with_coeff(1.872_207_2)
+ }
+
+ /// Feed one sample through the resonator.
+ ///
+ /// After calling `update`, the `energy` field contains the squared energy of the current
+ /// block. For block-based usage, reset `s0`, `s1`, `s2` to 0.0 at each symbol boundary.
+ pub fn update(&mut self, x: f32) {
+ self.s0 = x + self.coeff * self.s1 - self.s2;
+ self.s2 = self.s1;
+ self.s1 = self.s0;
+ self.energy = self.s1 * self.s1 + self.s2 * self.s2 - self.coeff * self.s1 * self.s2;
+ }
+
+ pub fn energy(&self) -> f32 {
+ self.energy
+ }
+
+ pub fn reset(&mut self) {
+ self.s0 = 0.0;
+ self.s1 = 0.0;
+ self.s2 = 0.0;
+ self.energy = 0.0;
+ }
+}
+
+/// A Bell 202 demodulator that produces byte-aligned output using NRZI decoding and HDLC flag
+/// detection.
+pub struct Bell202Demodulator {
+ mark: GoertzelFilter,
+ space: GoertzelFilter,
+ symbol_estimate: Symbol,
+ previous_symbol_estimate: Symbol,
+ symbol_phase: u8,
+ byte: u8,
+ byte_phase: u8,
+ output: Option<u8>,
+}
+
+/// A decoded symbol: mark (1200 Hz) or space (2200 Hz).
+#[derive(PartialEq, Clone, Copy, Debug)]
+pub enum Symbol {
+ Mark,
+ Space,
+}
+
+impl Bell202Demodulator {
+ pub const SAMPLE_RATE_HZ: f32 = 38460.;
+ pub const MARK_HZ: f32 = 1200.;
+ pub const SPACE_HZ: f32 = 2200.;
+ pub const SYMBOL_RATE_HZ: f32 = 1200.;
+ pub const SAMPLES_PER_SYMBOL: u8 = (Self::SAMPLE_RATE_HZ / Self::SYMBOL_RATE_HZ) as u8;
+
+ pub const fn new() -> Self {
+ Self {
+ mark: GoertzelFilter::mark(),
+ space: GoertzelFilter::space(),
+ symbol_estimate: Symbol::Space,
+ previous_symbol_estimate: Symbol::Space,
+ symbol_phase: 0,
+ byte: 0,
+ byte_phase: 0,
+ output: None,
+ }
+ }
+
+ /// Feed one sample into the Goertzel resonators.
+ ///
+ /// This should be called at the configured sample rate.
+ pub fn sample_update(&mut self, sample: f32) {
+ self.mark.update(sample);
+ self.space.update(sample);
+ let new_symbol_estimate = if self.mark.energy() > self.space.energy() {
+ Symbol::Mark
+ } else {
+ Symbol::Space
+ };
+ if new_symbol_estimate != self.symbol_estimate {
+ self.symbol_phase = 0;
+ } else {
+ self.symbol_phase = self.symbol_phase.saturating_add(1) % Self::SAMPLES_PER_SYMBOL;
+ }
+ self.symbol_estimate = new_symbol_estimate;
+ }
+
+ /// Called once per symbol period. Produces NRZI-decoded, byte-aligned output.
+ ///
+ /// The demodulator outputs a byte whenever `0x7e` (HDLC flag) is detected in the NRZI stream.
+ /// Returns the output byte, or `None` if no flag was found.
+ pub fn symbol_update(&mut self) -> Option<u8> {
+ self.byte <<= 1;
+ if self.symbol_estimate == self.previous_symbol_estimate {
+ self.byte += 1;
+ }
+ self.byte_phase = (self.byte_phase + 1) % 8;
+ if self.byte == 0x7e {
+ self.byte_phase = 0;
+ }
+ if self.byte_phase == 0 {
+ self.output = Some(self.byte);
+ } else {
+ self.output = None;
+ }
+ self.previous_symbol_estimate = self.symbol_estimate;
+ self.output
+ }
+
+ pub fn reset(&mut self) {
+ self.mark.reset();
+ self.space.reset();
+ self.symbol_estimate = Symbol::Space;
+ self.previous_symbol_estimate = Symbol::Space;
+ self.symbol_phase = 0;
+ self.byte = 0;
+ self.byte_phase = 0;
+ self.output = None;
+ }
+
+ pub fn get_output(&self) -> Option<u8> {
+ self.output
+ }
+}
diff --git a/tnc-core/src/lib.rs b/tnc-core/src/lib.rs
@@ -0,0 +1,7 @@
+//! Shared, hardware-agnostic algorithms for the Arduino TNC.
+//!
+//! To run tests: `cargo test` (from workspace root or from tnc-core/)
+
+#![no_std]
+
+pub mod goertzel;
diff --git a/tnc-core/tests/demodulator_test.rs b/tnc-core/tests/demodulator_test.rs
@@ -0,0 +1,262 @@
+use std::f64::consts::TAU;
+
+use tnc_core::goertzel::{Bell202Demodulator, GoertzelFilter, Symbol};
+
+const SAMPLE_RATE: f32 = 38460.;
+
+fn generate_audio_phase_continuous(
+ symbols: &[Symbol],
+ sample_rate: f32,
+ mark_hz: f32,
+ space_hz: f32,
+) -> Vec<f32> {
+ let samples_per_symbol = (sample_rate / 1200.) as usize;
+ let mut audio = Vec::with_capacity(symbols.len() * samples_per_symbol);
+
+ let mut phase = 0_f64;
+ let dt = 1. / sample_rate as f64;
+
+ for sym in symbols {
+ let freq = match sym {
+ Symbol::Mark => mark_hz as f64,
+ Symbol::Space => space_hz as f64,
+ };
+ let phase_inc = freq * dt * TAU;
+
+ for _ in 0..samples_per_symbol {
+ audio.push(phase.sin() as f32);
+ phase += phase_inc;
+ }
+ }
+
+ audio
+}
+
+struct Rng(u64);
+
+impl Rng {
+ fn new(seed: u64) -> Self {
+ Self(seed)
+ }
+ fn next(&mut self) -> u64 {
+ self.0 = self
+ .0
+ .wrapping_mul(6364136223846793005)
+ .wrapping_add(1442695040888963407);
+ self.0
+ }
+ fn uniform(&mut self) -> f32 {
+ self.next() as f32 / u64::MAX as f32
+ }
+}
+
+fn generate_symbols(n: usize, seed: u64) -> Vec<Symbol> {
+ let mut rng = Rng::new(seed);
+ (0..n)
+ .map(|_| {
+ if rng.next() % 2 == 0 {
+ Symbol::Mark
+ } else {
+ Symbol::Space
+ }
+ })
+ .collect()
+}
+
+fn add_noise(samples: &[f32], noise_std: f32, seed: u64) -> Vec<f32> {
+ let mut rng = Rng::new(seed);
+ samples
+ .iter()
+ .map(|&s| {
+ if noise_std <= 0.0 {
+ return s;
+ }
+ let normal: f32 = (0..12).map(|_| rng.uniform() - 0.5).sum::<f32>() / (3_f32).sqrt();
+ s + normal * noise_std
+ })
+ .collect()
+}
+
+#[test]
+fn mark_only_tone_yields_mark_decisions() {
+ let mut demod = Bell202Demodulator::new();
+ let sps = Bell202Demodulator::SAMPLES_PER_SYMBOL as usize;
+ let total_samples = 64 * sps;
+ let dt = 1. / SAMPLE_RATE as f64;
+ let mut phase = 0_f64;
+ let phase_inc = 1200. * dt * TAU;
+
+ for i in 0..total_samples {
+ let sample = phase.sin() as f32;
+ demod.sample_update(sample);
+ if i > 0 && i % sps == 0 {
+ demod.symbol_update();
+ }
+ phase += phase_inc;
+ }
+}
+
+#[test]
+fn space_only_tone_yields_space_decisions() {
+ let mut demod = Bell202Demodulator::new();
+ let sps = Bell202Demodulator::SAMPLES_PER_SYMBOL as usize;
+ let total_samples = 64 * sps;
+ let dt = 1. / SAMPLE_RATE as f64;
+ let mut phase = 0_f64;
+ let phase_inc = 2200. * dt * TAU;
+
+ for i in 0..total_samples {
+ let sample = phase.sin() as f32;
+ demod.sample_update(sample);
+ if i > 0 && i % sps == 0 {
+ demod.symbol_update();
+ }
+ phase += phase_inc;
+ }
+}
+
+#[test]
+fn alternating_symbols_no_noise() {
+ let mut symbols = Vec::with_capacity(64);
+ for i in 0..64 {
+ symbols.push(if i % 2 == 0 {
+ Symbol::Mark
+ } else {
+ Symbol::Space
+ });
+ }
+ let audio = generate_audio_phase_continuous(&symbols, SAMPLE_RATE, 1200., 2200.);
+ let sps = Bell202Demodulator::SAMPLES_PER_SYMBOL as usize;
+
+ let mut mark = GoertzelFilter::mark();
+ let mut space = GoertzelFilter::space();
+ let mut correct = 0usize;
+ let mut total = 0usize;
+
+ for (i, &sample) in audio.iter().enumerate() {
+ mark.update(sample);
+ space.update(sample);
+ if i > 0 && i % sps == 0 {
+ total += 1;
+ let expected_mark = symbols[i / sps - 1] == Symbol::Mark;
+ if (mark.energy() > space.energy()) == expected_mark {
+ correct += 1;
+ }
+ mark.reset();
+ space.reset();
+ }
+ }
+
+ let accuracy = correct as f64 / total as f64;
+ assert!(
+ accuracy >= 0.95,
+ "Symbol accuracy should be >= 95% for clean alternating symbols, got {:.1}%",
+ accuracy * 100.0
+ );
+}
+
+#[test]
+fn random_symbols_no_noise() {
+ let symbols = generate_symbols(200, 42);
+ let audio = generate_audio_phase_continuous(&symbols, SAMPLE_RATE, 1200.0, 2200.0);
+ let sps = Bell202Demodulator::SAMPLES_PER_SYMBOL as usize;
+
+ let mut mark = GoertzelFilter::mark();
+ let mut space = GoertzelFilter::space();
+ let mut correct = 0usize;
+ let mut total = 0usize;
+
+ for (i, &sample) in audio.iter().enumerate() {
+ mark.update(sample);
+ space.update(sample);
+ if i > 0 && i % sps == 0 {
+ total += 1;
+ let expected_mark = symbols[i / sps - 1] == Symbol::Mark;
+ if (mark.energy() > space.energy()) == expected_mark {
+ correct += 1;
+ }
+ mark.reset();
+ space.reset();
+ }
+ }
+
+ let accuracy = correct as f64 / total as f64;
+ assert!(
+ accuracy >= 0.99,
+ "Symbol accuracy should be >= 99% for clean random symbols, got {:.1}% ({} / {})",
+ accuracy * 100.0,
+ correct,
+ total
+ );
+}
+
+#[test]
+fn moderate_noise_accuracy() {
+ let noise_std = 0.5 * 10.0f32.powf(-15.0 / 20.0);
+ let symbols = generate_symbols(200, 42);
+ let audio = generate_audio_phase_continuous(&symbols, SAMPLE_RATE, 1200., 2200.);
+ let noisy = add_noise(&audio, noise_std, 12345);
+ let sps = Bell202Demodulator::SAMPLES_PER_SYMBOL as usize;
+
+ let mut mark = GoertzelFilter::mark();
+ let mut space = GoertzelFilter::space();
+ let mut correct = 0usize;
+ let mut total = 0usize;
+
+ for (i, &sample) in noisy.iter().enumerate() {
+ mark.update(sample);
+ space.update(sample);
+ if i > 0 && i % sps == 0 {
+ total += 1;
+ let expected_mark = symbols[i / sps - 1] == Symbol::Mark;
+ if (mark.energy() > space.energy()) == expected_mark {
+ correct += 1;
+ }
+ mark.reset();
+ space.reset();
+ }
+ }
+
+ let accuracy = correct as f64 / total as f64;
+ assert!(
+ accuracy >= 0.90,
+ "Symbol accuracy at ~15 dB SNR should be >= 90%, got {:.1}% ({} / {})",
+ accuracy * 100.0,
+ correct,
+ total
+ );
+}
+
+#[test]
+fn frequency_offset_tolerance() {
+ let space_hz = 3200_f32;
+ let symbols = generate_symbols(100, 42);
+ let audio = generate_audio_phase_continuous(&symbols, SAMPLE_RATE, 1200.0, space_hz);
+ let sps = Bell202Demodulator::SAMPLES_PER_SYMBOL as usize;
+
+ let mut mark = GoertzelFilter::mark();
+ let mut space = GoertzelFilter::space();
+ let mut correct = 0usize;
+ let mut total = 0usize;
+
+ for (i, &sample) in audio.iter().enumerate() {
+ mark.update(sample);
+ space.update(sample);
+ if i > 0 && i % sps == 0 {
+ total += 1;
+ let expected_mark = symbols[i / sps - 1] == Symbol::Mark;
+ if (mark.energy() > space.energy()) == expected_mark {
+ correct += 1;
+ }
+ mark.reset();
+ space.reset();
+ }
+ }
+
+ let accuracy = correct as f64 / total as f64;
+ assert!(
+ accuracy >= 0.95,
+ "Symbol accuracy with +1000 Hz space offset should be >= 95%, got {:.1}%",
+ accuracy * 100.0
+ );
+}
diff --git a/tnc-core/tests/goertzel_filter_test.rs b/tnc-core/tests/goertzel_filter_test.rs
@@ -0,0 +1,78 @@
+use tnc_core::goertzel::GoertzelFilter;
+
+const SAMPLE_RATE: f32 = 38460.;
+
+fn generate_tone(frequency: f32, duration_samples: usize, sample_rate: f32) -> Vec<f32> {
+ let mut samples = Vec::new();
+ let dt = 1. / sample_rate;
+ for i in 0..duration_samples {
+ let phase = 2.0 * std::f32::consts::PI * frequency * dt * i as f32;
+ samples.push(phase.sin());
+ }
+ samples
+}
+
+#[test]
+fn mark_tone_gives_higher_mark_energy() {
+ let mut mark = GoertzelFilter::mark();
+ let mut space = GoertzelFilter::space();
+
+ let samples = generate_tone(1200.0, 32, SAMPLE_RATE);
+
+ for &sample in &samples {
+ mark.update(sample);
+ space.update(sample);
+ }
+
+ assert!(
+ mark.energy() > space.energy(),
+ "Mark resonator should have higher energy than space resonator for 1200 Hz tone. \
+ mark_energy={}, space_energy={}",
+ mark.energy(),
+ space.energy()
+ );
+}
+
+#[test]
+fn space_tone_gives_higher_space_energy() {
+ let mut mark = GoertzelFilter::mark();
+ let mut space = GoertzelFilter::space();
+
+ let samples = generate_tone(2200.0, 32, SAMPLE_RATE);
+
+ for &sample in &samples {
+ mark.update(sample);
+ space.update(sample);
+ }
+
+ assert!(
+ space.energy() > mark.energy(),
+ "Space resonator should have higher energy than mark resonator for 2200 Hz tone. \
+ mark_energy={}, space_energy={}",
+ mark.energy(),
+ space.energy()
+ );
+}
+
+#[test]
+fn filter_reset_clears_states() {
+ let mut filter = GoertzelFilter::mark();
+
+ let samples = generate_tone(1200.0, 32, SAMPLE_RATE);
+ for &sample in &samples {
+ filter.update(sample);
+ }
+
+ let energy_before = filter.energy();
+ assert!(
+ energy_before > 0.0,
+ "Filter should have non-zero energy after feeding samples"
+ );
+
+ filter.reset();
+
+ assert_eq!(filter.s0, 0.0, "s0 should be reset to 0.0");
+ assert_eq!(filter.s1, 0.0, "s1 should be reset to 0.0");
+ assert_eq!(filter.s2, 0.0, "s2 should be reset to 0.0");
+ assert_eq!(filter.energy, 0.0, "energy should be reset to 0.0");
+}