diff --git a/Cargo.lock b/Cargo.lock index 4c3c06f..e898465 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,12 +61,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "anyhow" -version = "1.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" - [[package]] name = "arrayvec" version = "0.7.6" @@ -454,12 +448,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "generic-array" version = "0.14.7" @@ -497,19 +485,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "getrandom" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", - "wasip3", -] - [[package]] name = "git-version" version = "0.3.9" @@ -532,9 +507,9 @@ dependencies = [ [[package]] name = "glam" -version = "0.32.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34627c5158214743a374170fed714833fdf4e4b0cbcc1ea98417866a4c5d4441" +checksum = "74a4d85559e2637d3d839438b5b3d75c31e655276f9544d72475c36b92fabbed" [[package]] name = "glob" @@ -542,15 +517,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - [[package]] name = "hashbrown" version = "0.16.1" @@ -579,22 +545,14 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "indexmap" -version = "2.13.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", + "hashbrown", ] [[package]] @@ -653,17 +611,11 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" -version = "0.2.182" +version = "0.2.179" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" [[package]] name = "libloading" @@ -710,9 +662,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "memchr" -version = "2.8.0" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "minimal-lexical" @@ -825,9 +777,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-integer" @@ -965,16 +917,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "primal-check" version = "0.3.4" @@ -986,9 +928,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] @@ -1007,7 +949,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -1030,7 +972,7 @@ dependencies = [ "rustls-pki-types", "rustls-platform-verifier", "slab", - "thiserror 2.0.18", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -1052,9 +994,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] @@ -1083,7 +1025,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.5", + "rand_core 0.9.3", ] [[package]] @@ -1103,7 +1045,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.5", + "rand_core 0.9.3", ] [[package]] @@ -1117,9 +1059,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.5" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom 0.3.4", ] @@ -1139,9 +1081,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -1151,9 +1093,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -1162,9 +1104,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "ring" @@ -1242,7 +1184,7 @@ dependencies = [ "openssl-probe 0.2.1", "rustls-pki-types", "schannel", - "security-framework 3.6.0", + "security-framework 3.5.1", ] [[package]] @@ -1279,7 +1221,7 @@ dependencies = [ "rustls-native-certs 0.8.3", "rustls-platform-verifier-android", "rustls-webpki", - "security-framework 3.6.0", + "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs", "windows-sys 0.61.2", @@ -1341,9 +1283,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.6.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ "bitflags", "core-foundation 0.10.1", @@ -1354,20 +1296,14 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.16.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", ] -[[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" @@ -1516,9 +1452,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.116" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -1604,11 +1540,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.17", ] [[package]] @@ -1624,9 +1560,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -1644,30 +1580,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde_core", + "serde", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -1773,7 +1709,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" dependencies = [ "crossbeam-channel", - "thiserror 2.0.18", + "thiserror 2.0.17", "time", "tracing-subscriber", ] @@ -1868,9 +1804,9 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-xid" @@ -1898,11 +1834,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.21.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.3.4", "js-sys", "wasm-bindgen", ] @@ -1937,18 +1873,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] @@ -1998,40 +1925,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-time" version = "1.1.0" @@ -2299,91 +2192,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.51.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "yasna" @@ -2396,18 +2207,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "1fabae64378cb18147bb18bca364e63bdbe72a0ffe4adf0addfec8aa166b2c56" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "c9c2d862265a8bb4471d87e033e730f536e2a285cc7cb05dbce09a2a97075f90" dependencies = [ "proc-macro2", "quote", @@ -2422,6 +2233,6 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" diff --git a/bins/bluestation-bs/src/main.rs b/bins/bluestation-bs/src/main.rs index 7bd7f0e..c901977 100644 --- a/bins/bluestation-bs/src/main.rs +++ b/bins/bluestation-bs/src/main.rs @@ -72,7 +72,6 @@ fn build_bs_stack(cfg: &mut SharedConfig) -> MessageRouter { username: brew_cfg.username, password: brew_cfg.password, issi: brew_cfg.issi, - groups: brew_cfg.groups, reconnect_delay: Duration::from_secs(brew_cfg.reconnect_delay_secs), jitter_initial_latency_frames: brew_cfg.jitter_initial_latency_frames, }; diff --git a/crates/tetra-config/src/stack_config_brew.rs b/crates/tetra-config/src/stack_config_brew.rs index d9977d1..5ffc0df 100644 --- a/crates/tetra-config/src/stack_config_brew.rs +++ b/crates/tetra-config/src/stack_config_brew.rs @@ -18,8 +18,6 @@ pub struct CfgBrew { pub password: Option, /// ISSI to register with the TetraPack server pub issi: u32, - /// GSSIs (group IDs) to affiliate to - pub groups: Vec, /// Reconnection delay in seconds pub reconnect_delay_secs: u64, /// Extra initial jitter playout delay in frames (added on top of adaptive baseline) @@ -41,8 +39,6 @@ pub struct CfgBrewDto { pub password: String, /// ISSI to register with the TetraPack server pub issi: u32, - /// GSSIs (group IDs) to affiliate to - pub groups: Vec, /// Reconnection delay in seconds #[serde(default = "default_brew_reconnect_delay")] pub reconnect_delay_secs: u64, @@ -71,7 +67,6 @@ pub fn apply_brew_patch(src: CfgBrewDto) -> CfgBrew { username: Some(src.username.to_string()), password: Some(src.password), issi: src.issi, - groups: src.groups, reconnect_delay_secs: src.reconnect_delay_secs, jitter_initial_latency_frames: src.jitter_initial_latency_frames, } diff --git a/crates/tetra-entities/src/brew/entity.rs b/crates/tetra-entities/src/brew/entity.rs index cf39658..f88893b 100644 --- a/crates/tetra-entities/src/brew/entity.rs +++ b/crates/tetra-entities/src/brew/entity.rs @@ -1,22 +1,22 @@ //! Brew protocol entity bridging TetraPack WebSocket to UMAC/MLE with hangtime-based circuit reuse -use std::collections::{HashMap, VecDeque}; +use std::collections::{HashMap, HashSet, VecDeque}; use std::thread; use std::time::{Duration, Instant}; use crossbeam_channel::{Receiver, Sender, unbounded}; use uuid::Uuid; +use crate::{MessageQueue, TetraEntityTrait}; use tetra_config::SharedConfig; use tetra_core::{Sap, TdmaTime, tetra_entities::TetraEntity}; +use tetra_saps::control::brew::{BrewSubscriberAction, BrewSubscriberUpdate}; use tetra_saps::{SapMsg, SapMsgInner, control::call_control::CallControl, tmd::TmdCircuitDataReq}; -use crate::{MessageQueue, TetraEntityTrait}; - use super::worker::{BrewCommand, BrewConfig, BrewEvent, BrewWorker}; /// Hangtime before releasing group call circuit to allow reuse without re-signaling. -const GROUP_CALL_HANGTIME: Duration = Duration::from_secs(5); +const GROUP_CALL_HANGTIME: Duration = Duration::from_secs(1); /// Minimum playout buffer depth in frames. const BREW_JITTER_MIN_FRAMES: usize = 2; /// Default playout buffer depth in frames. @@ -248,6 +248,9 @@ pub struct BrewEntity { /// UL calls being forwarded to TetraPack, keyed by timeslot ul_forwarded: HashMap, + /// Registered subscriber groups (ISSI -> set of GSSIs) + subscriber_groups: HashMap>, + /// Whether the worker is connected connected: bool, @@ -286,6 +289,9 @@ impl BrewEntity { dl_jitter: HashMap::new(), hanging_calls: HashMap::new(), ul_forwarded: HashMap::new(), + subscriber_groups: HashMap::new(), + // next_call_id: 100, // Start at 100 to avoid collision with CMCE + // next_usage: 10, // Start at 10 to avoid collision connected: false, worker_handle: Some(handle), } @@ -297,6 +303,8 @@ impl BrewEntity { match event { BrewEvent::Connected => { tracing::info!("BrewEntity: connected to TetraPack server"); + self.connected = true; + self.resync_subscribers(); self.set_network_connected(true); } BrewEvent::Disconnected(reason) => { @@ -331,6 +339,107 @@ impl BrewEntity { } } + fn handle_subscriber_update(&mut self, update: BrewSubscriberUpdate) { + let issi = update.issi; + let groups = update.groups; + + match update.action { + BrewSubscriberAction::Register => { + let known = self.subscriber_groups.contains_key(&issi); + self.subscriber_groups.entry(issi).or_insert_with(HashSet::new); + tracing::info!("BrewEntity: subscriber register issi={} known={}", issi, known); + let _ = self.command_sender.send(BrewCommand::RegisterSubscriber { issi }); + } + BrewSubscriberAction::Deregister => { + let mut removed_groups = Vec::new(); + if let Some(existing) = self.subscriber_groups.remove(&issi) { + for gssi in existing { + removed_groups.push(gssi); + } + } + if !removed_groups.is_empty() { + tracing::info!( + "BrewEntity: subscriber deaffiliate on deregister issi={} groups={:?}", + issi, + removed_groups + ); + let _ = self.command_sender.send(BrewCommand::DeaffiliateGroups { + issi, + groups: removed_groups, + }); + } + tracing::info!("BrewEntity: subscriber deregister issi={}", issi); + let _ = self.command_sender.send(BrewCommand::DeregisterSubscriber { issi }); + } + BrewSubscriberAction::Affiliate => { + let is_new = !self.subscriber_groups.contains_key(&issi); + let mut new_groups = Vec::new(); + { + let entry = self.subscriber_groups.entry(issi).or_insert_with(HashSet::new); + for gssi in groups { + if entry.insert(gssi) { + new_groups.push(gssi); + } + } + } + + if is_new { + tracing::info!("BrewEntity: affiliate from unknown issi={}, sending register", issi); + let _ = self.command_sender.send(BrewCommand::RegisterSubscriber { issi }); + } + + if new_groups.is_empty() { + tracing::debug!("BrewEntity: affiliate ignored (no new groups) issi={}", issi); + } else { + tracing::info!("BrewEntity: subscriber affiliate issi={} groups={:?}", issi, new_groups); + let _ = self.command_sender.send(BrewCommand::AffiliateGroups { issi, groups: new_groups }); + } + } + BrewSubscriberAction::Deaffiliate => { + let mut removed_groups = Vec::new(); + if let Some(entry) = self.subscriber_groups.get_mut(&issi) { + for gssi in groups { + if entry.remove(&gssi) { + removed_groups.push(gssi); + } + } + } else { + removed_groups = groups; + } + + if removed_groups.is_empty() { + tracing::debug!("BrewEntity: deaffiliate ignored (no matching groups) issi={}", issi); + } else { + tracing::info!("BrewEntity: subscriber deaffiliate issi={} groups={:?}", issi, removed_groups); + let _ = self.command_sender.send(BrewCommand::DeaffiliateGroups { + issi, + groups: removed_groups, + }); + } + } + } + } + + fn resync_subscribers(&self) { + if self.subscriber_groups.is_empty() { + tracing::debug!("BrewEntity: no subscribers to resync"); + return; + } + + tracing::info!("BrewEntity: resyncing {} subscribers to TetraPack", self.subscriber_groups.len()); + + for (issi, groups) in &self.subscriber_groups { + let _ = self.command_sender.send(BrewCommand::RegisterSubscriber { issi: *issi }); + if !groups.is_empty() { + let gssi_list: Vec = groups.iter().copied().collect(); + let _ = self.command_sender.send(BrewCommand::AffiliateGroups { + issi: *issi, + groups: gssi_list, + }); + } + } + } + fn set_network_connected(&mut self, connected: bool) { self.connected = connected; let mut state = self.config.state_write(); @@ -496,12 +605,10 @@ impl BrewEntity { /// Clean up expired hanging call tracking hints (CMCE already released circuits) fn expire_hanging_calls(&mut self, _queue: &mut MessageQueue) { - const HANGTIME_SECS: u64 = 5; - let expired: Vec = self .hanging_calls .iter() - .filter(|(_, h)| h.since.elapsed().as_secs() > HANGTIME_SECS) + .filter(|(_, h)| h.since.elapsed() >= GROUP_CALL_HANGTIME) .map(|(gssi, _)| *gssi) .collect(); @@ -644,6 +751,30 @@ impl BrewEntity { tracing::warn!("BrewEntity: NetworkCallReady for unknown uuid={}", brew_uuid); } } + + fn drop_network_call(&mut self, brew_uuid: Uuid) { + if let Some(call) = self.active_calls.remove(&brew_uuid) { + tracing::info!( + "BrewEntity: dropping network call uuid={} gssi={} (CMCE request)", + brew_uuid, + call.dest_gssi + ); + self.dl_jitter.remove(&brew_uuid); + self.hanging_calls.remove(&call.dest_gssi); + return; + } + + let hanging_gssi = self + .hanging_calls + .iter() + .find_map(|(gssi, hanging)| if hanging.uuid == brew_uuid { Some(*gssi) } else { None }); + if let Some(gssi) = hanging_gssi { + tracing::info!("BrewEntity: dropping hanging call uuid={} gssi={} (CMCE request)", brew_uuid, gssi); + self.hanging_calls.remove(&gssi); + } else { + tracing::debug!("BrewEntity: drop requested for unknown uuid={}", brew_uuid); + } + } } // ─── TetraEntityTrait implementation ────────────────────────────── @@ -688,6 +819,9 @@ impl TetraEntityTrait for BrewEntity { SapMsgInner::CmceCallControl(CallControl::CallEnded { call_id, ts }) => { self.handle_local_call_end(call_id, ts); } + SapMsgInner::CmceCallControl(CallControl::NetworkCallEnd { brew_uuid }) => { + self.drop_network_call(brew_uuid); + } SapMsgInner::CmceCallControl(CallControl::NetworkCallReady { brew_uuid, call_id, @@ -696,6 +830,9 @@ impl TetraEntityTrait for BrewEntity { }) => { self.rx_network_call_ready(brew_uuid, call_id, ts, usage); } + SapMsgInner::BrewSubscriberUpdate(update) => { + self.handle_subscriber_update(update); + } _ => { tracing::debug!("BrewEntity: unexpected rx_prim from {:?} on {:?}", message.src, message.sap); } @@ -713,16 +850,43 @@ impl BrewEntity { tracing::trace!("BrewEntity: not connected, ignoring local call start"); return; } + // TODO: Check if local + // if dest_gssi == 9 { + // tracing::debug!( + // "BrewEntity: suppressing local call forwarding for TG 9 (call_id={} src={} ts={})", + // call_id, + // source_issi, + // ts + // ); + // return; + // } - // Check if this group is subscribed in Brew config - // let groups = &self.config.config().brew.groups; - let groups = &self.brew_config.groups; - if !groups.contains(&dest_gssi) { - tracing::debug!( - "BrewEntity: local call on GSSI {} not subscribed (subscribed: {:?}), not forwarding", + // If we're already forwarding on this timeslot, treat as a talker change/update + if let Some(fwd) = self.ul_forwarded.get_mut(&ts) { + if fwd.call_id != call_id || fwd.dest_gssi != dest_gssi { + tracing::warn!( + "BrewEntity: updating forwarded call on ts={} (was call_id={} gssi={}) -> (call_id={} gssi={})", + ts, + fwd.call_id, + fwd.dest_gssi, + call_id, + dest_gssi + ); + } + + fwd.call_id = call_id; + fwd.source_issi = source_issi; + fwd.dest_gssi = dest_gssi; + fwd.frame_count = 0; + + // Send GROUP_TX update for the new talker + let _ = self.command_sender.send(BrewCommand::SendGroupTx { + uuid: fwd.uuid, + source_issi, dest_gssi, - groups - ); + priority: 0, + service: 0, // TETRA encoded speech + }); return; } diff --git a/crates/tetra-entities/src/brew/worker.rs b/crates/tetra-entities/src/brew/worker.rs index 46e1155..0c5e9f9 100644 --- a/crates/tetra-entities/src/brew/worker.rs +++ b/crates/tetra-entities/src/brew/worker.rs @@ -1,5 +1,6 @@ //! Brew WebSocket worker thread handling HTTP Digest Auth, TLS, and bidirectional Brew message exchange +use std::collections::{HashMap, HashSet}; use std::io::{Read, Write}; use std::net::{TcpStream, ToSocketAddrs}; use std::sync::Arc; @@ -53,9 +54,15 @@ pub enum BrewCommand { /// Register a subscriber (ISSI) RegisterSubscriber { issi: u32 }, + /// Deregister a subscriber (ISSI) + DeregisterSubscriber { issi: u32 }, + /// Affiliate subscriber to groups AffiliateGroups { issi: u32, groups: Vec }, + /// Deaffiliate subscriber from groups + DeaffiliateGroups { issi: u32, groups: Vec }, + /// Send GROUP_TX to TetraPack (local radio started transmitting on subscribed group) SendGroupTx { uuid: Uuid, @@ -91,8 +98,6 @@ pub struct BrewConfig { pub password: Option, /// ISSI to register with the server pub issi: u32, - /// GSSIs (group IDs) to affiliate to - pub groups: Vec, /// Reconnection delay pub reconnect_delay: Duration, /// Extra initial jitter playout delay in frames (added on top of adaptive baseline) @@ -250,6 +255,8 @@ pub struct BrewWorker { event_sender: Sender, /// Receive commands from the BrewEntity command_receiver: Receiver, + /// Registered subscribers and their affiliated groups (tracked from commands) + subscriber_groups: HashMap>, } impl BrewWorker { @@ -258,6 +265,7 @@ impl BrewWorker { config, event_sender, command_receiver, + subscriber_groups: HashMap::new(), } } @@ -463,7 +471,7 @@ impl BrewWorker { _ => {} } - // Step 3: Register subscriber and affiliate to groups + // Step 3: Register subscriber self.send_registration(&mut ws)?; // Step 4: Main message loop @@ -471,39 +479,37 @@ impl BrewWorker { } /// Send initial registration and group affiliation - fn send_registration(&self, ws: &mut WebSocket>) -> Result<(), String> { + fn send_registration(&mut self, ws: &mut WebSocket>) -> Result<(), String> { // Register ISSI - let reg_msg = build_subscriber_register(self.config.issi, &self.config.groups); + let reg_msg = build_subscriber_register(self.config.issi, &[]); ws.send(Message::Binary(reg_msg.into())) .map_err(|e| format!("failed to send registration: {}", e))?; tracing::info!("BrewWorker: registered ISSI {}", self.config.issi); - - // Affiliate to groups - if !self.config.groups.is_empty() { - let aff_msg = build_subscriber_affiliate(self.config.issi, &self.config.groups); - ws.send(Message::Binary(aff_msg.into())) - .map_err(|e| format!("failed to send affiliation: {}", e))?; - tracing::info!("BrewWorker: affiliated to groups {:?}", self.config.groups); - } + self.subscriber_groups.entry(self.config.issi).or_insert_with(HashSet::new); Ok(()) } /// Graceful teardown: DEAFFILIATE → DEREGISTER → WS close fn graceful_teardown(&self, ws: &mut WebSocket>) { - if !self.config.groups.is_empty() { - let deaff_msg = build_subscriber_deaffiliate(self.config.issi, &self.config.groups); - if let Err(e) = ws.send(Message::Binary(deaff_msg.into())) { - tracing::error!("BrewWorker: failed to send deaffiliation: {}", e); - } else { - tracing::info!("BrewWorker: deaffiliated from groups {:?}", self.config.groups); + for (issi, groups) in &self.subscriber_groups { + if !groups.is_empty() { + let mut group_list: Vec = groups.iter().copied().collect(); + group_list.sort_unstable(); + let deaff_msg = build_subscriber_deaffiliate(*issi, &group_list); + if let Err(e) = ws.send(Message::Binary(deaff_msg.into())) { + tracing::error!("BrewWorker: failed to send deaffiliation: {}", e); + } else { + tracing::info!("BrewWorker: deaffiliated issi={} groups={:?}", issi, group_list); + } + } + + let dereg_msg = build_subscriber_deregister(*issi); + if let Err(e) = ws.send(Message::Binary(dereg_msg.into())) { + tracing::error!("BrewWorker: failed to send deregistration: {}", e); + } else { + tracing::info!("BrewWorker: deregistered ISSI {}", issi); } - } - let dereg_msg = build_subscriber_deregister(self.config.issi); - if let Err(e) = ws.send(Message::Binary(dereg_msg.into())) { - tracing::error!("BrewWorker: failed to send deregistration: {}", e); - } else { - tracing::info!("BrewWorker: deregistered ISSI {}", self.config.issi); } let _ = ws.close(None); } @@ -595,15 +601,46 @@ impl BrewWorker { }; match cmd { BrewCommand::RegisterSubscriber { issi } => { + self.subscriber_groups.entry(issi).or_insert_with(HashSet::new); let msg = build_subscriber_register(issi, &[]); if let Err(e) = ws.send(Message::Binary(msg.into())) { tracing::error!("BrewWorker: failed to send registration: {}", e); + } else { + tracing::debug!("BrewWorker: sent REGISTER issi={}", issi); + } + } + BrewCommand::DeregisterSubscriber { issi } => { + self.subscriber_groups.remove(&issi); + let msg = build_subscriber_deregister(issi); + if let Err(e) = ws.send(Message::Binary(msg.into())) { + tracing::error!("BrewWorker: failed to send deregistration: {}", e); + } else { + tracing::debug!("BrewWorker: sent DEREGISTER issi={}", issi); } } BrewCommand::AffiliateGroups { issi, groups } => { + let entry = self.subscriber_groups.entry(issi).or_insert_with(HashSet::new); + for gssi in &groups { + entry.insert(*gssi); + } let msg = build_subscriber_affiliate(issi, &groups); if let Err(e) = ws.send(Message::Binary(msg.into())) { tracing::error!("BrewWorker: failed to send affiliation: {}", e); + } else { + tracing::debug!("BrewWorker: sent AFFILIATE issi={} groups={:?}", issi, groups); + } + } + BrewCommand::DeaffiliateGroups { issi, groups } => { + if let Some(entry) = self.subscriber_groups.get_mut(&issi) { + for gssi in &groups { + entry.remove(gssi); + } + } + let msg = build_subscriber_deaffiliate(issi, &groups); + if let Err(e) = ws.send(Message::Binary(msg.into())) { + tracing::error!("BrewWorker: failed to send deaffiliation: {}", e); + } else { + tracing::debug!("BrewWorker: sent DEAFFILIATE issi={} groups={:?}", issi, groups); } } BrewCommand::SendGroupTx { diff --git a/crates/tetra-entities/src/cmce/cmce_bs.rs b/crates/tetra-entities/src/cmce/cmce_bs.rs index 4fd3b0a..1aca80e 100644 --- a/crates/tetra-entities/src/cmce/cmce_bs.rs +++ b/crates/tetra-entities/src/cmce/cmce_bs.rs @@ -109,6 +109,9 @@ impl TetraEntityTrait for CmceBs { SapMsgInner::CmceCallControl(_) => { self.cc.rx_call_control(queue, message); } + SapMsgInner::BrewSubscriberUpdate(update) => { + self.cc.handle_subscriber_update(queue, update); + } _ => { panic!("Unexpected control message: {:?}", message.msg); } diff --git a/crates/tetra-entities/src/cmce/subentities/cc_bs.rs b/crates/tetra-entities/src/cmce/subentities/cc_bs.rs index 132bd8d..2836099 100644 --- a/crates/tetra-entities/src/cmce/subentities/cc_bs.rs +++ b/crates/tetra-entities/src/cmce/subentities/cc_bs.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use tetra_config::SharedConfig; use tetra_core::TimeslotOwner; @@ -18,6 +18,7 @@ use tetra_pdus::cmce::{ use tetra_saps::{ SapMsg, SapMsgInner, control::{ + brew::{BrewSubscriberAction, BrewSubscriberUpdate}, call_control::{CallControl, Circuit}, enums::{circuit_mode_type::CircuitModeType, communication_type::CommunicationType}, }, @@ -42,6 +43,10 @@ pub struct CcBsSubentity { circuits: CircuitMgr, /// Active group calls: call_id -> call info active_calls: HashMap, + /// Registered subscriber groups (ISSI -> set of GSSIs) + subscriber_groups: HashMap>, + /// Listener counts per GSSI + group_listeners: HashMap, } /// Origin of a group call @@ -82,6 +87,8 @@ impl CcBsSubentity { cached_setups: HashMap::new(), circuits: CircuitMgr::new(), active_calls: HashMap::new(), + subscriber_groups: HashMap::new(), + group_listeners: HashMap::new(), } } @@ -242,6 +249,124 @@ impl CcBsSubentity { sdu } + fn has_listener(&self, gssi: u32) -> bool { + self.group_listeners.get(&gssi).copied().unwrap_or(0) > 0 + } + + fn inc_group_listener(&mut self, gssi: u32) { + let entry = self.group_listeners.entry(gssi).or_insert(0); + *entry += 1; + } + + fn dec_group_listener(&mut self, gssi: u32) { + if let Some(entry) = self.group_listeners.get_mut(&gssi) { + if *entry <= 1 { + self.group_listeners.remove(&gssi); + } else { + *entry -= 1; + } + } + } + + fn drop_group_calls_if_unlistened(&mut self, queue: &mut MessageQueue, gssi: u32) { + if self.has_listener(gssi) { + return; + } + + let to_drop: Vec<(u16, CallOrigin)> = self + .active_calls + .iter() + .filter(|(_, call)| call.dest_gssi == gssi) + .map(|(call_id, call)| (*call_id, call.origin.clone())) + .collect(); + + for (call_id, origin) in to_drop { + tracing::info!("CMCE: dropping call_id={} gssi={} (no listeners)", call_id, gssi); + if let CallOrigin::Network { brew_uuid } = origin { + if self.config.config().brew.is_some() { + queue.push_back(SapMsg { + sap: Sap::Control, + src: TetraEntity::Cmce, + dest: TetraEntity::Brew, + dltime: self.dltime, + msg: SapMsgInner::CmceCallControl(CallControl::NetworkCallEnd { brew_uuid }), + }); + } + } + self.release_call(queue, call_id); + } + } + + pub fn handle_subscriber_update(&mut self, queue: &mut MessageQueue, update: BrewSubscriberUpdate) { + let issi = update.issi; + let groups = update.groups; + + match update.action { + BrewSubscriberAction::Register => { + let known = self.subscriber_groups.contains_key(&issi); + self.subscriber_groups.entry(issi).or_insert_with(HashSet::new); + tracing::info!("CMCE: subscriber register issi={} known={}", issi, known); + } + BrewSubscriberAction::Deregister => { + if let Some(existing) = self.subscriber_groups.remove(&issi) { + for gssi in existing { + self.dec_group_listener(gssi); + self.drop_group_calls_if_unlistened(queue, gssi); + } + } + tracing::info!("CMCE: subscriber deregister issi={}", issi); + } + BrewSubscriberAction::Affiliate => { + let mut new_groups = Vec::new(); + { + let entry = self.subscriber_groups.entry(issi).or_insert_with(HashSet::new); + for gssi in groups { + if entry.insert(gssi) { + new_groups.push(gssi); + } + } + } + for gssi in &new_groups { + self.inc_group_listener(*gssi); + } + + if new_groups.is_empty() { + tracing::debug!("CMCE: affiliate ignored (no new groups) issi={}", issi); + } else { + tracing::info!("CMCE: subscriber affiliate issi={} groups={:?}", issi, new_groups); + } + } + BrewSubscriberAction::Deaffiliate => { + let mut removed_groups = Vec::new(); + let mut known_issi = false; + if let Some(entry) = self.subscriber_groups.get_mut(&issi) { + known_issi = true; + for gssi in groups { + if entry.remove(&gssi) { + removed_groups.push(gssi); + } + } + } else { + removed_groups = groups; + } + if known_issi { + for gssi in &removed_groups { + self.dec_group_listener(*gssi); + } + } + + if removed_groups.is_empty() { + tracing::debug!("CMCE: deaffiliate ignored (no matching groups) issi={}", issi); + } else { + tracing::info!("CMCE: subscriber deaffiliate issi={} groups={:?}", issi, removed_groups); + for gssi in &removed_groups { + self.drop_group_calls_if_unlistened(queue, *gssi); + } + } + } + } + } + fn send_d_call_proceeding(&mut self, queue: &mut MessageQueue, message: &SapMsg, pdu_request: &USetup, call_id: u16) { tracing::trace!("send_d_call_proceeding"); @@ -469,6 +594,15 @@ impl CcBsSubentity { let dest_gssi = dest_gssi as u32; let dest_addr = TetraAddress::new(dest_gssi, SsiType::Gssi); + if !self.has_listener(dest_gssi) { + tracing::info!( + "CMCE: rejecting U-SETUP from issi={} to gssi={} (no listeners)", + calling_party.ssi, + dest_gssi + ); + return; + } + // Allocate circuit (DL+UL for group call) let circuit = match { let mut state = self.config.state_write(); @@ -577,7 +711,7 @@ impl CcBsSubentity { simplex_duplex_selection: pdu.simplex_duplex_selection, basic_service_information: pdu.basic_service_information.clone(), transmission_grant: TransmissionGrant::GrantedToOtherUser, - transmission_request_permission: false, + transmission_request_permission: true, call_priority: pdu.call_priority, notification_indicator: None, temporary_address: None, @@ -727,8 +861,8 @@ impl CcBsSubentity { /// Check if any active calls in hangtime have expired, and if so, release them fn check_hangtime_expiry(&mut self, queue: &mut MessageQueue) { - // Hangtime: ~5 seconds = 5 * 18 * 4 = 360 frames (approximately) - const HANGTIME_FRAMES: i32 = 5 * 18 * 4; + // Hangtime: ~1 second = 1 * 18 * 4 = 72 frames (approximately) + const HANGTIME_FRAMES: i32 = 1 * 18 * 4; let expired: Vec = self .active_calls @@ -1096,6 +1230,25 @@ impl CcBsSubentity { /// Handle network-initiated group call start fn rx_network_call_start(&mut self, queue: &mut MessageQueue, brew_uuid: uuid::Uuid, source_issi: u32, dest_gssi: u32, _priority: u8) { + if !self.has_listener(dest_gssi) { + tracing::info!( + "CMCE: ignoring network call start uuid={} gssi={} (no listeners)", + brew_uuid, + dest_gssi + ); + self.drop_group_calls_if_unlistened(queue, dest_gssi); + if self.config.config().brew.is_some() { + queue.push_back(SapMsg { + sap: Sap::Control, + src: TetraEntity::Cmce, + dest: TetraEntity::Brew, + dltime: self.dltime, + msg: SapMsgInner::CmceCallControl(CallControl::NetworkCallEnd { brew_uuid }), + }); + } + return; + } + // Check if there's an active call for this GSSI (speaker change scenario) if let Some((call_id, call)) = self.active_calls.iter_mut().find(|(_, c)| c.dest_gssi == dest_gssi) { // Speaker change during active or hangtime diff --git a/crates/tetra-entities/src/mm/components/client_state.rs b/crates/tetra-entities/src/mm/components/client_state.rs index ff04d73..e90deaf 100644 --- a/crates/tetra-entities/src/mm/components/client_state.rs +++ b/crates/tetra-entities/src/mm/components/client_state.rs @@ -67,6 +67,15 @@ impl MmClientMgr { self.clients.contains_key(&issi) } + pub fn set_client_state(&mut self, issi: u32, state: MmClientState) -> Result<(), ClientMgrErr> { + if let Some(client) = self.clients.get_mut(&issi) { + client.state = state; + Ok(()) + } else { + Err(ClientMgrErr::ClientNotFound { issi }) + } + } + /// Registers a fresh state for a client, based on ssi /// If client is already registered, previous state is discarded. pub fn try_register_client(&mut self, issi: u32, attached: bool) -> Result { diff --git a/crates/tetra-entities/src/mm/mm_bs.rs b/crates/tetra-entities/src/mm/mm_bs.rs index e0d27e2..5f59bb0 100644 --- a/crates/tetra-entities/src/mm/mm_bs.rs +++ b/crates/tetra-entities/src/mm/mm_bs.rs @@ -1,11 +1,12 @@ use crate::{MessageQueue, TetraEntityTrait}; use tetra_config::SharedConfig; use tetra_core::tetra_entities::TetraEntity; -use tetra_core::{BitBuffer, Sap, SsiType, TetraAddress, assert_warn, unimplemented_log}; +use tetra_core::{BitBuffer, Sap, SsiType, TdmaTime, TetraAddress, assert_warn, unimplemented_log}; +use tetra_saps::control::brew::{BrewSubscriberAction, BrewSubscriberUpdate}; use tetra_saps::lmm::LmmMleUnitdataReq; use tetra_saps::{SapMsg, SapMsgInner}; -use crate::mm::components::client_state::MmClientMgr; +use crate::mm::components::client_state::{MmClientMgr, MmClientState}; use crate::mm::components::not_supported::make_ul_mm_pdu_function_not_supported; use tetra_pdus::mm::enums::location_update_type::LocationUpdateType; use tetra_pdus::mm::enums::mm_pdu_type_ul::MmPduTypeUl; @@ -34,6 +35,27 @@ impl MmBs { } } + fn emit_brew_update(&self, queue: &mut MessageQueue, dltime: TdmaTime, issi: u32, groups: Vec, action: BrewSubscriberAction) { + let update = BrewSubscriberUpdate { issi, groups, action }; + let msg = SapMsg { + sap: Sap::Control, + src: TetraEntity::Mm, + dest: TetraEntity::Brew, + dltime, + msg: SapMsgInner::BrewSubscriberUpdate(update.clone()), + }; + queue.push_back(msg); + + let msg = SapMsg { + sap: Sap::Control, + src: TetraEntity::Mm, + dest: TetraEntity::Cmce, + dltime, + msg: SapMsgInner::BrewSubscriberUpdate(update), + }; + queue.push_back(msg); + } + fn rx_u_itsi_detach(&mut self, _queue: &mut MessageQueue, mut message: SapMsg) { tracing::trace!("rx_u_itsi_detach"); let SapMsgInner::LmmMleUnitdataInd(prim) = &mut message.msg else { @@ -59,7 +81,13 @@ impl MmBs { let ssi = prim.received_address.ssi; let detached_client = self.client_mgr.remove_client(ssi); - if detached_client.is_none() { + if let Some(client) = detached_client { + if !client.groups.is_empty() { + let groups: Vec = client.groups.iter().copied().collect(); + self.emit_brew_update(_queue, message.dltime, ssi, groups, BrewSubscriberAction::Deaffiliate); + } + self.emit_brew_update(_queue, message.dltime, ssi, Vec::new(), BrewSubscriberAction::Deregister); + } else { tracing::warn!("Received UItsiDetach for unknown client with SSI: {}", ssi); // return; }; @@ -107,20 +135,28 @@ impl MmBs { // Try to register the client let issi = prim.received_address.ssi; - match self.client_mgr.try_register_client(issi, true) { - Ok(_) => {} - Err(e) => { - tracing::warn!("Failed registering roaming MS {}: {:?}", issi, e); - // unimplemented_log!("Handle failed registration of roaming MS"); - return; + let is_new = !self.client_mgr.client_is_known(issi); + if is_new { + match self.client_mgr.try_register_client(issi, true) { + Ok(_) => { + self.emit_brew_update(queue, message.dltime, issi, Vec::new(), BrewSubscriberAction::Register); + } + Err(e) => { + tracing::warn!("Failed registering roaming MS {}: {:?}", issi, e); + // unimplemented_log!("Handle failed registration of roaming MS"); + return; + } } + } else if let Err(e) = self.client_mgr.set_client_state(issi, MmClientState::Attached) { + tracing::warn!("Failed updating roaming MS {}: {:?}", issi, e); + return; } // Process optional GroupIdentityLocationDemand field let gila = if let Some(gild) = pdu.group_identity_location_demand { // Try to attach to requested groups, then build GroupIdentityLocationAccept element let accepted_groups = if let Some(giu) = &gild.group_identity_uplink { - Some(self.try_attach_detach_groups(issi, &giu)) + Some(self.try_attach_detach_groups(queue, message.dltime, issi, &giu)) } else { None }; @@ -268,8 +304,17 @@ impl MmBs { // If group_identity_attach_detach_mode == 1, we first detach all groups if pdu.group_identity_attach_detach_mode == true { + let prior_groups: Vec = self + .client_mgr + .get_client_by_issi(issi) + .map(|client| client.groups.iter().copied().collect()) + .unwrap_or_default(); match self.client_mgr.client_detach_all_groups(issi) { - Ok(_) => {} + Ok(_) => { + if !prior_groups.is_empty() { + self.emit_brew_update(queue, message.dltime, issi, prior_groups, BrewSubscriberAction::Deaffiliate); + } + } Err(e) => { tracing::warn!("Failed detaching all groups for MS {}: {:?}", issi, e); return; @@ -279,7 +324,7 @@ impl MmBs { // Try to attach to requested groups, and retrieve list of accepted GroupIdentityDownlink elements // We can unwrap since we did compat check earlier - let accepted_gid = self.try_attach_detach_groups(issi, &pdu.group_identity_uplink.unwrap()); + let accepted_gid = self.try_attach_detach_groups(queue, message.dltime, issi, &pdu.group_identity_uplink.unwrap()); // Build reply PDU let pdu_response = DAttachDetachGroupIdentityAcknowledgement { @@ -352,8 +397,17 @@ impl MmBs { }; } - fn try_attach_detach_groups(&mut self, issi: u32, giu_vec: &Vec) -> Vec { + fn try_attach_detach_groups( + &mut self, + queue: &mut MessageQueue, + dltime: TdmaTime, + issi: u32, + giu_vec: &Vec, + ) -> Vec { let mut accepted_groups = Vec::new(); + let mut aff_groups = Vec::new(); + let mut deaff_groups = Vec::new(); + for giu in giu_vec.iter() { if giu.gssi.is_none() || giu.vgssi.is_some() || giu.address_extension.is_some() { unimplemented_log!("Only support GroupIdentityUplink with address_type 0"); @@ -361,26 +415,60 @@ impl MmBs { } let gssi = giu.gssi.unwrap(); // can't fail - match self.client_mgr.client_group_attach(issi, gssi, true) { - Ok(_) => { - // We have added the client to this group. Add an entry to the downlink response - let gid = GroupIdentityDownlink { - group_identity_attachment: Some(GroupIdentityAttachment { - group_identity_attachment_lifetime: 3, // re-attach after location update - class_of_usage: giu.class_of_usage.unwrap_or(0), - }), - group_identity_detachment_uplink: None, - gssi: Some(giu.gssi.unwrap()), - address_extension: None, - vgssi: None, - }; - accepted_groups.push(gid); + let is_detach = giu.group_identity_detachment_uplink.is_some(); + + if is_detach { + match self.client_mgr.client_group_attach(issi, gssi, false) { + Ok(changed) => { + if changed { + deaff_groups.push(gssi); + } + let gid = GroupIdentityDownlink { + group_identity_attachment: None, + group_identity_detachment_uplink: giu.group_identity_detachment_uplink, + gssi: Some(gssi), + address_extension: None, + vgssi: None, + }; + accepted_groups.push(gid); + } + Err(e) => { + tracing::warn!("Failed detaching MS {} from group {}: {:?}", issi, gssi, e); + } } - Err(e) => { - tracing::warn!("Failed attaching MS {} to group {}: {:?}", issi, gssi, e); + } else { + match self.client_mgr.client_group_attach(issi, gssi, true) { + Ok(changed) => { + if changed { + aff_groups.push(gssi); + } + // We have added the client to this group. Add an entry to the downlink response + let gid = GroupIdentityDownlink { + group_identity_attachment: Some(GroupIdentityAttachment { + group_identity_attachment_lifetime: 3, // re-attach after location update + class_of_usage: giu.class_of_usage.unwrap_or(0), + }), + group_identity_detachment_uplink: None, + gssi: Some(gssi), + address_extension: None, + vgssi: None, + }; + accepted_groups.push(gid); + } + Err(e) => { + tracing::warn!("Failed attaching MS {} to group {}: {:?}", issi, gssi, e); + } } } } + + if !aff_groups.is_empty() { + self.emit_brew_update(queue, dltime, issi, aff_groups, BrewSubscriberAction::Affiliate); + } + if !deaff_groups.is_empty() { + self.emit_brew_update(queue, dltime, issi, deaff_groups, BrewSubscriberAction::Deaffiliate); + } + accepted_groups } diff --git a/crates/tetra-saps/src/control/brew.rs b/crates/tetra-saps/src/control/brew.rs new file mode 100644 index 0000000..925b875 --- /dev/null +++ b/crates/tetra-saps/src/control/brew.rs @@ -0,0 +1,14 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BrewSubscriberAction { + Register, + Deregister, + Affiliate, + Deaffiliate, +} + +#[derive(Debug, Clone)] +pub struct BrewSubscriberUpdate { + pub issi: u32, + pub groups: Vec, + pub action: BrewSubscriberAction, +} diff --git a/crates/tetra-saps/src/control/call_control.rs b/crates/tetra-saps/src/control/call_control.rs index 0b8fb78..f00e73e 100644 --- a/crates/tetra-saps/src/control/call_control.rs +++ b/crates/tetra-saps/src/control/call_control.rs @@ -68,8 +68,8 @@ pub enum CallControl { ts: u8, // Allocated timeslot usage: u8, // Usage number }, - /// Request CMCE to end a network call - /// Sent by Brew when TetraPack sends GROUP_IDLE + /// Request ending a network call + /// Sent by Brew when TetraPack sends GROUP_IDLE, or by CMCE to make Brew drop a call NetworkCallEnd { brew_uuid: uuid::Uuid, // Identifies the call to end }, diff --git a/crates/tetra-saps/src/control/mod.rs b/crates/tetra-saps/src/control/mod.rs index 5152f83..76a9f18 100644 --- a/crates/tetra-saps/src/control/mod.rs +++ b/crates/tetra-saps/src/control/mod.rs @@ -1,2 +1,3 @@ +pub mod brew; pub mod call_control; pub mod enums; diff --git a/crates/tetra-saps/src/sapmsg.rs b/crates/tetra-saps/src/sapmsg.rs index fabefde..51275af 100644 --- a/crates/tetra-saps/src/sapmsg.rs +++ b/crates/tetra-saps/src/sapmsg.rs @@ -4,6 +4,7 @@ use tetra_core::Sap; use tetra_core::TdmaTime; use tetra_core::tetra_entities::TetraEntity; +use crate::control::brew::BrewSubscriberUpdate; use crate::control::call_control::CallControl; use crate::tmd::TmdCircuitDataInd; use crate::tmd::TmdCircuitDataReq; @@ -76,6 +77,9 @@ pub enum SapMsgInner { // CMCE -> UMAC control CmceCallControl(CallControl), + // MM -> Brew/CMCE subscriber update + BrewSubscriberUpdate(BrewSubscriberUpdate), + // LTPD-SAP (MLE-LTPD) LtpdMleUnitdataInd(LtpdMleUnitdataInd), @@ -104,6 +108,9 @@ impl Display for SapMsgInner { SapMsgInner::TlmbSyncInd(_) => write!(f, "TmbSyncInd"), SapMsgInner::TlmbSysinfoInd(_) => write!(f, "TmbSysinfoInd"), + // Control/Brew + SapMsgInner::BrewSubscriberUpdate(_) => write!(f, "BrewSubscriberUpdate"), + // TLB-SAP // SapMsgInner::TlbTlSyncInd(_) => write!(f, "TlbTlSyncInd"), // SapMsgInner::TlbTlSysinfoInd(_) => write!(f, "TlbTlSysinfoInd"), diff --git a/example_config/config.toml b/example_config/config.toml index 00f1db3..287c0f9 100644 --- a/example_config/config.toml +++ b/example_config/config.toml @@ -147,6 +147,7 @@ voice_service = true ############################################################################### # Brew protocol: Connect to TetraPack/BrandMeister server via TETRA Homebrew Protocol. +# All groups that radios attach to are forwarded to Brew as affiliations. # Uncomment this section to automatically load and use Brew entity # See: https://wiki.tetrapack.online/books/tetra/page/brew @@ -167,10 +168,6 @@ voice_service = true # ISSI to register with server. Usually, your DMR ID. # issi = 012345 -# Comma separated list of Group IDs (GSSIs) to affiliate with -# BlueStation will receive those groups from TetraPack/Brandmeister -# groups = [91] - # Reconnection delay (seconds) # reconnect_delay_secs = 15