diff --git a/pixi.toml b/pixi.toml index 24faec9bf..cb41ba1d2 100644 --- a/pixi.toml +++ b/pixi.toml @@ -14,6 +14,7 @@ version = "0.1.0" zenoh = "ros2 run rmw_zenoh_cpp rmw_zenohd" deploy = {cmd = "scripts/deploy_robots.py", description = "Deploys the current environment."} format = {cmd = "pre-commit run --all-files", description = "Runs code formatting and linting."} +teamplayer = { cmd = "cargo run --release --manifest-path src/bitbots_misc/bitbots_bringup/tui/Cargo.toml --", description = "Launch the main software components" } [tasks.build] # Skipping ZED-camera related packages as they require a local installation of the ZED SDK. diff --git a/src/bitbots_misc/bitbots_bringup/launch/visualization.launch b/src/bitbots_misc/bitbots_bringup/launch/visualization.launch deleted file mode 100644 index 8b4bf73d5..000000000 --- a/src/bitbots_misc/bitbots_bringup/launch/visualization.launch +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/bitbots_misc/bitbots_bringup/tui/Cargo.lock b/src/bitbots_misc/bitbots_bringup/tui/Cargo.lock new file mode 100644 index 000000000..f76f29d3f --- /dev/null +++ b/src/bitbots_misc/bitbots_bringup/tui/Cargo.lock @@ -0,0 +1,869 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ansi-to-tui" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b0e348dcd256ba06d44d5deabc88a7c0e80ee7303158253ca069bcd9e9b7f57" +dependencies = [ + "nom", + "ratatui", + "thiserror", +] + +[[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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[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 = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags", + "crossterm_winapi", + "futures-core", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[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.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[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 = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ratatui" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "itertools 0.12.1", + "lru", + "paste", + "stability", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio 0.8.11", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stability" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "teamplayer" +version = "0.1.0" +dependencies = [ + "ansi-to-tui", + "anyhow", + "clap", + "crossterm", + "futures", + "nix", + "ratatui", + "tokio", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio 1.2.0", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[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.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/src/bitbots_misc/bitbots_bringup/tui/Cargo.toml b/src/bitbots_misc/bitbots_bringup/tui/Cargo.toml new file mode 100644 index 000000000..d9754baf5 --- /dev/null +++ b/src/bitbots_misc/bitbots_bringup/tui/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "teamplayer" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "teamplayer" +path = "src/main.rs" + +[dependencies] +ratatui = "0.26" +crossterm = { version = "0.27", features = ["event-stream"] } +tokio = { version = "1", features = ["full"] } +anyhow = "1" +futures = "0.3" +nix = { version = "0.27", features = ["signal", "process"] } +ansi-to-tui = "3" +clap = { version = "~4.4", features = ["derive"] } diff --git a/src/bitbots_misc/bitbots_bringup/tui/src/app.rs b/src/bitbots_misc/bitbots_bringup/tui/src/app.rs new file mode 100644 index 000000000..675e959d9 --- /dev/null +++ b/src/bitbots_misc/bitbots_bringup/tui/src/app.rs @@ -0,0 +1,427 @@ +use std::{ + collections::VecDeque, + sync::{Arc, Mutex}, +}; + +use tokio::{ + io::{AsyncBufReadExt, BufReader}, + process::Command, + sync::mpsc, +}; + +pub use crate::components::GlobalParams; +use crate::components::{COMPONENT_DEFS, FIELDNAME_DEFAULT_HW, FIELDNAME_DEFAULT_SIM}; + +// ─── State types ────────────────────────────────────────────────────────────── + +#[derive(Clone, Debug, PartialEq)] +pub enum ProcState { + Idle, + Running, + /// SIGTERM sent, waiting for exit + Stopping, + /// SIGTERM sent, will restart automatically when the process exits + Restarting, + Stopped, + Crashed, +} + +impl ProcState { + pub fn label(&self) -> &'static str { + match self { + ProcState::Idle => "IDLE", + ProcState::Running => "RUN ", + ProcState::Stopping => "STOP", + ProcState::Restarting => "↺ ", + ProcState::Stopped => "DONE", + ProcState::Crashed => "FAIL", + } + } +} + +pub struct ComponentState { + pub comp_idx: usize, + pub enabled: bool, + pub state: ProcState, + pub logs: Arc>>, + pub pids: Vec, + pub task_handles: Vec>, +} + +impl ComponentState { + pub fn new(comp_idx: usize, enabled: bool) -> Self { + Self { + comp_idx, + enabled, + state: ProcState::Idle, + logs: Arc::new(Mutex::new(VecDeque::with_capacity(2000))), + pids: Vec::new(), + task_handles: Vec::new(), + } + } +} + +pub enum AppMsg { + ProcessExited { key: String, code: i32 }, +} + +// ─── Screen / focus ─────────────────────────────────────────────────────────── + +#[derive(PartialEq, Clone)] +pub enum Screen { + Config, + Runtime, + Logs(usize), +} + +#[derive(Clone, PartialEq, Debug)] +pub enum ConfigFocus { + Zenoh, + Sim, + Fieldname, + DsdFile, + Component(usize), + Start, +} + +// ─── App ────────────────────────────────────────────────────────────────────── + +pub struct App { + pub screen: Screen, + pub params: GlobalParams, + pub components: Vec, + pub config_focus: ConfigFocus, + pub config_fieldname_input: String, + pub config_dsd_input: String, + pub runtime_selected: usize, + pub tick_count: u64, + pub msg_tx: mpsc::UnboundedSender, + pub msg_rx: mpsc::UnboundedReceiver, + pub log_scroll: usize, + pub all_logs: Arc>>, + pub show_log_panel: bool, +} + +impl App { + pub fn new() -> Self { + let (msg_tx, msg_rx) = mpsc::unbounded_channel(); + let components: Vec = COMPONENT_DEFS + .iter() + .enumerate() + .map(|(i, def)| { + ComponentState::new(i, if def.sim_component { false } else { def.default_enabled }) + }) + .collect(); + + App { + screen: Screen::Config, + params: GlobalParams::default(), + components, + config_focus: ConfigFocus::Zenoh, + config_fieldname_input: FIELDNAME_DEFAULT_HW.to_string(), + config_dsd_input: "main.dsd".to_string(), + runtime_selected: 0, + tick_count: 0, + msg_tx, + msg_rx, + log_scroll: 0, + all_logs: Arc::new(Mutex::new(VecDeque::with_capacity(5000))), + show_log_panel: true, + } + } + + pub fn apply_cli_presets( + &mut self, + sim: bool, + no_zenoh: bool, + fieldname: Option<&str>, + dsd_file: &str, + enable: &[String], + disable: &[String], + ) { + if sim { + self.params.sim = true; + self.apply_sim_toggle(); + } + if let Some(f) = fieldname { + self.params.fieldname = f.to_string(); + self.config_fieldname_input = f.to_string(); + } + self.params.dsd_file = dsd_file.to_string(); + self.config_dsd_input = dsd_file.to_string(); + + if no_zenoh { + if let Some(idx) = COMPONENT_DEFS.iter().position(|d| d.key == "zenoh") { + self.components[idx].enabled = false; + self.params.zenoh = false; + } + } + for key in enable { + if let Some(idx) = COMPONENT_DEFS.iter().position(|d| d.key == key.as_str()) { + self.components[idx].enabled = true; + } + } + for key in disable { + if let Some(idx) = COMPONENT_DEFS.iter().position(|d| d.key == key.as_str()) { + self.components[idx].enabled = false; + } + } + } + + pub fn toggleable_indices(&self) -> Vec { + COMPONENT_DEFS + .iter() + .enumerate() + .filter(|(_, d)| { + !d.infrastructure && !d.sim_component && !(d.hardware_only && self.params.sim) + }) + .map(|(i, _)| i) + .collect() + } + + pub fn config_focus_items(&self) -> Vec { + let mut items = vec![ + ConfigFocus::Zenoh, + ConfigFocus::Sim, + ConfigFocus::Fieldname, + ConfigFocus::DsdFile, + ]; + for i in 0..self.toggleable_indices().len() { + items.push(ConfigFocus::Component(i)); + } + items.push(ConfigFocus::Start); + items + } + + pub fn config_focus_next(&mut self) { + let items = self.config_focus_items(); + let pos = items.iter().position(|f| f == &self.config_focus).unwrap_or(0); + self.config_focus = items[(pos + 1) % items.len()].clone(); + } + + pub fn config_focus_prev(&mut self) { + let items = self.config_focus_items(); + let pos = items.iter().position(|f| f == &self.config_focus).unwrap_or(0); + self.config_focus = items[(pos + items.len() - 1) % items.len()].clone(); + } + + pub fn runtime_visible_indices(&self) -> Vec { + COMPONENT_DEFS + .iter() + .enumerate() + .filter(|(i, d)| { + if d.sim_component { + return self.params.sim; + } + if d.hardware_only && self.params.sim { + return false; + } + self.components[*i].enabled + }) + .map(|(i, _)| i) + .collect() + } + + pub fn apply_sim_toggle(&mut self) { + let sim = self.params.sim; + for (i, def) in COMPONENT_DEFS.iter().enumerate() { + if def.sim_component { + self.components[i].enabled = sim; + } else if def.hardware_only { + self.components[i].enabled = !sim && def.default_enabled; + } + } + self.config_fieldname_input = + if sim { FIELDNAME_DEFAULT_SIM } else { FIELDNAME_DEFAULT_HW }.to_string(); + self.params.fieldname = self.config_fieldname_input.clone(); + self.config_focus = ConfigFocus::Zenoh; + } + + pub async fn start_component(&mut self, comp_idx: usize) { + let def = &COMPONENT_DEFS[comp_idx]; + let cmds = (def.cmds)(&self.params); + let key = def.key.to_string(); + let comp_name = def.name; // &'static str + + let logs = self.components[comp_idx].logs.clone(); + let all_logs = self.all_logs.clone(); + self.components[comp_idx].state = ProcState::Running; + self.components[comp_idx].pids.clear(); + self.components[comp_idx].task_handles.clear(); + + for cmd_args in cmds { + if cmd_args.is_empty() { + continue; + } + let tx = self.msg_tx.clone(); + let logs = logs.clone(); + let all_logs = all_logs.clone(); + let key = key.clone(); + + let mut child = match Command::new(&cmd_args[0]) + .args(&cmd_args[1..]) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .process_group(0) + .kill_on_drop(true) + .spawn() + { + Ok(c) => c, + Err(e) => { + let entry = format!("[ERROR] spawn failed: {e}"); + logs.lock().unwrap().push_back(entry.clone()); + all_logs.lock().unwrap().push_back(format!("[{comp_name}] {entry}")); + let _ = tx.send(AppMsg::ProcessExited { key, code: -1 }); + return; + } + }; + + if let Some(pid) = child.id() { + self.components[comp_idx].pids.push(pid); + } + + let stdout = child.stdout.take().map(BufReader::new); + let stderr = child.stderr.take().map(BufReader::new); + + let handle = tokio::spawn(async move { + async fn pump( + mut r: BufReader, + logs: Arc>>, + all_logs: Arc>>, + prefix: &'static str, + comp_name: &'static str, + ) { + let mut line = String::new(); + while let Ok(n) = r.read_line(&mut line).await { + if n == 0 { + break; + } + let entry = format!("{}{}", prefix, line.trim_end_matches('\n')); + { + let mut l = logs.lock().unwrap(); + if l.len() >= 2000 { + l.pop_front(); + } + l.push_back(entry.clone()); + } + { + let mut l = all_logs.lock().unwrap(); + if l.len() >= 5000 { + l.pop_front(); + } + l.push_back(format!("[{comp_name}] {entry}")); + } + line.clear(); + } + } + // Read stdout and stderr concurrently so neither blocks the other. + let (logs2, all2) = (logs.clone(), all_logs.clone()); + tokio::join!( + async move { + if let Some(r) = stdout { + pump(r, logs2, all2, "", comp_name).await; + } + }, + async move { + if let Some(r) = stderr { + pump(r, logs, all_logs, "[ERR] ", comp_name).await; + } + } + ); + let code = child + .wait() + .await + .map(|s| s.code().unwrap_or(-1)) + .unwrap_or(-1); + let _ = tx.send(AppMsg::ProcessExited { key, code }); + }); + self.components[comp_idx].task_handles.push(handle); + } + } + + /// Send SIGTERM to the component's direct child processes and mark as Stopping. + pub async fn stop_component(&mut self, comp_idx: usize) { + match self.components[comp_idx].state { + ProcState::Running => { + sigterm_pids(&self.components[comp_idx].pids); + self.components[comp_idx].state = ProcState::Stopping; + } + ProcState::Restarting => { + // Already sent SIGTERM — cancel the pending restart + self.components[comp_idx].state = ProcState::Stopping; + } + _ => {} + } + } + + /// Send SIGTERM and mark as Restarting — will auto-restart when the process exits. + pub fn restart_component(&mut self, comp_idx: usize) { + if self.components[comp_idx].state != ProcState::Running { + return; + } + sigterm_pids(&self.components[comp_idx].pids); + self.components[comp_idx].state = ProcState::Restarting; + } + + pub async fn stop_all(&mut self) { + for i in 0..self.components.len() { + self.stop_component(i).await; + } + } + + pub async fn start_all_enabled(&mut self) { + let enabled: Vec = self + .components + .iter() + .enumerate() + .filter(|(_, c)| c.enabled && c.state == ProcState::Idle) + .map(|(i, _)| i) + .collect(); + for i in enabled { + self.start_component(i).await; + } + } + + /// Drain the message queue and return the indices of components that need restarting. + pub fn drain_messages(&mut self) -> Vec { + let mut to_restart = Vec::new(); + while let Ok(msg) = self.msg_rx.try_recv() { + match msg { + AppMsg::ProcessExited { key, code } => { + if let Some((idx, comp)) = self + .components + .iter_mut() + .enumerate() + .find(|(_, c)| COMPONENT_DEFS[c.comp_idx].key == key) + { + match comp.state { + ProcState::Restarting => { + comp.pids.clear(); + comp.state = ProcState::Idle; + to_restart.push(idx); + } + ProcState::Stopping | ProcState::Running => { + comp.state = if code == 0 { + ProcState::Stopped + } else { + ProcState::Crashed + }; + comp.pids.clear(); + } + _ => {} + } + } + } + } + } + to_restart + } +} + +fn sigterm_pids(pids: &[u32]) { + use nix::sys::signal::{killpg, Signal}; + use nix::unistd::Pid; + for &pid in pids { + let _ = killpg(Pid::from_raw(pid as i32), Signal::SIGTERM); + } +} diff --git a/src/bitbots_misc/bitbots_bringup/tui/src/components.rs b/src/bitbots_misc/bitbots_bringup/tui/src/components.rs new file mode 100644 index 000000000..86221896e --- /dev/null +++ b/src/bitbots_misc/bitbots_bringup/tui/src/components.rs @@ -0,0 +1,324 @@ +// ─── Global params ──────────────────────────────────────────────────────────── + +pub const FIELDNAME_DEFAULT_HW: &str = "labor"; +pub const FIELDNAME_DEFAULT_SIM: &str = "hsl_kid"; + +#[derive(Clone, Debug)] +pub struct GlobalParams { + pub sim: bool, + pub zenoh: bool, + pub fieldname: String, + pub dsd_file: String, +} + +impl Default for GlobalParams { + fn default() -> Self { + Self { + sim: false, + zenoh: true, + fieldname: FIELDNAME_DEFAULT_HW.to_string(), + dsd_file: "main.dsd".to_string(), + } + } +} + +// ─── Component definitions ──────────────────────────────────────────────────── + +pub struct ComponentDef { + pub name: &'static str, + pub key: &'static str, + pub default_enabled: bool, + pub infrastructure: bool, + /// Only valid on real hardware — hidden/disabled in sim + pub hardware_only: bool, + /// Only shown/started when sim mode is active (e.g. the simulator itself) + pub sim_component: bool, + /// Returns the list of commands to spawn for this component + pub cmds: fn(&GlobalParams) -> Vec>, +} + +// ─── Command helpers ────────────────────────────────────────────────────────── + +pub fn ros2_launch(pkg: &str, file: &str, extra: &[&str]) -> Vec { + let mut v = vec![ + "ros2".into(), + "launch".into(), + pkg.into(), + file.into(), + ]; + v.extend(extra.iter().map(|s| s.to_string())); + v +} + +pub fn ros2_run(pkg: &str, exec: &str, extra: &[&str]) -> Vec { + let mut v = vec![ + "ros2".into(), + "run".into(), + pkg.into(), + exec.into(), + ]; + v.extend(extra.iter().map(|s| s.to_string())); + v +} + +fn sim(p: &GlobalParams) -> String { + format!("sim:={}", p.sim) +} + +// ─── Static component registry ──────────────────────────────────────────────── + +pub static COMPONENT_DEFS: &[ComponentDef] = &[ + // ── Infrastructure (always started) ────────────────────────────────────── + ComponentDef { + name: "Zenoh", + key: "zenoh", + default_enabled: true, + infrastructure: true, + hardware_only: false, + sim_component: false, + cmds: |_| vec![ros2_run("rmw_zenoh_cpp", "rmw_zenohd", &[])], + }, + ComponentDef { + name: "Param Blackboard", + key: "blackboard", + default_enabled: true, + infrastructure: true, + hardware_only: false, + sim_component: false, + cmds: |p| { + vec![ros2_launch( + "bitbots_parameter_blackboard", + "parameter_blackboard.launch", + &[&sim(p), &format!("fieldname:={}", p.fieldname)], + )] + }, + }, + ComponentDef { + name: "Robot Description", + key: "robot_description", + default_enabled: true, + infrastructure: true, + hardware_only: false, + sim_component: false, + cmds: |p| { + vec![ros2_launch( + "bitbots_robot_description", + "load_robot_description.launch", + &[&sim(p)], + )] + }, + }, + ComponentDef { + name: "Diagnostics", + key: "diagnostics", + default_enabled: true, + infrastructure: true, + hardware_only: false, + sim_component: false, + cmds: |_| vec![ros2_launch("bitbots_diagnostic", "aggregator.launch", &[])], + }, + ComponentDef { + name: "Simulator", + key: "simulator", + default_enabled: false, + infrastructure: true, + hardware_only: false, + sim_component: true, + cmds: |p| { + vec![ros2_launch( + "bitbots_mujoco_sim", + "simulator.launch", + &[&sim(p)], + )] + }, + }, + // ── User-toggleable ─────────────────────────────────────────────────────── + ComponentDef { + name: "Lowlevel", + key: "lowlevel", + default_enabled: true, + infrastructure: false, + hardware_only: true, + sim_component: false, + cmds: |_| vec![ros2_launch("livelybot_bringup", "lowlevel.launch", &[])], + }, + ComponentDef { + name: "Motion", + key: "motion", + default_enabled: true, + infrastructure: false, + hardware_only: false, + sim_component: false, + cmds: |p| vec![ros2_launch("bitbots_bringup", "motion.launch", &[&sim(p)])], + }, + ComponentDef { + name: "Game Controller", + key: "game_controller", + default_enabled: true, + infrastructure: false, + hardware_only: false, + sim_component: false, + cmds: |p| { + vec![ros2_launch( + "game_controller_hsl", + "game_controller.launch", + &[ + &sim(p), + "use_parameter_blackboard:=true", + "parameter_blackboard_name:=parameter_blackboard", + "team_id_param_name:=team_id", + "bot_id_param_name:=bot_id", + ], + )] + }, + }, + ComponentDef { + name: "Vision", + key: "vision", + default_enabled: true, + infrastructure: false, + hardware_only: false, + sim_component: false, + cmds: |p| vec![ros2_launch("bitbots_bringup", "vision.launch", &[&sim(p)])], + }, + ComponentDef { + name: "IPM", + key: "ipm", + default_enabled: true, + infrastructure: false, + hardware_only: false, + sim_component: false, + cmds: |p| vec![ros2_launch("bitbots_ipm", "ipm.launch", &[&sim(p)])], + }, + ComponentDef { + name: "Localization", + key: "localization", + default_enabled: true, + infrastructure: false, + hardware_only: false, + sim_component: false, + cmds: |p| { + vec![ros2_launch( + "bitbots_localization", + "localization.launch", + &[&sim(p)], + )] + }, + }, + ComponentDef { + name: "Path Planning", + key: "path_planning", + default_enabled: true, + infrastructure: false, + hardware_only: false, + sim_component: false, + cmds: |p| { + vec![ros2_launch( + "bitbots_path_planning", + "path_planning.launch", + &[&sim(p)], + )] + }, + }, + ComponentDef { + name: "Behavior", + key: "behavior", + default_enabled: true, + infrastructure: false, + hardware_only: false, + sim_component: false, + cmds: |p| { + vec![ros2_launch( + "bitbots_body_behavior", + "behavior.launch", + &[&sim(p), &format!("dsd_file:={}", p.dsd_file)], + )] + }, + }, + ComponentDef { + name: "Team Comm", + key: "teamcom", + default_enabled: true, + infrastructure: false, + hardware_only: false, + sim_component: false, + cmds: |p| { + vec![ros2_launch( + "bitbots_team_communication", + "team_comm.launch", + &[&sim(p)], + )] + }, + }, + ComponentDef { + name: "World Model", + key: "world_model", + default_enabled: true, + infrastructure: false, + hardware_only: false, + sim_component: false, + cmds: |p| { + vec![ + ros2_launch("bitbots_ball_filter", "ball_filter.launch", &[&sim(p)]), + ros2_launch("bitbots_robot_filter", "robot_filter.launch", &[&sim(p)]), + ] + }, + }, + ComponentDef { + name: "Whistle Det.", + key: "whistle_detector", + default_enabled: true, + infrastructure: false, + hardware_only: false, + sim_component: false, + cmds: |_| { + vec![ros2_launch( + "bitbots_whistle_detector", + "whistle_detector.launch", + &[], + )] + }, + }, + ComponentDef { + name: "Audio", + key: "audio", + default_enabled: true, + infrastructure: false, + hardware_only: false, + sim_component: false, + cmds: |_| vec![ros2_launch("bitbots_bringup", "audio.launch", &[])], + }, + ComponentDef { + name: "TTS", + key: "tts", + default_enabled: false, + infrastructure: false, + hardware_only: false, + sim_component: false, + cmds: |_| vec![ros2_launch("bitbots_tts", "tts.launch", &[])], + }, + ComponentDef { + name: "Monitoring", + key: "monitoring", + default_enabled: false, + infrastructure: false, + hardware_only: false, + sim_component: false, + cmds: |_| vec![ros2_launch("bitbots_bringup", "monitoring.launch", &[])], + }, + ComponentDef { + name: "Recording", + key: "record", + default_enabled: false, + infrastructure: false, + hardware_only: false, + sim_component: false, + cmds: |p| { + vec![ros2_launch( + "bitbots_bringup", + "rosbag_record.launch.py", + &[&sim(p)], + )] + }, + }, +]; diff --git a/src/bitbots_misc/bitbots_bringup/tui/src/input.rs b/src/bitbots_misc/bitbots_bringup/tui/src/input.rs new file mode 100644 index 000000000..2ee9e8ab9 --- /dev/null +++ b/src/bitbots_misc/bitbots_bringup/tui/src/input.rs @@ -0,0 +1,165 @@ +use crossterm::event::{KeyCode, KeyModifiers}; + +use crate::app::{App, ConfigFocus, ProcState, Screen}; +use crate::components::COMPONENT_DEFS; + +pub async fn handle_config_key(app: &mut App, key: KeyCode, modifiers: KeyModifiers) -> bool { + match (&app.config_focus.clone(), key) { + // Navigation + (_, KeyCode::Tab) => { + if modifiers.contains(KeyModifiers::SHIFT) { + app.config_focus_prev(); + } else { + app.config_focus_next(); + } + } + (_, KeyCode::Down) => app.config_focus_next(), + (_, KeyCode::Up) => app.config_focus_prev(), + + // Zenoh toggle + (ConfigFocus::Zenoh, KeyCode::Char(' ') | KeyCode::Enter) => { + app.params.zenoh = !app.params.zenoh; + if let Some(idx) = COMPONENT_DEFS.iter().position(|d| d.key == "zenoh") { + app.components[idx].enabled = app.params.zenoh; + } + } + + // Sim toggle + (ConfigFocus::Sim, KeyCode::Char(' ') | KeyCode::Enter) => { + app.params.sim = !app.params.sim; + app.apply_sim_toggle(); + } + + // Fieldname text input + (ConfigFocus::Fieldname, KeyCode::Char(c)) => { + if !modifiers.contains(KeyModifiers::CONTROL) { + app.config_fieldname_input.push(c); + app.params.fieldname = app.config_fieldname_input.clone(); + } + } + (ConfigFocus::Fieldname, KeyCode::Backspace) => { + app.config_fieldname_input.pop(); + app.params.fieldname = app.config_fieldname_input.clone(); + } + + // DSD file text input + (ConfigFocus::DsdFile, KeyCode::Char(c)) => { + if !modifiers.contains(KeyModifiers::CONTROL) { + app.config_dsd_input.push(c); + app.params.dsd_file = app.config_dsd_input.clone(); + } + } + (ConfigFocus::DsdFile, KeyCode::Backspace) => { + app.config_dsd_input.pop(); + app.params.dsd_file = app.config_dsd_input.clone(); + } + + // Component checkbox toggle + (ConfigFocus::Component(fi), KeyCode::Char(' ') | KeyCode::Enter) => { + let fi = *fi; + let toggleables = app.toggleable_indices(); + if fi < toggleables.len() { + let comp_idx = toggleables[fi]; + app.components[comp_idx].enabled = !app.components[comp_idx].enabled; + } + } + + // Start / Continue button + (ConfigFocus::Start, KeyCode::Enter | KeyCode::Char(' ')) => { + app.params.fieldname = app.config_fieldname_input.clone(); + app.params.dsd_file = app.config_dsd_input.clone(); + app.screen = Screen::Runtime; + app.start_all_enabled().await; + } + + // Quit + (_, KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc) => return true, + + _ => {} + } + false +} + +pub async fn handle_runtime_key(app: &mut App, key: KeyCode, _modifiers: KeyModifiers) -> bool { + let visible = app.runtime_visible_indices(); + let sel = app.runtime_selected.min(visible.len().saturating_sub(1)); + let comp_idx = visible.get(sel).copied(); + + match key { + KeyCode::Down | KeyCode::Char('j') => { + app.runtime_selected = (sel + 1).min(visible.len().saturating_sub(1)); + } + KeyCode::Up | KeyCode::Char('k') => { + app.runtime_selected = sel.saturating_sub(1); + } + + KeyCode::Char('s') => { + if let Some(idx) = comp_idx { + match app.components[idx].state { + ProcState::Running | ProcState::Stopping | ProcState::Restarting => { + app.stop_component(idx).await; + } + _ => { + app.components[idx].state = ProcState::Idle; + app.start_component(idx).await; + } + } + } + } + + KeyCode::Char('r') => { + if let Some(idx) = comp_idx { + match app.components[idx].state { + ProcState::Running => app.restart_component(idx), + ProcState::Stopping | ProcState::Restarting => {} // already terminating + _ => app.start_component(idx).await, + } + } + } + + KeyCode::Char('l') => { + if let Some(idx) = comp_idx { + app.log_scroll = app.components[idx].logs.lock().unwrap().len().saturating_sub(1); + app.screen = Screen::Logs(idx); + } + } + + KeyCode::Char('v') => app.show_log_panel = !app.show_log_panel, + KeyCode::Char('a') => app.start_all_enabled().await, + KeyCode::Char('x') => app.stop_all().await, + KeyCode::Esc => app.screen = Screen::Config, + KeyCode::Char('q') | KeyCode::Char('Q') => return true, + _ => {} + } + false +} + +pub async fn handle_logs_key( + app: &mut App, + key: KeyCode, + _modifiers: KeyModifiers, + comp_idx: usize, +) -> bool { + let log_len = app.components[comp_idx].logs.lock().unwrap().len(); + match key { + KeyCode::Down | KeyCode::Char('j') => { + app.log_scroll = (app.log_scroll + 1).min(log_len.saturating_sub(1)); + } + KeyCode::Up | KeyCode::Char('k') => { + app.log_scroll = app.log_scroll.saturating_sub(1); + } + KeyCode::PageDown => { + app.log_scroll = (app.log_scroll + 20).min(log_len.saturating_sub(1)); + } + KeyCode::PageUp => { + app.log_scroll = app.log_scroll.saturating_sub(20); + } + KeyCode::Char('G') => app.log_scroll = log_len.saturating_sub(1), + KeyCode::Char('g') => app.log_scroll = 0, + KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('l') => { + app.screen = Screen::Runtime; + } + _ => {} + } + false +} diff --git a/src/bitbots_misc/bitbots_bringup/tui/src/main.rs b/src/bitbots_misc/bitbots_bringup/tui/src/main.rs new file mode 100644 index 000000000..fc4ed7760 --- /dev/null +++ b/src/bitbots_misc/bitbots_bringup/tui/src/main.rs @@ -0,0 +1,235 @@ +mod app; +mod components; +mod input; +mod ui; + +use anyhow::Result; +use clap::Parser; +use crossterm::{ + event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use futures::StreamExt; +use ratatui::{backend::CrosstermBackend, Terminal}; +use std::time::Duration; +use tokio::time; + +use app::{App, ProcState, Screen}; +use components::COMPONENT_DEFS; + +// ─── CLI ────────────────────────────────────────────────────────────────────── + +#[derive(Parser)] +#[command( + name = "teamplayer", + about = "Bit-Bots teamplayer launcher — interactive TUI or headless mode", + long_about = None, +)] +struct Cli { + /// Run without TUI: start all enabled components and block until Ctrl-C + #[arg(long)] + headless: bool, + + /// Enable simulation mode (also enables the simulator, disables hardware-only components) + #[arg(long)] + sim: bool, + + /// Override the field name [default: labor, or hsl_kid in sim] + #[arg(long, value_name = "NAME")] + fieldname: Option, + + /// Override the behavior DSD file [default: main.dsd] + #[arg(long, value_name = "FILE", default_value = "main.dsd")] + dsd_file: String, + + /// Disable the Zenoh daemon (useful when zenoh is already running externally) + #[arg(long)] + no_zenoh: bool, + + /// Enable a component by key; may be specified multiple times. + /// Available keys: zenoh, blackboard, robot_description, diagnostics, + /// simulator, lowlevel, motion, game_controller, vision, ipm, localization, + /// path_planning, behavior, teamcom, world_model, whistle_detector, audio, + /// tts, monitoring, record + #[arg(long = "enable", value_name = "KEY")] + enable: Vec, + + /// Disable a component by key; may be specified multiple times + #[arg(long = "disable", value_name = "KEY")] + disable: Vec, +} + +// ─── Terminal helpers ───────────────────────────────────────────────────────── + +fn setup_panic_hook() { + let original = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + let _ = disable_raw_mode(); + let _ = execute!( + std::io::stdout(), + LeaveAlternateScreen, + DisableMouseCapture + ); + original(info); + })); +} + +fn cleanup_terminal() { + let _ = disable_raw_mode(); + let _ = execute!( + std::io::stdout(), + LeaveAlternateScreen, + DisableMouseCapture + ); +} + +// ─── Entry point ────────────────────────────────────────────────────────────── + +#[tokio::main] +async fn main() -> Result<()> { + let cli = Cli::parse(); + + let mut app = App::new(); + app.apply_cli_presets( + cli.sim, + cli.no_zenoh, + cli.fieldname.as_deref(), + &cli.dsd_file, + &cli.enable, + &cli.disable, + ); + + let result = if cli.headless { + run_headless(&mut app).await + } else { + setup_panic_hook(); + enable_raw_mode()?; + let mut stdout = std::io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let mut event_stream = EventStream::new(); + let mut tick_interval = time::interval(Duration::from_millis(100)); + let r = run_tui(&mut terminal, &mut app, &mut event_stream, &mut tick_interval).await; + cleanup_terminal(); + r + }; + + // SIGTERM all remaining process groups on exit + { + use nix::sys::signal::{killpg, Signal}; + use nix::unistd::Pid; + for comp in &app.components { + for &pid in &comp.pids { + let _ = killpg(Pid::from_raw(pid as i32), Signal::SIGTERM); + } + } + } + + result +} + +// ─── Headless mode ──────────────────────────────────────────────────────────── + +async fn run_headless(app: &mut App) -> Result<()> { + eprintln!("teamplayer [headless] — Ctrl-C to stop"); + eprintln!(); + + // Print what will be started + for comp in &app.components { + let def = &COMPONENT_DEFS[comp.comp_idx]; + if comp.enabled { + eprintln!(" [+] {}", def.name); + } + } + eprintln!(); + + app.start_all_enabled().await; + + let mut tick = time::interval(Duration::from_millis(50)); + loop { + tokio::select! { + _ = tick.tick() => { + let to_restart = app.drain_messages(); + for idx in to_restart { + app.start_component(idx).await; + } + { + let mut logs = app.all_logs.lock().unwrap(); + while let Some(entry) = logs.pop_front() { + println!("{entry}"); + } + } + let any_running = app.components.iter().any(|c| { + matches!(c.state, ProcState::Running | ProcState::Stopping | ProcState::Restarting) + }); + if !any_running { + eprintln!("All components have stopped."); + break; + } + } + _ = tokio::signal::ctrl_c() => { + eprintln!("\nInterrupted — stopping all components..."); + break; + } + } + } + Ok(()) +} + +// ─── TUI event loop ─────────────────────────────────────────────────────────── + +async fn run_tui( + terminal: &mut Terminal>, + app: &mut App, + event_stream: &mut EventStream, + tick_interval: &mut time::Interval, +) -> Result<()> { + loop { + terminal.draw(|f| ui::draw(f, app))?; + + tokio::select! { + _ = tick_interval.tick() => { + app.tick_count += 1; + let to_restart = app.drain_messages(); + for idx in to_restart { + app.start_component(idx).await; + } + if let Screen::Logs(idx) = &app.screen { + let idx = *idx; + let len = app.components[idx].logs.lock().unwrap().len(); + if app.log_scroll + 5 >= len.saturating_sub(1) { + app.log_scroll = len.saturating_sub(1); + } + } + } + + maybe_event = event_stream.next() => { + let Some(Ok(event)) = maybe_event else { break }; + if let Event::Key(key_event) = event { + let quit = match app.screen.clone() { + Screen::Config => { + input::handle_config_key(app, key_event.code, key_event.modifiers).await + } + Screen::Runtime => { + input::handle_runtime_key(app, key_event.code, key_event.modifiers).await + } + Screen::Logs(idx) => { + input::handle_logs_key(app, key_event.code, key_event.modifiers, idx).await + } + }; + if quit { + return Ok(()); + } + } + } + + _ = tokio::signal::ctrl_c() => { + return Ok(()); + } + } + } + Ok(()) +} diff --git a/src/bitbots_misc/bitbots_bringup/tui/src/ui.rs b/src/bitbots_misc/bitbots_bringup/tui/src/ui.rs new file mode 100644 index 000000000..2b5e6c1af --- /dev/null +++ b/src/bitbots_misc/bitbots_bringup/tui/src/ui.rs @@ -0,0 +1,501 @@ +use ratatui::{ + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph, Wrap}, + Frame, +}; + +use crate::app::{App, ConfigFocus, ProcState, Screen}; +use crate::components::COMPONENT_DEFS; + +// ─── Banner ─────────────────────────────────────────────────────────────────── + +const BANNER: &str = r#" + ██████╗ ██╗████████╗ ██████╗ ██████╗ ████████╗███████╗ + ██╔══██╗██║╚══██╔══╝ ██╔══██╗██╔═══██╗╚══██╔══╝██╔════╝ + ██████╔╝██║ ██║ ██████╔╝██║ ██║ ██║ ███████╗ + ██╔══██╗██║ ██║ ██╔══██╗██║ ██║ ██║ ╚════██║ + ██████╔╝██║ ██║ ██████╔╝╚██████╔╝ ██║ ███████║ + ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚══════╝ + Teamplayer Launcher +"#; + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +pub fn proc_state_style(state: &ProcState) -> Style { + match state { + ProcState::Idle => Style::default().fg(Color::DarkGray), + ProcState::Running => Style::default().fg(Color::Green), + ProcState::Stopping => Style::default().fg(Color::Yellow), + ProcState::Restarting => Style::default().fg(Color::Cyan), + ProcState::Stopped => Style::default().fg(Color::Blue), + ProcState::Crashed => Style::default().fg(Color::Red), + } +} + +fn spinner_char(tick: u64) -> char { + const FRAMES: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; + FRAMES[(tick as usize) % FRAMES.len()] +} + +// ─── Top-level dispatch ─────────────────────────────────────────────────────── + +pub fn draw(f: &mut Frame, app: &App) { + match &app.screen { + Screen::Config => draw_config(f, app), + Screen::Runtime => draw_runtime(f, app), + Screen::Logs(idx) => draw_logs(f, app, *idx), + } +} + +// ─── Config screen ──────────────────────────────────────────────────────────── + +pub fn draw_config(f: &mut Frame, app: &App) { + let area = f.size(); + let banner_lines = BANNER.lines().count() as u16; + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(banner_lines), + Constraint::Length(3), + Constraint::Min(0), + Constraint::Length(3), + Constraint::Length(1), + ]) + .split(area); + + f.render_widget( + Paragraph::new(BANNER) + .style(Style::default().fg(Color::Cyan)) + .alignment(Alignment::Center), + chunks[0], + ); + + draw_config_flags(f, app, chunks[1]); + draw_config_components(f, app, chunks[2]); + draw_start_button(f, app, chunks[3]); + + f.render_widget( + Paragraph::new(" Tab/↑↓: navigate Space/Enter: toggle/activate q: quit") + .style(Style::default().fg(Color::DarkGray)), + chunks[4], + ); +} + +fn draw_config_flags(f: &mut Frame, app: &App, area: Rect) { + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Length(18), + Constraint::Length(18), + Constraint::Min(20), + Constraint::Min(20), + ]) + .split(area); + + let focus_border = |focused: bool| { + if focused { + Style::default().fg(Color::Yellow) + } else { + Style::default() + } + }; + let focus_text = |focused: bool| { + if focused { + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::White) + } + }; + + let zenoh_focused = app.config_focus == ConfigFocus::Zenoh; + let sim_focused = app.config_focus == ConfigFocus::Sim; + let field_focused = app.config_focus == ConfigFocus::Fieldname; + let dsd_focused = app.config_focus == ConfigFocus::DsdFile; + + f.render_widget( + Paragraph::new(if app.params.zenoh { "[✓] Zenoh" } else { "[ ] Zenoh" }) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(focus_border(zenoh_focused)), + ) + .style(focus_text(zenoh_focused)), + chunks[0], + ); + + f.render_widget( + Paragraph::new(if app.params.sim { + "[✓] Simulator" + } else { + "[ ] Simulator" + }) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(focus_border(sim_focused)), + ) + .style(focus_text(sim_focused)), + chunks[1], + ); + + f.render_widget( + Paragraph::new(app.config_fieldname_input.as_str()).block( + Block::default() + .borders(Borders::ALL) + .title("Fieldname") + .border_style(focus_border(field_focused)), + ), + chunks[2], + ); + + f.render_widget( + Paragraph::new(app.config_dsd_input.as_str()).block( + Block::default() + .borders(Borders::ALL) + .title("DSD File") + .border_style(focus_border(dsd_focused)), + ), + chunks[3], + ); +} + +fn draw_config_components(f: &mut Frame, app: &App, area: Rect) { + let toggleables = app.toggleable_indices(); + let col_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(area); + + let left_count = (toggleables.len() + 1) / 2; + let (left, right) = toggleables.split_at(left_count); + draw_component_column(f, app, col_chunks[0], left, 0); + draw_component_column(f, app, col_chunks[1], right, left.len()); +} + +fn draw_component_column( + f: &mut Frame, + app: &App, + area: Rect, + comp_indices: &[usize], + focus_offset: usize, +) { + if comp_indices.is_empty() { + return; + } + let rows: Vec = comp_indices.iter().map(|_| Constraint::Length(1)).collect(); + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints(rows) + .split(area); + + for (local_i, &comp_idx) in comp_indices.iter().enumerate() { + let focused = app.config_focus == ConfigFocus::Component(focus_offset + local_i); + let comp = &app.components[comp_idx]; + let def = &COMPONENT_DEFS[comp_idx]; + + let check = if comp.enabled { "✓" } else { " " }; + let style = if focused { + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD) + } else if comp.enabled { + Style::default().fg(Color::White) + } else { + Style::default().fg(Color::DarkGray) + }; + + if local_i < chunks.len() { + f.render_widget( + Paragraph::new(format!("[{check}] {}", def.name)).style(style), + chunks[local_i], + ); + } + } +} + +fn draw_start_button(f: &mut Frame, app: &App, area: Rect) { + let focused = app.config_focus == ConfigFocus::Start; + let has_running = app.components.iter().any(|c| c.state == ProcState::Running); + let label = if has_running { + " Continue → " + } else { + " Launch → " + }; + let (border_style, text_style) = if focused { + ( + Style::default().fg(Color::Yellow), + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ) + } else { + ( + Style::default().fg(Color::Green), + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD), + ) + }; + f.render_widget( + Paragraph::new(label) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(border_style), + ) + .style(text_style) + .alignment(Alignment::Center), + area, + ); +} + +// ─── Runtime screen ─────────────────────────────────────────────────────────── + +pub fn draw_runtime(f: &mut Frame, app: &App) { + let area = f.size(); + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(3), Constraint::Min(0), Constraint::Length(1)]) + .split(area); + + draw_runtime_header(f, app, chunks[0]); + + if app.show_log_panel { + let cols = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(35), Constraint::Percentage(65)]) + .split(chunks[1]); + draw_runtime_list(f, app, cols[0]); + draw_joint_logs(f, app, cols[1]); + } else { + draw_runtime_list(f, app, chunks[1]); + } + + let hint = if app.show_log_panel { + " ↑↓/jk: select s: stop/start r: restart l: logs a: start all x: stop all v: hide logs Esc: config q: quit" + } else { + " ↑↓/jk: select s: stop/start r: restart l: logs a: start all x: stop all v: show logs Esc: config q: quit" + }; + f.render_widget( + Paragraph::new(hint).style(Style::default().fg(Color::DarkGray)), + chunks[2], + ); +} + +fn draw_runtime_header(f: &mut Frame, app: &App, area: Rect) { + let running = app + .components + .iter() + .filter(|c| c.state == ProcState::Running) + .count(); + let total_enabled = app.components.iter().filter(|c| c.enabled).count(); + let sim_badge = if app.params.sim { " [SIM]" } else { "" }; + f.render_widget( + Paragraph::new(format!( + " Bit-Bots Teamplayer [{running}/{total_enabled} running]{sim_badge}" + )) + .block(Block::default().borders(Borders::ALL)) + .style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)), + area, + ); +} + +fn draw_runtime_list(f: &mut Frame, app: &App, area: Rect) { + let visible = app.runtime_visible_indices(); + if visible.is_empty() { + f.render_widget( + Paragraph::new("No components enabled.").alignment(Alignment::Center), + area, + ); + return; + } + + let rows: Vec = visible.iter().map(|_| Constraint::Length(1)).collect(); + let row_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints(rows) + .split(area); + + for (vis_i, &comp_idx) in visible.iter().enumerate() { + if vis_i >= row_chunks.len() { + break; + } + let selected = app.runtime_selected == vis_i; + let comp = &app.components[comp_idx]; + let def = &COMPONENT_DEFS[comp_idx]; + + let row_style = if selected { + Style::default().bg(Color::DarkGray) + } else { + Style::default() + }; + let spinner = match comp.state { + ProcState::Running => format!("{} ", spinner_char(app.tick_count)), + ProcState::Stopping => "… ".to_string(), + ProcState::Restarting => "↺ ".to_string(), + _ => " ".to_string(), + }; + + let line = Line::from(vec![ + Span::styled( + if def.infrastructure { "·" } else { " " }, + Style::default().fg(Color::DarkGray), + ), + Span::styled(spinner, Style::default().fg(Color::Green)), + Span::styled( + format!("[{}]", comp.state.label()), + proc_state_style(&comp.state).add_modifier(Modifier::BOLD), + ), + Span::raw(" "), + Span::styled( + format!("{:<18}", def.name), + if selected { + Style::default() + .fg(Color::White) + .add_modifier(Modifier::BOLD) + } else { + Style::default() + }, + ), + Span::raw(" "), + Span::styled("s", Style::default().fg(Color::Yellow)), + Span::raw(":start/stop "), + Span::styled("r", Style::default().fg(Color::Yellow)), + Span::raw(":restart "), + Span::styled("l", Style::default().fg(Color::Yellow)), + Span::raw(":logs"), + ]); + f.render_widget(Paragraph::new(line).style(row_style), row_chunks[vis_i]); + } +} + +fn tag_color(tag: &str) -> Color { + const PALETTE: &[Color] = &[ + Color::Cyan, + Color::Green, + Color::Magenta, + Color::Yellow, + Color::LightBlue, + Color::LightGreen, + Color::LightMagenta, + Color::LightRed, + ]; + let h = tag.bytes().fold(0usize, |a, b| a.wrapping_add(b as usize)); + PALETTE[h % PALETTE.len()] +} + +/// Convert a raw log string (which may contain ANSI escape codes) into an owned ratatui Line. +fn ansi_to_line(s: &str) -> Line<'static> { + use ansi_to_tui::IntoText; + match s.into_text() { + Ok(text) => Line::from( + text.lines + .into_iter() + .flat_map(|l| l.spans) + .map(|s| Span::styled(s.content.into_owned(), s.style)) + .collect::>(), + ), + Err(_) => Line::from(s.to_owned()), + } +} + +fn draw_joint_logs(f: &mut Frame, app: &App, area: Rect) { + let inner_h = area.height.saturating_sub(2) as usize; + let inner_w = area.width.saturating_sub(2) as usize; + + let lines: Vec> = { + let logs = app.all_logs.lock().unwrap(); + // Grab more entries than the panel height — wrapping will consume extra rows. + let skip = logs.len().saturating_sub(inner_h * 3); + logs.iter() + .skip(skip) + .map(|entry| { + if let Some(close) = entry.find("] ") { + let tag = &entry[..close + 1]; // "[CompName]" + let rest = &entry[close + 2..]; + let mut spans: Vec> = vec![ + Span::styled( + tag.to_owned(), + Style::default().fg(tag_color(tag)).add_modifier(Modifier::BOLD), + ), + Span::raw(" "), + ]; + spans.extend(ansi_to_line(rest).spans); + Line::from(spans) + } else { + ansi_to_line(entry) + } + }) + .collect() + }; + + // Estimate total visual rows so we can scroll to pin the view to the bottom. + let total_rows: usize = lines + .iter() + .map(|l| { + let chars: usize = l.spans.iter().map(|s| s.content.chars().count()).sum(); + if inner_w > 0 { (chars + inner_w - 1) / inner_w } else { 1 }.max(1) + }) + .sum(); + let scroll = total_rows.saturating_sub(inner_h) as u16; + + f.render_widget( + Paragraph::new(lines) + .block(Block::default().borders(Borders::ALL).title(" All Logs ")) + .wrap(Wrap { trim: false }) + .scroll((scroll, 0)), + area, + ); +} + +// ─── Logs screen ────────────────────────────────────────────────────────────── + +pub fn draw_logs(f: &mut Frame, app: &App, comp_idx: usize) { + let area = f.size(); + let comp = &app.components[comp_idx]; + let name = COMPONENT_DEFS[comp_idx].name; + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(3), Constraint::Min(0), Constraint::Length(1)]) + .split(area); + + f.render_widget( + Paragraph::new(format!(" Logs: {name} ")) + .block(Block::default().borders(Borders::ALL)) + .style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)), + chunks[0], + ); + + let log_height = chunks[1].height as usize; + let lines: Vec> = { + let logs = comp.logs.lock().unwrap(); + let total = logs.len(); + let max_scroll = if total > log_height { total - log_height } else { 0 }; + let scroll = app.log_scroll.min(max_scroll); + logs.iter() + .skip(scroll) + .take(log_height * 2) // extra entries so wrapped lines still fill the panel + .map(|s| ansi_to_line(s)) + .collect() + }; + + f.render_widget( + Paragraph::new(lines) + .block(Block::default().borders(Borders::ALL)) + .style(Style::default().fg(Color::White)) + .wrap(Wrap { trim: false }), + chunks[1], + ); + + f.render_widget( + Paragraph::new(" ↑↓/jk/PgUp/PgDn: scroll g: top G: bottom Esc/q/l: back") + .style(Style::default().fg(Color::DarkGray)), + chunks[2], + ); +} diff --git a/src/bitbots_motion/bitbots_rl_walk/bitbots_rl_walk/walk.py b/src/bitbots_motion/bitbots_rl_walk/bitbots_rl_walk/walk.py index eeb6ef7ba..3091b1bcc 100644 --- a/src/bitbots_motion/bitbots_rl_walk/bitbots_rl_walk/walk.py +++ b/src/bitbots_motion/bitbots_rl_walk/bitbots_rl_walk/walk.py @@ -20,6 +20,7 @@ import numpy as np import onnxruntime as rt from ament_index_python import get_package_share_directory +from rclpy.experimental.events_executor import EventsExecutor from geometry_msgs.msg import Twist from rclpy.node import Node from sensor_msgs.msg import Imu, JointState @@ -195,6 +196,10 @@ def main(): rclpy.init() node = WalkNode() - rclpy.spin(node) - node.destroy_node() - rclpy.try_shutdown() + + executor = EventsExecutor() + executor.add_node(node) + try: + executor.spin() + except KeyboardInterrupt: + pass diff --git a/src/bitbots_team_communication/bitbots_team_communication/bitbots_team_communication/bitbots_team_communication.py b/src/bitbots_team_communication/bitbots_team_communication/bitbots_team_communication/bitbots_team_communication.py index d52c6e7a1..41c9d069c 100755 --- a/src/bitbots_team_communication/bitbots_team_communication/bitbots_team_communication/bitbots_team_communication.py +++ b/src/bitbots_team_communication/bitbots_team_communication/bitbots_team_communication/bitbots_team_communication.py @@ -3,6 +3,7 @@ import socket import struct import threading +import time from typing import Optional import rclpy @@ -63,24 +64,16 @@ def __init__(self): self.tf_buffer = Buffer(node=self.node) - self.run_spin_in_thread() self.try_to_establish_connection() + self.logger.info("Successfully established connection.") + self.node.create_timer(1 / self.rate, self.send_message, callback_group=MutuallyExclusiveCallbackGroup()) - self.receive_forever() - def spin(self): - executor = EventsExecutor() - executor.add_node(self.node) - try: - executor.spin() - except KeyboardInterrupt: - pass + receive_thread = threading.Thread(target=self.receive_forever, daemon=True) + receive_thread.start() - def run_spin_in_thread(self): - # Necessary in ROS2, else we are forever stuck receiving messages - thread = threading.Thread(target=self.spin, daemon=True) - thread.start() + self.logger.info("Initialization complete.") def set_state_defaults(self): self.gamestate: Optional[GameState] = None @@ -100,8 +93,9 @@ def set_state_defaults(self): def try_to_establish_connection(self): # we will try multiple times till we manage to get a connection while rclpy.ok() and not self.socket_communication.is_setup(): + self.logger.info("Trying to establish connection...") self.socket_communication.establish_connection() - self.node.get_clock().sleep_for(Duration(seconds=1)) + time.sleep(1) def create_publishers(self): self.team_data_publisher = self.node.create_publisher(TeamData, self.topics["team_data_topic"], qos_profile=1) @@ -317,8 +311,14 @@ def convert_to_euler(self, quaternion: Quaternion): def main(): rclpy.init(args=None) - TeamCommunication() - + tc = TeamCommunication() + + executor = EventsExecutor() + executor.add_node(tc.node) + try: + executor.spin() + except KeyboardInterrupt: + pass if __name__ == "__main__": main()