commit 025eb237b298a055ebdae22c37ab53ff1e39b7f7 Author: Stanislaw Grams Date: Mon Nov 24 22:02:23 2025 +0100 initial commit Signed-off-by: Stanislaw Grams diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f2bb475 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2025 Stanislaw Grams +# +# SPDX-License-Identifier: BSD-2-Clause + +/target/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..2222f6a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2025 Stanislaw Grams +# +# SPDX-License-Identifier: BSD-2-Clause + +[submodule "docs"] + path = docs + url = http://github.com/sgrams/trx-rs.wiki.git diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f84014d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2101 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "base64", + "bitflags 2.10.0", + "brotli", + "bytes", + "bytestring", + "derive_more 2.0.1", + "encoding_rs", + "flate2", + "foldhash", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-router" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +dependencies = [ + "bytestring", + "cfg-if", + "http", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2 0.5.10", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e43428f3bf11dee6d166b00ec2df4e3aa8cc1606aaa0b7433c146852e2f4e03b" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more 0.99.20", + "encoding_rs", + "futures-core", + "futures-util", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2 0.5.10", + "time", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "bytestring" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.2.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clap" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[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 = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[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.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "io-kit-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" +dependencies = [ + "core-foundation-sys", + "mach2", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mio-serial" +version = "5.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029e1f407e261176a983a6599c084efd322d9301028055c87174beac71397ba3" +dependencies = [ + "log", + "mio", + "nix 0.29.0", + "serialport", + "winapi", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[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", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serialport" +version = "4.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acaf3f973e8616d7ceac415f53fc60e190b2a686fbcf8d27d0256c741c5007b" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "core-foundation", + "core-foundation-sys", + "io-kit-sys", + "mach2", + "nix 0.26.4", + "scopeguard", + "unescaper", + "winapi", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +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", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.1", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-serial" +version = "5.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa1d5427f11ba7c5e6384521cfd76f2d64572ff29f3f4f7aa0f496282923fdc8" +dependencies = [ + "cfg-if", + "futures", + "log", + "mio-serial", + "serialport", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trx-backend" +version = "0.1.0" +dependencies = [ + "clap", + "serde", + "tokio", + "tokio-serial", + "tracing", + "trx-backend-ft817", + "trx-core", +] + +[[package]] +name = "trx-backend-ft817" +version = "0.1.0" +dependencies = [ + "serde", + "tokio", + "tokio-serial", + "tracing", + "trx-core", +] + +[[package]] +name = "trx-bin" +version = "0.1.0" +dependencies = [ + "clap", + "serde", + "serde_json", + "tokio", + "tokio-serial", + "tracing", + "tracing-subscriber", + "trx-backend", + "trx-core", + "trx-frontend", + "trx-frontend-http", +] + +[[package]] +name = "trx-core" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "trx-frontend" +version = "0.1.0" +dependencies = [ + "tokio", + "trx-core", +] + +[[package]] +name = "trx-frontend-http" +version = "0.1.0" +dependencies = [ + "actix-web", + "bytes", + "futures-util", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tracing", + "trx-core", + "trx-frontend", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unescaper" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01d12e3a56a4432a8b436f293c25f4808bdf9e9f9f98f9260bba1f1bc5a1f26" +dependencies = [ + "thiserror", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[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.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[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 = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[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", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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", + "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", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.lock.license b/Cargo.lock.license new file mode 100644 index 0000000..75a299e --- /dev/null +++ b/Cargo.lock.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 Stanislaw Grams + +SPDX-License-Identifier: BSD-2-Clause diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..598ada9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2025 Stanislaw Grams +# +# SPDX-License-Identifier: BSD-2-Clause + +[workspace] +members = [ + "src/trx-bin", + "src/trx-backend", + "src/trx-backend/src/trx-backend-ft817", + "src/trx-core", + "src/trx-frontend", + "src/trx-frontend/src/trx-frontend-http", +] +resolver = "2" + +[workspace.dependencies] +tokio = "1" +tokio-serial = "5" +serde = "1" +serde_json = "1" +tracing = "0.1" +tracing-subscriber = "0.3" +clap = "4" diff --git a/LICENSES/BSD-2-Clause.txt b/LICENSES/BSD-2-Clause.txt new file mode 100644 index 0000000..cbe01ea --- /dev/null +++ b/LICENSES/BSD-2-Clause.txt @@ -0,0 +1,9 @@ +Copyright (c) 2025 Stan Grams + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b019093 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +
+ trx-rs logo +
+ +# trx-rs (work in progress) + +This is an early, untested snapshot of a transceiver control stack (core + backend + HTTP frontend). Things may change quickly and APIs are not stable yet. Expect rough edges and bugs; use at your own risk and please report issues you hit. Features, tests and docs are still being written (or not). + +## Supported backends + +- Yaesu FT-817 (feature-gated crate `trx-backend-ft817`) +- Planned: other rigs I own; contributions and reports are welcome. + +## License + +This project is licensed under the BSD-2-Clause license. See `LICENSES/` for bundled third-party license files. diff --git a/assets/trx-logo.png b/assets/trx-logo.png new file mode 100644 index 0000000..0109d7f Binary files /dev/null and b/assets/trx-logo.png differ diff --git a/docs b/docs new file mode 160000 index 0000000..c98dc5a --- /dev/null +++ b/docs @@ -0,0 +1 @@ +Subproject commit c98dc5ab755b5cf65d8d4cf1ccb87e4b7f23b477 diff --git a/src/trx-backend/Cargo.toml b/src/trx-backend/Cargo.toml new file mode 100644 index 0000000..73dd6e5 --- /dev/null +++ b/src/trx-backend/Cargo.toml @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2025 Stanislaw Grams +# +# SPDX-License-Identifier: BSD-2-Clause + +[package] +name = "trx-backend" +version = "0.1.0" +edition = "2021" + +[features] +default = ["ft817"] +ft817 = ["dep:trx-backend-ft817"] + +[dependencies] +trx-core = { path = "../trx-core" } +trx-backend-ft817 = { path = "src/trx-backend-ft817", optional = true } +tokio = { workspace = true, features = ["full"] } +tokio-serial = { workspace = true } +serde = { workspace = true, features = ["derive"] } +tracing = { workspace = true } +clap = { workspace = true, features = ["derive"] } diff --git a/src/trx-backend/src/lib.rs b/src/trx-backend/src/lib.rs new file mode 100644 index 0000000..01bdd0b --- /dev/null +++ b/src/trx-backend/src/lib.rs @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +use clap::ValueEnum; +use trx_core::rig::RigCat; +use trx_core::DynResult; + +#[cfg(feature = "ft817")] +use trx_backend_ft817::Ft817; + +/// Supported rig backends selectable at runtime. +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum RigKind { + #[cfg(feature = "ft817")] + #[value(alias = "ft-817")] + Ft817, +} + +impl RigKind { + pub fn all() -> &'static [RigKind] { + &[ + #[cfg(feature = "ft817")] + RigKind::Ft817, + ] + } +} + +impl std::fmt::Display for RigKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + #[cfg(feature = "ft817")] + RigKind::Ft817 => write!(f, "ft817"), + } + } +} + +/// Connection details for instantiating a rig backend. +#[derive(Debug, Clone)] +pub enum RigAccess { + Serial { path: String, baud: u32 }, + Tcp { addr: String }, +} + +/// Instantiate a rig backend based on the selected kind and access method. +pub fn build_rig(kind: RigKind, access: RigAccess) -> DynResult> { + match (kind, access) { + // Yaesu FT-817 + #[cfg(feature = "ft817")] + (RigKind::Ft817, RigAccess::Serial { path, baud }) => { + Ok(Box::new(Ft817::new(&path, baud)?)) + } + #[cfg(feature = "ft817")] + (RigKind::Ft817, RigAccess::Tcp { .. }) => { + Err("FT-817 only supports serial CAT access".into()) + } + + // Fallback for unsupported combinations + #[allow(unreachable_patterns)] + _ => Err("Selected rig is not enabled/available".into()), + } +} diff --git a/src/trx-backend/src/trx-backend-ft817/Cargo.toml b/src/trx-backend/src/trx-backend-ft817/Cargo.toml new file mode 100644 index 0000000..9341ee5 --- /dev/null +++ b/src/trx-backend/src/trx-backend-ft817/Cargo.toml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2025 Stanislaw Grams +# +# SPDX-License-Identifier: BSD-2-Clause + +[package] +name = "trx-backend-ft817" +version = "0.1.0" +edition = "2021" + +[dependencies] +trx-core = { path = "../../../trx-core" } +tokio = { workspace = true, features = ["full"] } +tokio-serial = { workspace = true } +tracing = { workspace = true } +serde = { workspace = true, features = ["derive"] } diff --git a/src/trx-backend/src/trx-backend-ft817/src/lib.rs b/src/trx-backend/src/trx-backend-ft817/src/lib.rs new file mode 100644 index 0000000..6a5a6bb --- /dev/null +++ b/src/trx-backend/src/trx-backend-ft817/src/lib.rs @@ -0,0 +1,596 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +use std::pin::Pin; + +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::time::{timeout, Duration}; +use tokio_serial::{ClearBuffer, SerialPort, SerialPortBuilderExt, SerialStream}; + +use trx_core::math::{decode_freq_bcd, encode_freq_bcd}; +use trx_core::radio::freq::{Band, Freq}; +use trx_core::rig::{ + Rig, RigAccessMethod, RigCapabilities, RigCat, RigInfo, RigStatusFuture, RigVfo, RigVfoEntry, +}; +use trx_core::{DynResult, RigMode}; + +/// Backend for Yaesu FT-817 CAT control. +pub struct Ft817 { + port: SerialStream, + info: RigInfo, + vfo_side: Ft817VfoSide, + vfo_a_freq: Option, + vfo_b_freq: Option, + vfo_a_mode: Option, + vfo_b_mode: Option, +} + +impl Ft817 { + const READ_TIMEOUT: Duration = Duration::from_millis(800); + + pub fn new(path: &str, baud: u32) -> DynResult { + let builder = tokio_serial::new(path, baud); + let port = builder.open_native_async()?; + let info = RigInfo { + manufacturer: "Yaesu", + model: "FT-817", + revision: "", + capabilities: RigCapabilities { + supported_bands: vec![ + // Transmit-capable amateur bands + Band { + low_hz: 1_800_000, + high_hz: 2_000_000, + tx_allowed: true, + }, + Band { + low_hz: 3_500_000, + high_hz: 4_000_000, + tx_allowed: true, + }, + Band { + low_hz: 5_250_000, + high_hz: 5_450_000, + tx_allowed: true, + }, + Band { + low_hz: 7_000_000, + high_hz: 7_300_000, + tx_allowed: true, + }, + Band { + low_hz: 10_100_000, + high_hz: 10_150_000, + tx_allowed: true, + }, + Band { + low_hz: 14_000_000, + high_hz: 14_350_000, + tx_allowed: true, + }, + Band { + low_hz: 18_068_000, + high_hz: 18_168_000, + tx_allowed: true, + }, + Band { + low_hz: 21_000_000, + high_hz: 21_450_000, + tx_allowed: true, + }, + Band { + low_hz: 24_890_000, + high_hz: 24_990_000, + tx_allowed: true, + }, + Band { + low_hz: 28_000_000, + high_hz: 29_700_000, + tx_allowed: true, + }, + Band { + low_hz: 50_000_000, + high_hz: 54_000_000, + tx_allowed: true, + }, + Band { + low_hz: 144_000_000, + high_hz: 148_000_000, + tx_allowed: true, + }, + Band { + low_hz: 430_000_000, + high_hz: 450_000_000, + tx_allowed: true, + }, + // Receive-only coverage segments + Band { + low_hz: 100_000, + high_hz: 1_799_999, + tx_allowed: false, + }, + Band { + low_hz: 2_000_001, + high_hz: 3_499_999, + tx_allowed: false, + }, + Band { + low_hz: 4_000_001, + high_hz: 5_249_999, + tx_allowed: false, + }, + Band { + low_hz: 5_450_001, + high_hz: 6_999_999, + tx_allowed: false, + }, + Band { + low_hz: 7_300_001, + high_hz: 10_099_999, + tx_allowed: false, + }, + Band { + low_hz: 10_150_001, + high_hz: 13_999_999, + tx_allowed: false, + }, + Band { + low_hz: 14_350_001, + high_hz: 18_067_999, + tx_allowed: false, + }, + Band { + low_hz: 18_168_001, + high_hz: 20_999_999, + tx_allowed: false, + }, + Band { + low_hz: 21_450_001, + high_hz: 24_889_999, + tx_allowed: false, + }, + Band { + low_hz: 24_990_001, + high_hz: 27_999_999, + tx_allowed: false, + }, + Band { + low_hz: 29_700_001, + high_hz: 49_999_999, + tx_allowed: false, + }, + Band { + low_hz: 54_000_001, + high_hz: 75_999_999, + tx_allowed: false, + }, + Band { + low_hz: 76_000_000, + high_hz: 107_999_999, + tx_allowed: false, + }, + Band { + low_hz: 108_000_000, + high_hz: 143_999_999, + tx_allowed: false, + }, + Band { + low_hz: 148_000_001, + high_hz: 429_999_999, + tx_allowed: false, + }, + Band { + low_hz: 450_000_001, + high_hz: 470_000_000, + tx_allowed: false, + }, + ], + supported_modes: vec![ + RigMode::LSB, + RigMode::USB, + RigMode::CW, + RigMode::CWR, + RigMode::AM, + RigMode::WFM, + RigMode::FM, + RigMode::DIG, + RigMode::PKT, + ], + num_vfos: 2, + // CAT only exposes lock and VFO toggle; the other features are panel-only. + lockable: true, + attenuator: false, + preamp: false, + rit: false, + rpt: false, + split: false, + lock: true, + }, + access: RigAccessMethod::Serial { + path: path.to_string(), + baud, + }, + }; + Ok(Self { + port, + info, + vfo_side: Ft817VfoSide::Unknown, + vfo_a_freq: None, + vfo_b_freq: None, + vfo_a_mode: None, + vfo_b_mode: None, + }) + } + + /// Query current status (frequency, mode, VFO) from FT-817. + pub async fn get_status(&mut self) -> DynResult<(Freq, RigMode, Option)> { + let (hz, mode) = self.read_status().await?; + let freq = Freq { hz }; + self.update_vfo_freq(freq); + self.update_vfo_mode(mode.clone()); + let mut entries = Vec::new(); + if let Some(a) = self.vfo_a_freq { + entries.push(RigVfoEntry { + name: "A".to_string(), + freq: a, + mode: self.vfo_a_mode.clone(), + }); + } + if let Some(b) = self.vfo_b_freq { + entries.push(RigVfoEntry { + name: "B".to_string(), + freq: b, + mode: self.vfo_b_mode.clone(), + }); + } + let active = match self.vfo_side { + Ft817VfoSide::A if self.vfo_a_freq.is_some() => Some(0), + Ft817VfoSide::B if self.vfo_a_freq.is_some() => Some(1), + Ft817VfoSide::B if self.vfo_a_freq.is_none() && self.vfo_b_freq.is_some() => Some(0), + _ => None, + }; + let vfo = if entries.is_empty() { + None + } else { + Some(RigVfo { entries, active }) + }; + Ok((freq, mode, vfo)) + } + + /// Query current frequency from FT-817. + pub async fn get_freq(&mut self) -> DynResult { + let (freq, _, _) = self.get_status().await?; + Ok(freq) + } + + /// Query current mode from FT-817. + pub async fn get_mode(&mut self) -> DynResult { + let (_, mode, _) = self.get_status().await?; + Ok(mode) + } + + /// Send CAT command to set frequency on FT-817. + pub async fn set_freq(&mut self, freq: Freq) -> DynResult<()> { + let bcd = encode_freq_bcd(freq.hz)?; + let frame = [bcd[0], bcd[1], bcd[2], bcd[3], CMD_SET_FREQ]; + self.write_frame(&frame).await?; + self.update_vfo_freq(freq); + Ok(()) + } + + /// Send CAT command to set mode on FT-817. + pub async fn set_mode(&mut self, mode: &RigMode) -> DynResult<()> { + // Ensure panel is unlocked and drop any stale bytes before sending. + let _ = self.unlock().await; + let _ = self.port.clear(ClearBuffer::Input); + + // Data byte 1 = mode, data bytes 2-4 = 0x00, command = 0x07. + let mode_code = encode_mode(mode); + tracing::debug!("FT-817 set_mode -> code 0x{:02X} ({:?})", mode_code, mode); + let frame = [mode_code, 0x00, 0x00, 0x00, CMD_SET_MODE]; + self.write_frame(&frame).await?; + self.port.flush().await?; + // Some rigs occasionally miss the first frame; send a second time after a short delay. + tokio::time::sleep(std::time::Duration::from_millis(80)).await; + self.write_frame(&frame).await?; + self.port.flush().await?; + self.update_vfo_mode(mode.clone()); + Ok(()) + } + + /// Send CAT command to control PTT on FT-817. + pub async fn set_ptt(&mut self, ptt: bool) -> DynResult<()> { + let opcode = if ptt { CMD_PTT_ON } else { CMD_PTT_OFF }; + // PTT on/off does not take a payload; CAT uses separate opcodes. + let frame = [0x00, 0x00, 0x00, 0x00, opcode]; + self.write_frame(&frame).await?; + Ok(()) + } + + /// Turn the radio on via CAT. The first frame is ignored while the CPU wakes, + /// so send a dummy payload before issuing the actual command. + pub async fn power_on(&mut self) -> DynResult<()> { + const POWER_ON_DUMMY: [u8; 5] = [0x00, 0x00, 0x00, 0x00, 0x00]; + self.port.write_all(&POWER_ON_DUMMY).await?; + self.port.flush().await?; + // Give the radio a moment to wake up and lock onto CAT framing. + tokio::time::sleep(std::time::Duration::from_millis(120)).await; + + let frame = [0x00, 0x00, 0x00, 0x00, CMD_POWER_ON]; + self.write_frame(&frame).await?; + self.port.flush().await?; + // Drop any boot noise that might remain in the input buffer before we start polling. + let _ = self.port.clear(ClearBuffer::Input); + Ok(()) + } + + /// Turn the radio off via CAT. + pub async fn power_off(&mut self) -> DynResult<()> { + let frame = [0x00, 0x00, 0x00, 0x00, CMD_POWER_OFF]; + self.write_frame(&frame).await?; + Ok(()) + } + + /// Toggle between VFO A/B. + pub async fn toggle_vfo(&mut self) -> DynResult<()> { + let frame = [0x00, 0x00, 0x00, 0x00, CMD_TOGGLE_VFO]; + self.write_frame(&frame).await?; + self.vfo_side = self.vfo_side.other(); + Ok(()) + } + + /// Enable front panel lock. + pub async fn lock(&mut self) -> DynResult<()> { + let frame = [0x00, 0x00, 0x00, 0x00, CMD_LOCK]; + self.write_frame(&frame).await?; + let mut buf = [0u8; 1]; + if let Err(e) = self.port.read_exact(&mut buf).await { + tracing::warn!("LOCK read failed: {:?}", e); + } else { + tracing::debug!("LOCK response: 0x{:02X}", buf[0]); + } + Ok(()) + } + + /// Disable front panel lock. + pub async fn unlock(&mut self) -> DynResult<()> { + let frame = [0x00, 0x00, 0x00, 0x00, CMD_UNLOCK]; + self.write_frame(&frame).await?; + let mut buf = [0u8; 1]; + if let Err(e) = self.port.read_exact(&mut buf).await { + tracing::warn!("UNLOCK read failed: {:?}", e); + } else { + tracing::debug!("UNLOCK response: 0x{:02X}", buf[0]); + } + Ok(()) + } + + /// Read the current signal strength meter (S-meter/PWR) from the radio. + /// + /// The returned value is the raw CAT meter byte (0-255). In receive it + /// represents S-meter level; in transmit it reports power/ALC depending on + /// rig state. + pub async fn get_signal_strength(&mut self) -> DynResult { + self.read_meter().await + } + + /// Read the current transmit power indication (raw meter value). + /// + /// The FT-817 reports the same meter byte for TX power as for the S-meter; + /// callers should interpret based on current PTT state. + pub async fn get_tx_power(&mut self) -> DynResult { + self.read_meter().await + } + + async fn read_status(&mut self) -> DynResult<(u64, RigMode)> { + // Status request returns frequency (4 BCD bytes, LSB first) and mode code. + let _ = self.port.clear(ClearBuffer::Input); + let frame = [0x00, 0x00, 0x00, 0x00, CMD_READ_STATUS]; + self.write_frame(&frame).await?; + + let mut buf = [0u8; 5]; + timeout(Self::READ_TIMEOUT, self.port.read_exact(&mut buf)) + .await + .map_err(|_| "CAT status read timeout")??; + + let freq = decode_freq_bcd([buf[0], buf[1], buf[2], buf[3]])?; + let mode = decode_mode(buf[4]); + Ok((freq, mode)) + } + + async fn read_meter(&mut self) -> DynResult { + let frame = [0x00, 0x00, 0x00, 0x00, CMD_READ_METER]; + self.write_frame(&frame).await?; + + let mut buf = [0u8; 1]; + timeout(Self::READ_TIMEOUT, self.port.read_exact(&mut buf)) + .await + .map_err(|_| "CAT meter read timeout")??; + Ok(buf[0]) + } + + async fn write_frame(&mut self, frame: &[u8; 5]) -> DynResult<()> { + self.port.write_all(frame).await?; + self.port.flush().await?; + Ok(()) + } + + fn update_vfo_freq(&mut self, freq: Freq) { + match self.vfo_side { + Ft817VfoSide::A => self.vfo_a_freq = Some(freq), + Ft817VfoSide::B => self.vfo_b_freq = Some(freq), + Ft817VfoSide::Unknown => { + // Try to infer which VFO we are on using cached values; default to A only. + if self.vfo_b_freq.map(|f| f.hz == freq.hz).unwrap_or(false) + && self.vfo_a_freq.is_none() + { + self.vfo_side = Ft817VfoSide::B; + self.vfo_b_freq = Some(freq); + } else { + self.vfo_side = Ft817VfoSide::A; + self.vfo_a_freq = Some(freq); + } + } + } + } + + fn update_vfo_mode(&mut self, mode: RigMode) { + match self.vfo_side { + Ft817VfoSide::A => self.vfo_a_mode = Some(mode), + Ft817VfoSide::B => self.vfo_b_mode = Some(mode), + Ft817VfoSide::Unknown => { + // Default to current VFO (assume A) when unknown. + self.vfo_a_mode = Some(mode); + } + } + } +} + +impl Rig for Ft817 { + fn info(&self) -> &RigInfo { + &self.info + } +} + +impl RigCat for Ft817 { + fn get_status<'a>(&'a mut self) -> RigStatusFuture<'a> { + Box::pin(async move { self.get_status().await }) + } + + fn set_freq<'a>( + &'a mut self, + freq: Freq, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { Ft817::set_freq(self, freq).await }) + } + + fn set_mode<'a>( + &'a mut self, + mode: RigMode, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { Ft817::set_mode(self, &mode).await }) + } + + fn set_ptt<'a>( + &'a mut self, + ptt: bool, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { Ft817::set_ptt(self, ptt).await }) + } + + fn power_on<'a>( + &'a mut self, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { Ft817::power_on(self).await }) + } + + fn power_off<'a>( + &'a mut self, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { Ft817::power_off(self).await }) + } + + fn get_signal_strength<'a>( + &'a mut self, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { Ft817::get_signal_strength(self).await }) + } + + fn get_tx_power<'a>( + &'a mut self, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { Ft817::get_tx_power(self).await }) + } + + fn get_tx_limit<'a>( + &'a mut self, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { Err("TX limit query not supported on FT-817".into()) }) + } + + fn set_tx_limit<'a>( + &'a mut self, + _limit: u8, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { Err("TX limit setting not supported on FT-817".into()) }) + } + + fn toggle_vfo<'a>( + &'a mut self, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { Ft817::toggle_vfo(self).await }) + } + + fn lock<'a>( + &'a mut self, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { Ft817::lock(self).await }) + } + + fn unlock<'a>( + &'a mut self, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { Ft817::unlock(self).await }) + } +} + +#[derive(Clone, Copy)] +enum Ft817VfoSide { + A, + B, + Unknown, +} + +impl Ft817VfoSide { + fn other(self) -> Self { + match self { + Ft817VfoSide::A => Ft817VfoSide::B, + Ft817VfoSide::B => Ft817VfoSide::A, + Ft817VfoSide::Unknown => Ft817VfoSide::A, + } + } +} + +// Command codes per Yaesu CAT protocol. +const CMD_SET_FREQ: u8 = 0x01; +const CMD_READ_STATUS: u8 = 0x03; +const CMD_SET_MODE: u8 = 0x07; +const CMD_PTT_ON: u8 = 0x08; +const CMD_PTT_OFF: u8 = 0x88; +const CMD_POWER_ON: u8 = 0x0F; +const CMD_POWER_OFF: u8 = 0x8F; +const CMD_TOGGLE_VFO: u8 = 0x81; +const CMD_LOCK: u8 = 0x00; +const CMD_UNLOCK: u8 = 0x80; +const CMD_READ_METER: u8 = 0xE7; + +fn encode_mode(mode: &RigMode) -> u8 { + match mode { + RigMode::LSB => 0x00, + RigMode::USB => 0x01, + RigMode::CW => 0x02, + RigMode::CWR => 0x03, + RigMode::AM => 0x04, + RigMode::WFM => 0x06, + RigMode::FM => 0x08, + RigMode::DIG => 0x0A, + RigMode::PKT => 0x0C, + RigMode::Other(_) => 0x00, + } +} + +fn decode_mode(code: u8) -> RigMode { + match code { + 0x00 => RigMode::LSB, + 0x01 => RigMode::USB, + 0x02 => RigMode::CW, + 0x03 => RigMode::CWR, + 0x04 => RigMode::AM, + 0x06 => RigMode::WFM, + 0x08 => RigMode::FM, + 0x0A => RigMode::DIG, + 0x0C => RigMode::PKT, + other => RigMode::Other(format!("0x{:02X}", other)), + } +} diff --git a/src/trx-bin/Cargo.toml b/src/trx-bin/Cargo.toml new file mode 100644 index 0000000..53935e1 --- /dev/null +++ b/src/trx-bin/Cargo.toml @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2025 Stanislaw Grams +# +# SPDX-License-Identifier: BSD-2-Clause + +[package] +name = "trx-bin" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { workspace = true, features = ["full"] } +tokio-serial = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +clap = { workspace = true, features = ["derive"] } +trx-backend = { path = "../trx-backend" } +trx-core = { path = "../trx-core" } +trx-frontend = { path = "../trx-frontend" } +trx-frontend-http = { path = "../trx-frontend/src/trx-frontend-http" } diff --git a/src/trx-bin/src/error.rs b/src/trx-bin/src/error.rs new file mode 100644 index 0000000..45240a0 --- /dev/null +++ b/src/trx-bin/src/error.rs @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +use std::error::Error; + +/// Detect the specific CAT decode error for invalid BCD digits. +pub fn is_invalid_bcd_error(err: &(dyn Error + 'static)) -> bool { + if err.to_string().contains("invalid BCD digit in frequency") { + return true; + } + err.source().map(is_invalid_bcd_error).unwrap_or(false) +} diff --git a/src/trx-bin/src/main.rs b/src/trx-bin/src/main.rs new file mode 100644 index 0000000..7e867ff --- /dev/null +++ b/src/trx-bin/src/main.rs @@ -0,0 +1,757 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +use std::net::SocketAddr; + +use clap::{Parser, ValueEnum}; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::signal; +use tokio::sync::{mpsc, oneshot, watch}; +use tokio::time::{self, Duration, Instant}; +use tracing::{debug, error, info, warn}; + +mod error; + +use crate::error::is_invalid_bcd_error; +use trx_backend::{build_rig, RigAccess, RigKind}; +use trx_core::radio::freq::Freq; +use trx_core::rig::command::RigCommand; +use trx_core::rig::request::RigRequest; +use trx_core::rig::state::{RigMode, RigSnapshot, RigState}; +use trx_core::rig::{RigCat, RigControl, RigRxStatus, RigStatus, RigTxStatus}; +use trx_core::{ClientCommand, ClientResponse, DynResult, RigError, RigResult}; +use trx_frontend::FrontendSpawner; +use trx_frontend_http::server::HttpFrontend; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +enum FrontendKind { + Http, +} + +const PKG_DESCRIPTION: &str = concat!(env!("CARGO_PKG_NAME"), " - ", env!("CARGO_PKG_DESCRIPTION")); +const PKG_LONG_ABOUT: &str = concat!( + env!("CARGO_PKG_DESCRIPTION"), + "\nHomepage: ", + env!("CARGO_PKG_HOMEPAGE") +); + +#[derive(Debug, Parser)] +#[command( + author = env!("CARGO_PKG_AUTHORS"), + version = env!("CARGO_PKG_VERSION"), + about = PKG_DESCRIPTION, + long_about = PKG_LONG_ABOUT +)] +struct Cli { + /// Rig backend to use (e.g. ft817) + #[arg(short = 'r', long = "rig", value_enum)] + rig: RigKind, + /// Access method to reach the rig CAT interface + #[arg(short = 'a', long = "access", value_enum, default_value_t = AccessKind::Serial)] + access: AccessKind, + /// Frontend to expose for control/status (e.g. http) + #[arg(short = 'f', long = "frontend", value_enum, default_value_t = FrontendKind::Http)] + frontend: FrontendKind, + /// Rig CAT address: + /// when access is serial: ; + /// when access is TCP: : + #[arg(value_name = "RIG_ADDR")] + rig_addr: String, + /// Optional callsign/owner label to show in the frontend + #[arg(short = 'c', long = "callsign")] + callsign: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +enum AccessKind { + Serial, + Tcp, +} + +/// Parse a serial rig address of the form " ". +fn parse_serial_addr(addr: &str) -> DynResult<(String, u32)> { + let mut parts = addr.split_whitespace(); + let path = parts + .next() + .ok_or("Serial rig address must be ' '")?; + let baud_str = parts + .next() + .ok_or("Serial rig address must be ' '")?; + if parts.next().is_some() { + return Err("Serial rig address must be ' ' (got extra data)".into()); + } + let baud: u32 = baud_str + .parse() + .map_err(|e| format!("Invalid baud '{}': {}", baud_str, e))?; + Ok((path.to_string(), baud)) +} + +#[tokio::main] +async fn main() -> DynResult<()> { + init_tracing(); + + let cli = Cli::parse(); + + let access = match cli.access { + AccessKind::Serial => { + let (path, baud) = parse_serial_addr(&cli.rig_addr)?; + info!( + "Starting trxd (rig: {}, access: serial {} @ {} baud)", + cli.rig, path, baud + ); + RigAccess::Serial { path, baud } + } + AccessKind::Tcp => { + info!( + "Starting trxd (rig: {}, access: tcp {})", + cli.rig, cli.rig_addr + ); + RigAccess::Tcp { + addr: cli.rig_addr.clone(), + } + } + }; + // Channel used to communicate with the rig task. + let (tx, rx) = mpsc::channel::(32); + let initial_state = RigState { + rig_info: None, + status: RigStatus { + freq: Freq { hz: 144_300_000 }, + mode: RigMode::USB, + tx_en: false, + vfo: None, + tx: Some(RigTxStatus { + power: None, + limit: None, + swr: None, + alc: None, + }), + rx: Some(RigRxStatus { sig: None }), + lock: Some(false), + }, + initialized: false, + control: RigControl { + rpt_offset_hz: None, + ctcss_hz: None, + dcs_code: None, + lock: Some(false), + clar_hz: None, + clar_on: None, + enabled: Some(false), + }, + }; + let (state_tx, state_rx) = watch::channel(initial_state.clone()); + + // Spawn the rig task. + let _rig_handle = tokio::spawn(rig_task(cli.rig, access, rx, state_tx, initial_state)); + + // Start TCP listener for clients. + let listen_addr = SocketAddr::from(([127, 0, 0, 1], 0)); + let listener = TcpListener::bind(listen_addr).await?; + let actual_addr = listener.local_addr()?; + info!("TCP listener started on {}", actual_addr); + + // Start simple HTTP status server on 127.0.0.1:8080. + let http_state_rx = state_rx.clone(); + if matches!(cli.frontend, FrontendKind::Http) { + HttpFrontend::spawn_frontend(http_state_rx, tx.clone(), cli.callsign.clone()); + } + + loop { + tokio::select! { + res = listener.accept() => { + let (socket, addr) = res?; + info!("New client connected: {}", addr); + + let tx_clone = tx.clone(); + tokio::spawn(async move { + if let Err(e) = handle_client(socket, addr, tx_clone).await { + error!("Client {} error: {:?}", addr, e); + } + }); + } + _ = signal::ctrl_c() => { + info!("Ctrl+C received, shutting down"); + break; + } + } + } + + Ok(()) +} + +/// Initialize logging/tracing. +fn init_tracing() { + // Uses default formatting and RUST_LOG if available. + tracing_subscriber::fmt().with_target(false).init(); +} + +/// Task that owns the TRX state and talks to the serial port. +async fn rig_task( + rig_kind: RigKind, + access: RigAccess, + mut rx: mpsc::Receiver, + state_tx: watch::Sender, + mut state: RigState, +) -> DynResult<()> { + info!("Opening rig backend {}", rig_kind); + match &access { + RigAccess::Serial { path, baud } => info!("Serial: {} @ {} baud", path, baud), + RigAccess::Tcp { addr } => info!("TCP CAT: {}", addr), + } + + let mut rig: Box = build_rig(rig_kind, access)?; + info!("Rig backend ready"); + + let mut poll = time::interval(Duration::from_millis(250)); + let mut poll_pause_until: Option = None; + let mut last_power_on: Option = None; + + // Initial bring-up and VFO priming. + let rig_info = rig.info().clone(); + state.rig_info = Some(rig_info); + if let Some(info) = state.rig_info.as_ref() { + info!( + "Rig info: {} {} {}", + info.manufacturer, info.model, info.revision + ); + } + let _ = state_tx.send(state.clone()); + if !state.control.enabled.unwrap_or(false) { + info!("Sending initial PowerOn to wake rig"); + match rig.power_on().await { + Ok(()) => { + state.control.enabled = Some(true); + time::sleep(Duration::from_secs(3)).await; + if let Err(e) = refresh_state_with_retry(&mut rig, &mut state, 2).await { + warn!( + "Initial PowerOn refresh failed: {:?}; retrying once after short delay", + e + ); + time::sleep(Duration::from_millis(500)).await; + if let Err(e2) = refresh_state_with_retry(&mut rig, &mut state, 1).await { + warn!( + "Initial PowerOn second refresh failed (continuing): {:?}", + e2 + ); + } + } + info!("Rig initialized after power on sequence"); + } + Err(e) => warn!("Initial PowerOn failed (continuing): {:?}", e), + } + } + if let Err(e) = prime_vfo_state(&mut rig, &mut state).await { + warn!("VFO priming failed: {:?}", e); + } + state.initialized = true; + let _ = state_tx.send(state.clone()); + + // Single-task loop: handle commands and periodic polling. + loop { + tokio::select! { + _ = poll.tick() => { + if let Some(until) = poll_pause_until { + if Instant::now() < until { + continue; + } else { + poll_pause_until = None; + } + } + if matches!(state.control.enabled, Some(false)) { + continue; + } + match refresh_state_with_retry(&mut rig, &mut state, 2).await { + Ok(()) => { let _ = state_tx.send(state.clone()); } + Err(e) => { + error!("CAT polling error: {:?}", e); + if let Some(last_on) = last_power_on { + if Instant::now().duration_since(last_on) < Duration::from_secs(5) { + poll_pause_until = Some(Instant::now() + Duration::from_millis(800)); + continue; + } + } + } + } + }, + maybe_req = rx.recv() => { + let Some(first_req) = maybe_req else { break; }; + let mut batch = vec![first_req]; + while let Ok(next) = rx.try_recv() { + batch.push(next); + } + while let Some(RigRequest { cmd, respond_to }) = batch.pop() { + let responders = vec![respond_to]; + let cmd_label = format!("{:?}", cmd); + let started = Instant::now(); + + let result: RigResult = { + let not_ready = !state.initialized + && !matches!(cmd, RigCommand::PowerOn | RigCommand::GetSnapshot); + if not_ready { + Err(RigError("rig not initialized yet".into())) + } else { + match cmd { + RigCommand::GetSnapshot => match refresh_state_with_retry(&mut rig, &mut state, 2).await { + Ok(()) => { + let _ = state_tx.send(state.clone()); + snapshot_from(&state) + } + Err(e) => { + error!("Failed to read CAT status: {:?}", e); + Err(RigError(format!("CAT error: {}", e))) + } + }, + RigCommand::SetFreq(freq) => { + info!("SetFreq requested: {} Hz", freq.hz); + if state.control.lock.unwrap_or(false) { + warn!("SetFreq blocked: panel lock is active"); + Err(RigError("panel is locked".into())) + } else { + let res = time::timeout(Duration::from_secs(1), rig.set_freq(freq)).await; + match res { + Ok(Ok(())) => { + state.apply_freq(freq); + poll_pause_until = Some(Instant::now() + Duration::from_millis(200)); + let _ = state_tx.send(state.clone()); + snapshot_from(&state) + } + Ok(Err(e)) => { + error!("Failed to send CAT SetFreq: {:?}", e); + Err(RigError(format!("CAT error: {}", e))) + } + Err(elapsed) => { + warn!("CAT SetFreq timed out ({:?}) but proceeding with state update", elapsed); + state.apply_freq(freq); + poll_pause_until = Some(Instant::now() + Duration::from_millis(200)); + let _ = state_tx.send(state.clone()); + snapshot_from(&state) + } + } + } + } + RigCommand::SetMode(mode) => { + info!("SetMode requested: {:?}", mode); + if state.control.lock.unwrap_or(false) { + warn!("SetMode blocked: panel lock is active"); + Err(RigError("panel is locked".into())) + } else { + let res = time::timeout(Duration::from_secs(1), rig.set_mode(mode.clone())).await; + match res { + Ok(Ok(())) => { + state.apply_mode(mode.clone()); + poll_pause_until = Some(Instant::now() + Duration::from_millis(200)); + let _ = state_tx.send(state.clone()); + snapshot_from(&state) + } + Ok(Err(e)) => { + error!("Failed to send CAT SetMode: {:?}", e); + Err(RigError(format!("CAT error: {}", e))) + } + Err(elapsed) => { + warn!("CAT SetMode timed out ({:?}) but proceeding with state update", elapsed); + state.apply_mode(mode.clone()); + poll_pause_until = Some(Instant::now() + Duration::from_millis(200)); + let _ = state_tx.send(state.clone()); + snapshot_from(&state) + } + } + } + } + RigCommand::SetPtt(ptt) => { + info!("SetPtt requested: {}", ptt); + if let Err(e) = rig.set_ptt(ptt).await { + error!("Failed to send CAT SetPtt: {:?}", e); + Err(RigError(format!("CAT error: {}", e))) + } else { + state.status.tx_en = ptt; + if !ptt { + if let Some(tx) = state.status.tx.as_mut() { + tx.power = Some(0); + tx.swr = Some(0.0); + } + } + state.status.lock = state.control.lock; + let _ = state_tx.send(state.clone()); + snapshot_from(&state) + } + } + RigCommand::PowerOn => { + info!("PowerOn requested"); + if let Err(e) = rig.power_on().await { + error!("Failed to send CAT PowerOn: {:?}", e); + Err(RigError(format!("CAT error: {}", e))) + } else { + state.control.enabled = Some(true); + time::sleep(Duration::from_secs(3)).await; + let now = Instant::now(); + poll_pause_until = Some(now + Duration::from_secs(3)); + last_power_on = Some(now); + match refresh_state_with_retry(&mut rig, &mut state, 2).await { + Ok(()) => { + let _ = state_tx.send(state.clone()); + snapshot_from(&state) + } + Err(e) => { + if is_invalid_bcd_error(e.as_ref()) { + warn!("Transient CAT decode after PowerOn (ignored): {:?}", e); + poll_pause_until = Some(Instant::now() + Duration::from_millis(1500)); + let _ = state_tx.send(state.clone()); + snapshot_from(&state) + } else { + error!("Failed to refresh after PowerOn: {:?}", e); + Err(RigError(format!("CAT error: {}", e))) + } + } + } + } + } + RigCommand::PowerOff => { + info!("PowerOff requested"); + if let Err(e) = rig.power_off().await { + error!("Failed to send CAT PowerOff: {:?}", e); + Err(RigError(format!("CAT error: {}", e))) + } else { + state.control.enabled = Some(false); + state.status.tx_en = false; + let _ = state_tx.send(state.clone()); + snapshot_from(&state) + } + } + RigCommand::ToggleVfo => { + info!("Toggle VFO requested"); + if state.control.lock.unwrap_or(false) { + warn!("ToggleVfo blocked: panel lock is active"); + Err(RigError("panel is locked".into())) + } else if let Err(e) = rig.toggle_vfo().await { + error!("Failed to send CAT ToggleVfo: {:?}", e); + Err(RigError(format!("CAT error: {}", e))) + } else { + time::sleep(Duration::from_millis(150)).await; + poll_pause_until = Some(Instant::now() + Duration::from_millis(300)); + match refresh_state_with_retry(&mut rig, &mut state, 2).await { + Ok(()) => { + let _ = state_tx.send(state.clone()); + snapshot_from(&state) + } + Err(e) => { + error!("Failed to refresh after ToggleVfo: {:?}", e); + Err(RigError(format!("CAT error: {}", e))) + } + } + } + } + RigCommand::GetTxLimit => match rig.get_tx_limit().await { + Ok(limit) => { + state + .status + .tx + .get_or_insert(RigTxStatus { power: None, limit: None, swr: None, alc: None }) + .limit = Some(limit); + let _ = state_tx.send(state.clone()); + snapshot_from(&state) + } + Err(e) => { + error!("Failed to read TX limit: {:?}", e); + Err(RigError(format!("CAT error: {}", e))) + } + }, + RigCommand::SetTxLimit(limit) => match rig.set_tx_limit(limit).await { + Ok(()) => { + state + .status + .tx + .get_or_insert(RigTxStatus { power: None, limit: None, swr: None, alc: None }) + .limit = Some(limit); + let _ = state_tx.send(state.clone()); + snapshot_from(&state) + } + Err(e) => { + error!("Failed to set TX limit: {:?}", e); + Err(RigError(format!("CAT error: {}", e))) + } + } + RigCommand::Lock => { + info!("Lock requested"); + match rig.lock().await { + Ok(()) => { + state.control.lock = Some(true); + state.status.lock = Some(true); + let _ = state_tx.send(state.clone()); + snapshot_from(&state) + } + Err(e) => { + error!("Failed to send CAT Lock: {:?}", e); + Err(RigError(format!("CAT error: {}", e))) + } + } + } + RigCommand::Unlock => { + info!("Unlock requested"); + match rig.unlock().await { + Ok(()) => { + state.control.lock = Some(false); + state.status.lock = Some(false); + let _ = state_tx.send(state.clone()); + snapshot_from(&state) + } + Err(e) => { + error!("Failed to send CAT Unlock: {:?}", e); + Err(RigError(format!("CAT error: {}", e))) + } + } + } + } + } + }; + + for tx in responders { + let _ = tx.send(result.clone()); + } + let elapsed = started.elapsed(); + if elapsed > Duration::from_millis(500) { + warn!("Rig command {} took {:?}", cmd_label, elapsed); + } else { + debug!("Rig command {} completed in {:?}", cmd_label, elapsed); + } + } + }, + } + } + + info!("rig_task shutting down (channel closed)"); + Ok(()) +} + +async fn refresh_state_from_cat(trx: &mut Box, state: &mut RigState) -> DynResult<()> { + let (freq, mode, vfo) = trx.get_status().await?; + state.control.enabled = Some(true); + state.apply_freq(freq); + state.apply_mode(mode); + state.status.vfo = vfo.clone(); + + if state.status.tx_en { + state.status.rx.get_or_insert(RigRxStatus { sig: None }).sig = Some(0); + } else if let Ok(meter) = trx.get_signal_strength().await { + let sig = map_signal_strength(&state.status.mode, meter); + state.status.rx.get_or_insert(RigRxStatus { sig: None }).sig = Some(sig); + } + if let Ok(limit) = trx.get_tx_limit().await { + state + .status + .tx + .get_or_insert(RigTxStatus { + power: None, + limit: None, + swr: None, + alc: None, + }) + .limit = Some(limit); + } + if state.status.tx_en { + if let Ok(power) = trx.get_tx_power().await { + state + .status + .tx + .get_or_insert(RigTxStatus { + power: None, + limit: None, + swr: None, + alc: None, + }) + .power = Some(power); + } + } + state.status.lock = Some(state.control.lock.unwrap_or(false)); + Ok(()) +} + +async fn refresh_state_with_retry( + trx: &mut Box, + state: &mut RigState, + attempts: usize, +) -> DynResult<()> { + let mut last_err: Option> = None; + for i in 0..attempts { + match refresh_state_from_cat(trx, state).await { + Ok(()) => return Ok(()), + Err(e) => { + let should_retry = is_invalid_bcd_error(e.as_ref()); + last_err = Some(e); + if should_retry && i + 1 < attempts { + warn!( + "Retrying CAT state read after invalid BCD (attempt {} of {})", + i + 1, + attempts + ); + time::sleep(Duration::from_millis(300)).await; + continue; + } else { + break; + } + } + } + } + + Err(last_err.unwrap_or_else(|| "Unknown CAT error".into())) +} + +async fn prime_vfo_state(trx: &mut Box, state: &mut RigState) -> DynResult<()> { + // Ensure panel is unlocked so we can CAT-control safely. + let _ = trx.unlock().await; + time::sleep(Duration::from_millis(100)).await; + + refresh_state_with_retry(trx, state, 2).await?; + time::sleep(Duration::from_millis(150)).await; + + trx.toggle_vfo().await?; + time::sleep(Duration::from_millis(150)).await; + refresh_state_with_retry(trx, state, 2).await?; + + trx.toggle_vfo().await?; + time::sleep(Duration::from_millis(150)).await; + refresh_state_with_retry(trx, state, 2).await?; + + Ok(()) +} + +/// Handle a single TCP client. +async fn handle_client( + socket: TcpStream, + addr: SocketAddr, + tx: mpsc::Sender, +) -> DynResult<()> { + let (reader, mut writer) = socket.into_split(); + let mut reader = BufReader::new(reader); + let mut line = String::new(); + + loop { + line.clear(); + let bytes_read = reader.read_line(&mut line).await?; + if bytes_read == 0 { + info!("Client {} disconnected", addr); + break; + } + + // Simple protocol: one line = one JSON command. + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + + let cmd: ClientCommand = match serde_json::from_str(trimmed) { + Ok(c) => c, + Err(e) => { + error!("Invalid JSON from {}: {} / {:?}", addr, trimmed, e); + let resp = ClientResponse { + success: false, + state: None, + error: Some(format!("Invalid JSON: {}", e)), + }; + let resp_line = serde_json::to_string(&resp)? + "\n"; + writer.write_all(resp_line.as_bytes()).await?; + writer.flush().await?; + continue; + } + }; + + // Map ClientCommand -> RigCommand. + let rig_cmd = match cmd { + ClientCommand::GetState => RigCommand::GetSnapshot, + ClientCommand::SetFreq { freq_hz } => RigCommand::SetFreq(Freq { hz: freq_hz }), + ClientCommand::SetMode { mode } => RigCommand::SetMode(parse_mode(&mode)), + ClientCommand::SetPtt { ptt } => RigCommand::SetPtt(ptt), + ClientCommand::PowerOn => RigCommand::PowerOn, + ClientCommand::PowerOff => RigCommand::PowerOff, + ClientCommand::ToggleVfo => RigCommand::ToggleVfo, + ClientCommand::GetTxLimit => RigCommand::GetTxLimit, + ClientCommand::SetTxLimit { limit } => RigCommand::SetTxLimit(limit), + }; + + let (resp_tx, resp_rx) = oneshot::channel(); + let req = RigRequest { + cmd: rig_cmd, + respond_to: resp_tx, + }; + + if let Err(e) = tx.send(req).await { + error!("Failed to send request to rig_task: {:?}", e); + let resp = ClientResponse { + success: false, + state: None, + error: Some("Internal error: rig task not available".into()), + }; + let resp_line = serde_json::to_string(&resp)? + "\n"; + writer.write_all(resp_line.as_bytes()).await?; + writer.flush().await?; + continue; + } + + match resp_rx.await { + Ok(Ok(snapshot)) => { + let resp = ClientResponse { + success: true, + state: Some(snapshot), + error: None, + }; + let resp_line = serde_json::to_string(&resp)? + "\n"; + writer.write_all(resp_line.as_bytes()).await?; + writer.flush().await?; + } + Ok(Err(err)) => { + let resp = ClientResponse { + success: false, + state: None, + error: Some(err.0), + }; + let resp_line = serde_json::to_string(&resp)? + "\n"; + writer.write_all(resp_line.as_bytes()).await?; + writer.flush().await?; + } + Err(e) => { + error!("Rig response oneshot recv error: {:?}", e); + let resp = ClientResponse { + success: false, + state: None, + error: Some("Internal error waiting for rig response".into()), + }; + let resp_line = serde_json::to_string(&resp)? + "\n"; + writer.write_all(resp_line.as_bytes()).await?; + writer.flush().await?; + } + } + } + + Ok(()) +} + +fn map_signal_strength(mode: &RigMode, raw: u8) -> i32 { + let val = raw as i32; + match mode { + RigMode::FM | RigMode::WFM => val.saturating_sub(128), + _ => val, + } +} + +/// Parse mode string coming from the client into RigMode. +fn parse_mode(s: &str) -> RigMode { + match s.to_uppercase().as_str() { + "LSB" => RigMode::LSB, + "USB" => RigMode::USB, + "CW" => RigMode::CW, + "CWR" => RigMode::CWR, + "AM" => RigMode::AM, + "FM" => RigMode::FM, + "DIG" | "DIGI" => RigMode::DIG, + "PKT" | "PACKET" => RigMode::PKT, + other => RigMode::Other(other.to_string()), + } +} + +fn snapshot_from(state: &RigState) -> RigResult { + state + .snapshot() + .ok_or_else(|| RigError("Rig info unavailable".into())) +} diff --git a/src/trx-core/Cargo.toml b/src/trx-core/Cargo.toml new file mode 100644 index 0000000..eb419a7 --- /dev/null +++ b/src/trx-core/Cargo.toml @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2025 Stanislaw Grams +# +# SPDX-License-Identifier: BSD-2-Clause + +[package] +name = "trx-core" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { workspace = true, features = ["full"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tracing = { workspace = true } diff --git a/src/trx-core/src/client.rs b/src/trx-core/src/client.rs new file mode 100644 index 0000000..688d4d1 --- /dev/null +++ b/src/trx-core/src/client.rs @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +use serde::{Deserialize, Serialize}; + +use crate::rig::state::RigSnapshot; + +/// Command received from network clients (JSON). +#[derive(Debug, Deserialize)] +#[serde(tag = "cmd", rename_all = "snake_case")] +pub enum ClientCommand { + GetState, + SetFreq { freq_hz: u64 }, + SetMode { mode: String }, + SetPtt { ptt: bool }, + PowerOn, + PowerOff, + ToggleVfo, + GetTxLimit, + SetTxLimit { limit: u8 }, +} + +/// Response sent to network clients over TCP. +#[derive(Debug, Serialize)] +pub struct ClientResponse { + pub success: bool, + pub state: Option, + pub error: Option, +} diff --git a/src/trx-core/src/lib.rs b/src/trx-core/src/lib.rs new file mode 100644 index 0000000..1c77665 --- /dev/null +++ b/src/trx-core/src/lib.rs @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +pub mod client; +pub mod math; +pub mod radio; +pub mod rig; + +pub type DynResult = Result>; + +pub use client::{ClientCommand, ClientResponse}; +pub use rig::command::RigCommand; +pub use rig::request::RigRequest; +pub use rig::response::{RigError, RigResult}; +pub use rig::state::{RigMode, RigSnapshot, RigState}; diff --git a/src/trx-core/src/math/bcd.rs b/src/trx-core/src/math/bcd.rs new file mode 100644 index 0000000..cd16c91 --- /dev/null +++ b/src/trx-core/src/math/bcd.rs @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +use crate::DynResult; + +/// Encode frequency in Hz into 4 BCD bytes (10 Hz resolution) used by Yaesu CAT. +pub fn encode_freq_bcd(freq_hz: u64) -> DynResult<[u8; 4]> { + if !freq_hz.is_multiple_of(10) { + return Err("frequency must be a multiple of 10 Hz for CAT encoding".into()); + } + + let mut n = freq_hz / 10; // FT-817 uses 10 Hz units. + if n > 99_999_999 { + return Err("frequency out of range for CAT BCD encoding".into()); + } + + let mut digits = [0u8; 8]; + for i in (0..8).rev() { + digits[i] = (n % 10) as u8; + n /= 10; + } + + let mut out = [0u8; 4]; + for i in 0..4 { + out[i] = (digits[i * 2] << 4) | digits[i * 2 + 1]; + } + + Ok(out) +} + +/// Decode 4 BCD bytes (10 Hz resolution) into frequency in Hz. +pub fn decode_freq_bcd(bytes: [u8; 4]) -> DynResult { + let mut value = 0u64; + + for b in bytes { + let high = (b >> 4) & 0x0F; + let low = b & 0x0F; + if high >= 10 || low >= 10 { + return Err("invalid BCD digit in frequency".into()); + } + + value = value * 10 + u64::from(high); + value = value * 10 + u64::from(low); + } + + Ok(value * 10) // Convert back to Hz from 10 Hz units. +} diff --git a/src/trx-core/src/math/mod.rs b/src/trx-core/src/math/mod.rs new file mode 100644 index 0000000..22fbbf1 --- /dev/null +++ b/src/trx-core/src/math/mod.rs @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +pub mod bcd; + +pub use bcd::{decode_freq_bcd, encode_freq_bcd}; diff --git a/src/trx-core/src/radio/freq.rs b/src/trx-core/src/radio/freq.rs new file mode 100644 index 0000000..f03eee4 --- /dev/null +++ b/src/trx-core/src/radio/freq.rs @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +use serde::{Deserialize, Serialize}; + +const SPEED_OF_LIGHT_M_PER_S: f64 = 299_792_458.0; + +/// Supported band range in Hz. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Band { + pub low_hz: u64, + pub high_hz: u64, + pub tx_allowed: bool, +} + +impl Band { + /// Midpoint frequency of the band in Hz. + #[must_use] + pub fn center_hz(&self) -> u64 { + u64::midpoint(self.low_hz, self.high_hz) + } +} + +/// Frequency wrapper (Hz). +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct Freq { + pub hz: u64, +} + +impl Freq { + #[must_use] + pub fn new(hz: u64) -> Self { + Self { hz } + } + + /// Return the band name for this frequency, if any, using the provided band list. + pub fn band_name(&self, bands: &[Band]) -> Option { + band_for_freq(bands, self).map(band_name) + } +} + +/// Find the band that contains the given frequency (inclusive), if any. +pub fn band_for_freq<'a>(bands: &'a [Band], freq: &Freq) -> Option<&'a Band> { + bands + .iter() + .find(|b| freq.hz >= b.low_hz && freq.hz <= b.high_hz) +} + +/// Convert a frequency in Hz to a human-friendly wavelength string. +/// +/// Values above one meter are rounded to the nearest meter; shorter wavelengths +/// are shown in centimeters. +pub fn wavelength_label(freq_hz: u64) -> String { + if freq_hz == 0 { + return "-".to_string(); + } + + let wavelength_m = SPEED_OF_LIGHT_M_PER_S / (freq_hz as f64); + if wavelength_m >= 1.0 { + format!("{:.0}m", wavelength_m.round()) + } else { + format!("{:.0}cm", (wavelength_m * 100.0).round()) + } +} + +/// Derive a human-friendly band label from a band's wavelength. +/// +/// The label is computed from the wavelength at the band's center frequency. +pub fn band_name(band: &Band) -> String { + wavelength_label(band.center_hz()) +} diff --git a/src/trx-core/src/radio/mod.rs b/src/trx-core/src/radio/mod.rs new file mode 100644 index 0000000..b40c5e9 --- /dev/null +++ b/src/trx-core/src/radio/mod.rs @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +pub mod freq; + +pub use freq::{band_for_freq, band_name, wavelength_label, Band, Freq}; diff --git a/src/trx-core/src/rig/command.rs b/src/trx-core/src/rig/command.rs new file mode 100644 index 0000000..ee2fbcc --- /dev/null +++ b/src/trx-core/src/rig/command.rs @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +use crate::radio::freq::Freq; +use crate::RigMode; + +/// Internal command handled by the rig task. +#[derive(Debug, Clone)] +pub enum RigCommand { + GetSnapshot, + SetFreq(Freq), + SetMode(RigMode), + SetPtt(bool), + PowerOn, + PowerOff, + ToggleVfo, + GetTxLimit, + SetTxLimit(u8), + Lock, + Unlock, +} diff --git a/src/trx-core/src/rig/mod.rs b/src/trx-core/src/rig/mod.rs new file mode 100644 index 0000000..b17cef1 --- /dev/null +++ b/src/trx-core/src/rig/mod.rs @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +use std::future::Future; +use std::pin::Pin; + +use serde::Serialize; + +use crate::radio::freq::{Band, Freq}; +use crate::{DynResult, RigMode}; + +/// Alias to reduce type complexity in RigCat. +pub type RigStatusFuture<'a> = + Pin)>> + Send + 'a>>; + +pub mod command; +pub mod request; +pub mod response; +pub mod state; + +/// How this backend communicates with the rig. +#[derive(Debug, Clone, Serialize)] +pub enum RigAccessMethod { + Serial { path: String, baud: u32 }, + Tcp { addr: String }, +} + +/// Static info describing a rig backend. +#[derive(Debug, Clone, Serialize)] +pub struct RigInfo { + pub manufacturer: &'static str, + pub model: &'static str, + pub revision: &'static str, + pub capabilities: RigCapabilities, + pub access: RigAccessMethod, +} + +#[derive(Debug, Clone, Serialize)] +pub struct RigCapabilities { + pub supported_bands: Vec, + pub supported_modes: Vec, + pub num_vfos: usize, + pub lock: bool, + pub lockable: bool, + pub attenuator: bool, + pub preamp: bool, + pub rit: bool, + pub rpt: bool, + pub split: bool, +} + +/// Common interface for rig backends. +pub trait Rig { + fn info(&self) -> &RigInfo; +} + +/// Common CAT control operations any rig backend should implement. +pub trait RigCat: Rig + Send { + fn get_status<'a>(&'a mut self) -> RigStatusFuture<'a>; + + fn set_freq<'a>( + &'a mut self, + freq: Freq, + ) -> Pin> + Send + 'a>>; + + fn set_mode<'a>( + &'a mut self, + mode: RigMode, + ) -> Pin> + Send + 'a>>; + + fn set_ptt<'a>( + &'a mut self, + ptt: bool, + ) -> Pin> + Send + 'a>>; + + fn power_on<'a>(&'a mut self) -> Pin> + Send + 'a>>; + + fn power_off<'a>(&'a mut self) -> Pin> + Send + 'a>>; + + fn get_signal_strength<'a>( + &'a mut self, + ) -> Pin> + Send + 'a>>; + + fn get_tx_power<'a>(&'a mut self) -> Pin> + Send + 'a>>; + + fn get_tx_limit<'a>(&'a mut self) -> Pin> + Send + 'a>>; + + fn set_tx_limit<'a>( + &'a mut self, + limit: u8, + ) -> Pin> + Send + 'a>>; + + fn toggle_vfo<'a>(&'a mut self) -> Pin> + Send + 'a>>; + + fn lock<'a>(&'a mut self) -> Pin> + Send + 'a>>; + + fn unlock<'a>(&'a mut self) -> Pin> + Send + 'a>>; +} + +/// Snapshot of a rig's status that every backend can expose. +#[derive(Debug, Clone, Serialize)] +pub struct RigStatus { + pub freq: Freq, + pub mode: RigMode, + pub tx_en: bool, + pub vfo: Option, + pub tx: Option, + pub rx: Option, + pub lock: Option, +} + +/// Trait for presenting rig status in a backend-agnostic way. +pub trait RigStatusProvider { + fn status(&self) -> RigStatus; +} + +#[derive(Debug, Clone, Serialize)] +pub struct RigVfo { + pub entries: Vec, + /// Index into `entries` for the active VFO, if known. + pub active: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct RigVfoEntry { + pub name: String, + pub freq: Freq, + pub mode: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct RigTxStatus { + pub power: Option, + pub limit: Option, + pub swr: Option, + pub alc: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct RigRxStatus { + pub sig: Option, +} + +/// Configurable control settings that can be pushed to the rig. +#[derive(Debug, Clone, Serialize)] +pub struct RigControl { + pub enabled: Option, + pub lock: Option, + pub clar_hz: Option, + pub clar_on: Option, + pub rpt_offset_hz: Option, + pub ctcss_hz: Option, + pub dcs_code: Option, +} diff --git a/src/trx-core/src/rig/request.rs b/src/trx-core/src/rig/request.rs new file mode 100644 index 0000000..0dd1ae0 --- /dev/null +++ b/src/trx-core/src/rig/request.rs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +use tokio::sync::oneshot; + +use crate::{RigCommand, RigResult, RigSnapshot}; + +/// Request sent to the rig task. +#[derive(Debug)] +pub struct RigRequest { + pub cmd: RigCommand, + pub respond_to: oneshot::Sender>, +} diff --git a/src/trx-core/src/rig/response.rs b/src/trx-core/src/rig/response.rs new file mode 100644 index 0000000..06f5349 --- /dev/null +++ b/src/trx-core/src/rig/response.rs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +use serde::Serialize; + +/// Error type returned by rig requests. +#[derive(Debug, Clone, Serialize)] +pub struct RigError(pub String); + +pub type RigResult = Result; + +impl From for RigError { + fn from(value: String) -> Self { + RigError(value) + } +} + +impl From<&str> for RigError { + fn from(value: &str) -> Self { + RigError(value.to_string()) + } +} diff --git a/src/trx-core/src/rig/state.rs b/src/trx-core/src/rig/state.rs new file mode 100644 index 0000000..78e8b47 --- /dev/null +++ b/src/trx-core/src/rig/state.rs @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +use serde::{Deserialize, Serialize}; + +use crate::rig::{RigControl, RigInfo, RigStatus, RigStatusProvider}; + +/// Simple transceiver state representation held by the rig task. +#[derive(Debug, Clone, Serialize)] +pub struct RigState { + #[serde(skip_deserializing)] + pub rig_info: Option, + pub status: RigStatus, + pub initialized: bool, + #[serde(skip_serializing, skip_deserializing)] + pub control: RigControl, +} + +/// Mode supported by the rig. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum RigMode { + LSB, + USB, + CW, + CWR, + AM, + WFM, + FM, + DIG, + PKT, + Other(String), +} + +impl RigStatusProvider for RigState { + fn status(&self) -> RigStatus { + self.status.clone() + } +} + +impl RigState { + pub fn band_name(&self) -> Option { + self.rig_info.as_ref().and_then(|info| { + self.status + .freq + .band_name(&info.capabilities.supported_bands) + }) + } + + /// Produce an immutable snapshot suitable for sharing with clients. + pub fn snapshot(&self) -> Option { + let info = self.rig_info.clone()?; + Some(RigSnapshot { + info, + status: self.status.clone(), + band: self.band_name(), + enabled: self.control.enabled, + initialized: self.initialized, + }) + } + + /// Apply a frequency change into the state. + pub fn apply_freq(&mut self, freq: crate::radio::freq::Freq) { + self.status.freq = freq; + } + + /// Apply a mode change into the state. + pub fn apply_mode(&mut self, mode: RigMode) { + self.status.mode = mode; + } + + /// Apply a PTT change, resetting meters on TX off. + pub fn apply_ptt(&mut self, ptt: bool) { + self.status.tx_en = ptt; + self.status.lock = self.control.lock; + if !ptt { + if let Some(tx) = self.status.tx.as_mut() { + tx.power = Some(0); + tx.swr = Some(0.0); + } + } + } +} + +/// Read-only projection of state shared with clients. +#[derive(Debug, Clone, Serialize)] +pub struct RigSnapshot { + pub info: RigInfo, + pub status: RigStatus, + pub band: Option, + pub enabled: Option, + pub initialized: bool, +} diff --git a/src/trx-frontend/Cargo.toml b/src/trx-frontend/Cargo.toml new file mode 100644 index 0000000..91f5058 --- /dev/null +++ b/src/trx-frontend/Cargo.toml @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2025 Stanislaw Grams +# +# SPDX-License-Identifier: BSD-2-Clause + +[package] +name = "trx-frontend" +version = "0.1.0" +edition = "2021" + +[dependencies] +trx-core = { path = "../trx-core" } +tokio = { workspace = true, features = ["sync"] } diff --git a/src/trx-frontend/src/lib.rs b/src/trx-frontend/src/lib.rs new file mode 100644 index 0000000..7570745 --- /dev/null +++ b/src/trx-frontend/src/lib.rs @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +use tokio::sync::{mpsc, watch}; +use tokio::task::JoinHandle; + +use trx_core::{RigRequest, RigState}; + +/// Trait implemented by concrete frontends to expose a runner entrypoint. +pub trait FrontendSpawner { + fn spawn_frontend( + state_rx: watch::Receiver, + rig_tx: mpsc::Sender, + callsign: Option, + ) -> JoinHandle<()>; +} diff --git a/src/trx-frontend/src/trx-frontend-http/Cargo.toml b/src/trx-frontend/src/trx-frontend-http/Cargo.toml new file mode 100644 index 0000000..2730b13 --- /dev/null +++ b/src/trx-frontend/src/trx-frontend-http/Cargo.toml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2025 Stanislaw Grams +# +# SPDX-License-Identifier: BSD-2-Clause + +[package] +name = "trx-frontend-http" +version = "0.1.0" +edition = "2021" + +[dependencies] +trx-core = { path = "../../../trx-core" } +trx-frontend = { path = "../.." } +tokio = { workspace = true, features = ["full"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tracing = { workspace = true } +actix-web = "=4.4.1" +tokio-stream = { version = "0.1", features = ["sync"] } +futures-util = "0.3" +bytes = "1" diff --git a/src/trx-frontend/src/trx-frontend-http/assets/trx-favicon.png b/src/trx-frontend/src/trx-frontend-http/assets/trx-favicon.png new file mode 100644 index 0000000..f37ceec Binary files /dev/null and b/src/trx-frontend/src/trx-frontend-http/assets/trx-favicon.png differ diff --git a/src/trx-frontend/src/trx-frontend-http/assets/trx-logo.png b/src/trx-frontend/src/trx-frontend-http/assets/trx-logo.png new file mode 100644 index 0000000..0109d7f Binary files /dev/null and b/src/trx-frontend/src/trx-frontend-http/assets/trx-logo.png differ diff --git a/src/trx-frontend/src/trx-frontend-http/src/api.rs b/src/trx-frontend/src/trx-frontend-http/src/api.rs new file mode 100644 index 0000000..da27e06 --- /dev/null +++ b/src/trx-frontend/src/trx-frontend-http/src/api.rs @@ -0,0 +1,301 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +use actix_web::{get, post, web, HttpResponse, Responder}; +use actix_web::{http::header, Error}; +use bytes::Bytes; +use futures_util::stream::{once, select, StreamExt}; +use tokio::sync::{mpsc, oneshot, watch}; +use tokio::time::{self, Duration}; +use tokio_stream::wrappers::{IntervalStream, WatchStream}; + +use trx_core::radio::freq::Freq; +use trx_core::rig::{RigAccessMethod, RigCapabilities, RigInfo}; +use trx_core::{ClientResponse, RigCommand, RigMode, RigRequest, RigSnapshot, RigState}; + +use crate::server::status; + +const FAVICON_BYTES: &[u8] = + include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/trx-favicon.png")); +const LOGO_BYTES: &[u8] = + include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/trx-logo.png")); + +#[get("/status")] +pub async fn status_api( + state: web::Data>, +) -> Result { + let state = wait_for_view(state.get_ref().clone()).await?; + Ok(HttpResponse::Ok().json(state)) +} + +#[get("/events")] +pub async fn events(state: web::Data>) -> Result { + let rx = state.get_ref().clone(); + let initial = wait_for_view(rx.clone()).await?; + + let initial_json = + serde_json::to_string(&initial).map_err(actix_web::error::ErrorInternalServerError)?; + let initial_stream = + once(async move { Ok::(Bytes::from(format!("data: {initial_json}\n\n"))) }); + + let updates = WatchStream::new(rx).filter_map(|state| async move { + state + .snapshot() + .and_then(|v| serde_json::to_string(&v).ok()) + .map(|json| Ok::(Bytes::from(format!("data: {json}\n\n")))) + }); + + let pings = IntervalStream::new(time::interval(Duration::from_secs(10))) + .map(|_| Ok::(Bytes::from(": ping\n\n"))); + + let stream = initial_stream.chain(select(pings, updates)); + + Ok(HttpResponse::Ok() + .insert_header((header::CONTENT_TYPE, "text/event-stream")) + .insert_header((header::CACHE_CONTROL, "no-cache")) + .insert_header((header::CONNECTION, "keep-alive")) + .streaming(stream)) +} + +#[post("/toggle_power")] +pub async fn toggle_power( + state: web::Data>, + rig_tx: web::Data>, +) -> Result { + let desired_on = !matches!(state.get_ref().borrow().control.enabled, Some(true)); + let cmd = if desired_on { + RigCommand::PowerOn + } else { + RigCommand::PowerOff + }; + send_command(&rig_tx, cmd).await +} + +#[post("/toggle_vfo")] +pub async fn toggle_vfo( + rig_tx: web::Data>, +) -> Result { + send_command(&rig_tx, RigCommand::ToggleVfo).await +} + +#[post("/lock")] +pub async fn lock_panel( + rig_tx: web::Data>, +) -> Result { + send_command(&rig_tx, RigCommand::Lock).await +} + +#[post("/unlock")] +pub async fn unlock_panel( + rig_tx: web::Data>, +) -> Result { + send_command(&rig_tx, RigCommand::Unlock).await +} + +#[derive(serde::Deserialize)] +pub struct FreqQuery { + pub hz: u64, +} + +#[post("/set_freq")] +pub async fn set_freq( + query: web::Query, + rig_tx: web::Data>, +) -> Result { + send_command(&rig_tx, RigCommand::SetFreq(Freq { hz: query.hz })).await +} + +#[derive(serde::Deserialize)] +pub struct ModeQuery { + pub mode: String, +} + +#[post("/set_mode")] +pub async fn set_mode( + query: web::Query, + rig_tx: web::Data>, +) -> Result { + let mode = parse_mode(&query.mode); + send_command(&rig_tx, RigCommand::SetMode(mode)).await +} + +#[derive(serde::Deserialize)] +pub struct PttQuery { + pub ptt: String, +} + +#[post("/set_ptt")] +pub async fn set_ptt( + query: web::Query, + rig_tx: web::Data>, +) -> Result { + let ptt = match query.ptt.to_ascii_lowercase().as_str() { + "1" | "true" | "on" => Ok(true), + "0" | "false" | "off" => Ok(false), + other => Err(actix_web::error::ErrorBadRequest(format!( + "invalid ptt parameter: {other}" + ))), + }?; + send_command(&rig_tx, RigCommand::SetPtt(ptt)).await +} + +#[derive(serde::Deserialize)] +pub struct TxLimitQuery { + pub limit: u8, +} + +#[post("/set_tx_limit")] +pub async fn set_tx_limit( + query: web::Query, + rig_tx: web::Data>, +) -> Result { + send_command(&rig_tx, RigCommand::SetTxLimit(query.limit)).await +} + +pub fn configure(cfg: &mut web::ServiceConfig) { + cfg.service(index) + .service(status_api) + .service(events) + .service(toggle_power) + .service(toggle_vfo) + .service(lock_panel) + .service(unlock_panel) + .service(set_freq) + .service(set_mode) + .service(set_ptt) + .service(set_tx_limit) + .service(favicon) + .service(logo); +} + +#[get("/")] +async fn index(callsign: web::Data>) -> impl Responder { + HttpResponse::Ok() + .insert_header((header::CONTENT_TYPE, "text/html; charset=utf-8")) + .body(status::index_html(callsign.get_ref().as_deref())) +} + +#[get("/favicon.ico")] +async fn favicon() -> impl Responder { + HttpResponse::Ok() + .insert_header((header::CONTENT_TYPE, "image/png")) + .body(FAVICON_BYTES) +} + +#[get("/logo.png")] +async fn logo() -> impl Responder { + HttpResponse::Ok() + .insert_header((header::CONTENT_TYPE, "image/png")) + .body(LOGO_BYTES) +} + +async fn send_command( + rig_tx: &mpsc::Sender, + cmd: RigCommand, +) -> Result { + let (resp_tx, resp_rx) = oneshot::channel(); + rig_tx + .send(RigRequest { + cmd, + respond_to: resp_tx, + }) + .await + .map_err(|e| { + actix_web::error::ErrorInternalServerError(format!("failed to send to rig: {e:?}")) + })?; + + let resp = tokio::time::timeout(Duration::from_secs(8), resp_rx) + .await + .map_err(|_| actix_web::error::ErrorGatewayTimeout("rig response timeout"))?; + + match resp { + Ok(Ok(snapshot)) => Ok(HttpResponse::Ok().json(ClientResponse { + success: true, + state: Some(snapshot), + error: None, + })), + Ok(Err(err)) => Ok(HttpResponse::BadRequest().json(ClientResponse { + success: false, + state: None, + error: Some(err.0), + })), + Err(e) => Err(actix_web::error::ErrorInternalServerError(format!( + "rig response channel error: {e:?}" + ))), + } +} + +async fn wait_for_view(mut rx: watch::Receiver) -> Result { + if let Some(view) = rx.borrow().snapshot() { + return Ok(view); + } + + while rx.changed().await.is_ok() { + if let Some(view) = rx.borrow().snapshot() { + return Ok(view); + } + } + + // Fallback: build a minimal snapshot if rig info is missing. + let state = rx.borrow().clone(); + Ok(RigSnapshot { + info: state + .rig_info + .clone() + .unwrap_or_else(|| RigInfoPlaceholder.into()), + status: state.status, + band: None, + enabled: state.control.enabled, + initialized: state.initialized, + }) +} + +struct RigInfoPlaceholder; + +impl Default for RigInfoPlaceholder { + fn default() -> Self { + RigInfoPlaceholder + } +} + +impl From for RigInfo { + fn from(_: RigInfoPlaceholder) -> Self { + RigInfo { + manufacturer: "Unknown", + model: "Rig", + revision: "", + capabilities: RigCapabilities { + supported_bands: vec![], + supported_modes: vec![], + num_vfos: 0, + lock: false, + lockable: false, + attenuator: false, + preamp: false, + rit: false, + rpt: false, + split: false, + }, + access: RigAccessMethod::Serial { + path: "".into(), + baud: 0, + }, + } + } +} + +fn parse_mode(s: &str) -> RigMode { + match s.to_ascii_uppercase().as_str() { + "LSB" => RigMode::LSB, + "USB" => RigMode::USB, + "CW" => RigMode::CW, + "CWR" => RigMode::CWR, + "AM" => RigMode::AM, + "FM" => RigMode::FM, + "WFM" => RigMode::WFM, + "DIG" | "DIGI" => RigMode::DIG, + "PKT" | "PACKET" => RigMode::PKT, + other => RigMode::Other(other.to_string()), + } +} diff --git a/src/trx-frontend/src/trx-frontend-http/src/lib.rs b/src/trx-frontend/src/trx-frontend-http/src/lib.rs new file mode 100644 index 0000000..9306a09 --- /dev/null +++ b/src/trx-frontend/src/trx-frontend-http/src/lib.rs @@ -0,0 +1,5 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +pub mod server; diff --git a/src/trx-frontend/src/trx-frontend-http/src/server.rs b/src/trx-frontend/src/trx-frontend-http/src/server.rs new file mode 100644 index 0000000..41105b7 --- /dev/null +++ b/src/trx-frontend/src/trx-frontend-http/src/server.rs @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +#[path = "api.rs"] +mod api; +#[path = "status.rs"] +pub mod status; + +use actix_web::dev::Server; +use actix_web::{web, App, HttpServer}; +use tokio::signal; +use tokio::sync::{mpsc, watch}; +use tokio::task::JoinHandle; +use tracing::{error, info}; + +use trx_core::RigRequest; +use trx_core::RigState; +use trx_frontend::FrontendSpawner; + +/// HTTP frontend implementation. +pub struct HttpFrontend; + +impl FrontendSpawner for HttpFrontend { + fn spawn_frontend( + state_rx: watch::Receiver, + rig_tx: mpsc::Sender, + callsign: Option, + ) -> JoinHandle<()> { + tokio::spawn(async move { + if let Err(e) = serve(state_rx, rig_tx, callsign).await { + error!("HTTP status server error: {:?}", e); + } + }) + } +} + +async fn serve( + state_rx: watch::Receiver, + rig_tx: mpsc::Sender, + callsign: Option, +) -> Result<(), actix_web::Error> { + let addr = ("127.0.0.1", 8080); + let server = build_server(addr, state_rx, rig_tx, callsign)?; + let handle = server.handle(); + tokio::spawn(async move { + let _ = signal::ctrl_c().await; + handle.stop(false).await; + }); + info!("HTTP status server on {}:{}", addr.0, addr.1); + server.await?; + Ok(()) +} + +fn build_server( + addr: (&str, u16), + state_rx: watch::Receiver, + rig_tx: mpsc::Sender, + callsign: Option, +) -> Result { + let state_data = web::Data::new(state_rx); + let rig_tx = web::Data::new(rig_tx); + let callsign = web::Data::new(callsign); + + let server = HttpServer::new(move || { + App::new() + .app_data(state_data.clone()) + .app_data(rig_tx.clone()) + .app_data(callsign.clone()) + .configure(api::configure) + }) + .shutdown_timeout(1) + .disable_signals() + .bind(addr)? + .run(); + Ok(server) +} + +pub fn configure(cfg: &mut web::ServiceConfig) { + cfg.configure(api::configure); +} diff --git a/src/trx-frontend/src/trx-frontend-http/src/status.rs b/src/trx-frontend/src/trx-frontend-http/src/status.rs new file mode 100644 index 0000000..d1133da --- /dev/null +++ b/src/trx-frontend/src/trx-frontend-http/src/status.rs @@ -0,0 +1,588 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +const PKG_NAME: &str = env!("CARGO_PKG_NAME"); +const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub fn index_html(callsign: Option<&str>) -> String { + INDEX_HTML_TEMPLATE + .replace("{pkg}", PKG_NAME) + .replace("{ver}", PKG_VERSION) + .replace("{callsign_opt}", callsign.unwrap_or("")) +} + +const INDEX_HTML_TEMPLATE: &str = r##" + + + + {pkg} v{ver} status + + + + +
+
+
+
+
Rig status
+
{pkg} v{ver}
+
+ +
+
+
Initializing (rig)…
+
+
+ +
+ + + +"##;