From 674dd6162c8dd0fb9a891671d38bbd69ef885878 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Urrea <68788933+jsurrea@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:15:21 -0500 Subject: [PATCH 01/17] rename rust project to antares --- antares/.gitignore | 1 + antares/Cargo.lock | 486 ++++++++++++++++++ antares/Cargo.toml | 10 + antares/README.md | 151 ++++++ antares/assets/config.toml | 21 + antares/src/config.rs | 14 + antares/src/controller.rs | 28 + antares/src/lib.rs | 16 + antares/src/main.rs | 24 + antares/src/radar/config.rs | 22 + antares/src/radar/detector/detector.rs | 71 +++ antares/src/radar/detector/mod.rs | 6 + antares/src/radar/detector/plot.rs | 10 + antares/src/radar/mod.rs | 18 + .../protocol/constants/client_command.rs | 72 +++ .../radar/protocol/constants/error_message.rs | 50 ++ .../protocol/constants/interface_ports.rs | 2 + antares/src/radar/protocol/constants/mod.rs | 15 + .../protocol/constants/server_command.rs | 63 +++ antares/src/radar/protocol/mod.rs | 5 + .../tcp_interfaces/base_track_interface.rs | 150 ++++++ .../src/radar/protocol/tcp_interfaces/mod.rs | 12 + .../tcp_interfaces/track_control_interface.rs | 46 ++ .../tcp_interfaces/track_data_interface.rs | 54 ++ antares/src/radar/radar.rs | 36 ++ antares/src/radar/tracker/mod.rs | 6 + antares/src/radar/tracker/track.rs | 84 +++ antares/src/radar/tracker/tracker.rs | 65 +++ antares/src/simulation/config.rs | 40 ++ antares/src/simulation/emitters/emitter.rs | 5 + antares/src/simulation/emitters/mod.rs | 8 + antares/src/simulation/emitters/ship.rs | 29 ++ antares/src/simulation/environment/mod.rs | 2 + antares/src/simulation/environment/wave.rs | 11 + antares/src/simulation/mod.rs | 19 + antares/src/simulation/movement/circle.rs | 34 ++ antares/src/simulation/movement/line.rs | 21 + antares/src/simulation/movement/mod.rs | 11 + antares/src/simulation/movement/random.rs | 22 + antares/src/simulation/movement/stationary.rs | 12 + antares/src/simulation/movement/strategy.rs | 14 + antares/src/simulation/simulation.rs | 92 ++++ antares/src/utils/escape_ascii.rs | 43 ++ antares/src/utils/mod.rs | 9 + antares/src/utils/thread_pool.rs | 93 ++++ 45 files changed, 2003 insertions(+) create mode 100644 antares/.gitignore create mode 100644 antares/Cargo.lock create mode 100644 antares/Cargo.toml create mode 100644 antares/README.md create mode 100644 antares/assets/config.toml create mode 100644 antares/src/config.rs create mode 100644 antares/src/controller.rs create mode 100644 antares/src/lib.rs create mode 100644 antares/src/main.rs create mode 100644 antares/src/radar/config.rs create mode 100644 antares/src/radar/detector/detector.rs create mode 100644 antares/src/radar/detector/mod.rs create mode 100644 antares/src/radar/detector/plot.rs create mode 100644 antares/src/radar/mod.rs create mode 100644 antares/src/radar/protocol/constants/client_command.rs create mode 100644 antares/src/radar/protocol/constants/error_message.rs create mode 100644 antares/src/radar/protocol/constants/interface_ports.rs create mode 100644 antares/src/radar/protocol/constants/mod.rs create mode 100644 antares/src/radar/protocol/constants/server_command.rs create mode 100644 antares/src/radar/protocol/mod.rs create mode 100644 antares/src/radar/protocol/tcp_interfaces/base_track_interface.rs create mode 100644 antares/src/radar/protocol/tcp_interfaces/mod.rs create mode 100644 antares/src/radar/protocol/tcp_interfaces/track_control_interface.rs create mode 100644 antares/src/radar/protocol/tcp_interfaces/track_data_interface.rs create mode 100644 antares/src/radar/radar.rs create mode 100644 antares/src/radar/tracker/mod.rs create mode 100644 antares/src/radar/tracker/track.rs create mode 100644 antares/src/radar/tracker/tracker.rs create mode 100644 antares/src/simulation/config.rs create mode 100644 antares/src/simulation/emitters/emitter.rs create mode 100644 antares/src/simulation/emitters/mod.rs create mode 100644 antares/src/simulation/emitters/ship.rs create mode 100644 antares/src/simulation/environment/mod.rs create mode 100644 antares/src/simulation/environment/wave.rs create mode 100644 antares/src/simulation/mod.rs create mode 100644 antares/src/simulation/movement/circle.rs create mode 100644 antares/src/simulation/movement/line.rs create mode 100644 antares/src/simulation/movement/mod.rs create mode 100644 antares/src/simulation/movement/random.rs create mode 100644 antares/src/simulation/movement/stationary.rs create mode 100644 antares/src/simulation/movement/strategy.rs create mode 100644 antares/src/simulation/simulation.rs create mode 100644 antares/src/utils/escape_ascii.rs create mode 100644 antares/src/utils/mod.rs create mode 100644 antares/src/utils/thread_pool.rs diff --git a/antares/.gitignore b/antares/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/antares/.gitignore @@ -0,0 +1 @@ +/target diff --git a/antares/Cargo.lock b/antares/Cargo.lock new file mode 100644 index 0000000..f1d4c3c --- /dev/null +++ b/antares/Cargo.lock @@ -0,0 +1,486 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "naval-radar-simulator" +version = "0.1.0" +dependencies = [ + "chrono", + "rand", + "serde", + "toml", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "serde" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/antares/Cargo.toml b/antares/Cargo.toml new file mode 100644 index 0000000..7f330ce --- /dev/null +++ b/antares/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "naval-radar-simulator" +version = "0.1.0" +edition = "2021" + +[dependencies] +chrono = "0.4.38" +rand = "0.8.5" +serde = { version = "1.0.214", features = ["derive"] } +toml = "0.8.19" diff --git a/antares/README.md b/antares/README.md new file mode 100644 index 0000000..13e6b0d --- /dev/null +++ b/antares/README.md @@ -0,0 +1,151 @@ +# Naval Radar - Simulator + +The Naval Radar Simulator is a robust project designed to simulate radar systems, generate radar data, and emulate real-world scenarios involving radar detection and tracking. + +## **Features** + +- **Configurable Radar Simulation**: + - Define radar parameters like range, resolution, and target detection. +- **Dynamic Simulation**: + - Simulates target movements (linear, random, circular, stationary) and environmental effects. +- **Communication Protocol**: + - Implements TCP interfaces for communication between radar components. +- **Realistic Tracking**: + - Includes tracking algorithms for managing targets. + +## **Setup Instructions** + +1. **Install Rust** + Make sure you have Rust and cargo installed. If not, you can install them following the instructions on the [Rust website](https://www.rust-lang.org/tools/install). + +2. **Go to the Project Directory** + + ```bash + cd naval-radar-simulator + ``` + +3. **Build the Project** + There are 2 ways to build the project: + + - **Development Build**: + ```bash + cargo build + ``` + - **Release Build**: + ```bash + cargo build --release + ``` + + It is recommended to use the release build for better performance. Use the development build for debugging and testing. + +4. **Run the Simulator** + Run the simulator with a configuration file: + + For the release build: + + ```bash + ./target/release/naval-radar-simulator + ``` + + For the development build: + + ```bash + cargo run -- + ``` + + Replace `` with the path to your TOML configuration file. + +## **Configuration File** + +The simulator uses a TOML configuration file to define settings such as radar range, simulation parameters, and environment details. A sample configuration file might look like this: + +```toml +[radar] + +[radar.protocol] +host = "0.0.0.0" +num_workers_tci = 1 +num_workers_tdi = 1 + +[radar.detector] +range = 100.0 +speed = 10.0 +angle = 0.0 +start_coordinates = [4.0, -72.0] + +[simulation] +emission_interval = 20 + +[simulation.ships] +line = [{ initial_position = [-50.0, 50.0], angle = 0.785, speed = 5.0 }] +circle = [{ initial_position = [50.0, -50.0], radius = 20.0, speed = 5.0 }] +random = [{ initial_position = [-50.0, -50.0], max_speed = 20.0 }] +stationary = [{ initial_position = [50.0, 50.0] }] +``` + +## **Directory Structure** + +The directory structure organizes the project for clarity and scalability: + +```plaintext +src +├── config.rs # Manages configuration structures and settings +├── controller.rs # Manages the Controller struct, starting the radar and simulation +├── lib.rs # Main library file with module definitions +├── main.rs # Entry point for the simulator +├── radar/ # Radar simulation logic +│ ├── config.rs # Configuration for radar-specific settings +│ ├── detector/ # Radar detection logic +│ │ ├── detector.rs # Core detection algorithms +│ │ ├── mod.rs # Detector module entry point +│ │ └── plot.rs # Handles radar plot generation +│ ├── mod.rs # Radar module entry point +│ ├── protocol/ # Radar communication protocol +│ │ ├── constants/ # Constants used in protocol definitions +│ │ │ ├── client_command.rs +│ │ │ ├── error_message.rs +│ │ │ ├── interface_ports.rs +│ │ │ ├── mod.rs +│ │ │ └── server_command.rs +│ │ ├── mod.rs # Protocol module entry point +│ │ └── tcp_interfaces/ +│ │ ├── base_track_interface.rs +│ │ ├── mod.rs +│ │ ├── track_control_interface.rs +│ │ └── track_data_interface.rs +│ ├── radar.rs # Core radar logic +│ └── tracker/ # Radar tracking algorithms +│ ├── mod.rs +│ ├── track.rs +│ └── tracker.rs +├── simulation/ # Simulation logic for the radar +│ ├── config.rs # Configuration for the simulation module +│ ├── emitters/ # Handles simulated emitters like ships or targets +│ │ ├── emitter.rs +│ │ ├── mod.rs +│ │ └── ship.rs +│ ├── environment/ # Simulated environmental effects +│ │ ├── mod.rs +│ │ └── wave.rs +│ ├── mod.rs # Simulation module entry point +│ ├── movement/ # Movement strategies for targets +│ │ ├── circle.rs +│ │ ├── line.rs +│ │ ├── mod.rs +│ │ ├── random.rs +│ │ ├── stationary.rs +│ │ └── strategy.rs +│ └── simulation.rs # Core simulation logic +└── utils/ # Utility functions and reusable structures + ├── escape_ascii.rs # ASCII character processing utilities + ├── mod.rs # Utils module entry point + └── thread_pool.rs # Thread pool implementation for concurrency +``` + +## **Other Dependencies** + +- **`chrono`**: Handles date and time functionality. +- **`rand`**: Generates random numbers for simulation randomness. +- **`serde` and `serde_derive`**: Serializes and deserializes data structures. +- **`toml`**: Parses TOML configuration files. +- **`std::thread`**: For multi-threaded processing. diff --git a/antares/assets/config.toml b/antares/assets/config.toml new file mode 100644 index 0000000..8d05dc6 --- /dev/null +++ b/antares/assets/config.toml @@ -0,0 +1,21 @@ +[radar] + +[radar.protocol] +host = "0.0.0.0" +num_workers_tci = 1 +num_workers_tdi = 1 + +[radar.detector] +range = 100.0 +speed = 0.0 +angle = 0.0 +start_coordinates = [4.0, -72.0] + +[simulation] +emission_interval = 20 + +[simulation.ships] +line = [{ initial_position = [-50.0, 50.0], angle = 0.785, speed = 5.0 }] +circle = [{ initial_position = [50.0, -50.0], radius = 20.0, speed = 5.0 }] +random = [{ initial_position = [-50.0, -50.0], max_speed = 20.0 }] +stationary = [{ initial_position = [50.0, 50.0] }] diff --git a/antares/src/config.rs b/antares/src/config.rs new file mode 100644 index 0000000..102d97b --- /dev/null +++ b/antares/src/config.rs @@ -0,0 +1,14 @@ +//! # Config module +//! +//! This module contains the configuration struct for the simulation and radar. +//! The configuration is loaded from a TOML file. +//! + +use super::{RadarConfig, SimulationConfig}; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Config { + pub simulation: SimulationConfig, + pub radar: RadarConfig, +} diff --git a/antares/src/controller.rs b/antares/src/controller.rs new file mode 100644 index 0000000..05b2492 --- /dev/null +++ b/antares/src/controller.rs @@ -0,0 +1,28 @@ +//! Controller module +//! +//! This module contains the Controller struct which is responsible for starting the simulation and radar. +//! + +use super::{Config, Radar, Simulation}; +use std::sync::mpsc::channel; + +pub struct Controller { + radar: Radar, + simulation: Simulation, +} + +impl Controller { + pub fn new(config: Config) -> Controller { + Controller { + radar: Radar::new(config.radar), + simulation: Simulation::new(config.simulation), + } + } + + pub fn run(self) { + let (wave_sender, wave_receiver) = channel(); + self.simulation.start(wave_sender); + self.radar.start(wave_receiver); + loop {} + } +} diff --git a/antares/src/lib.rs b/antares/src/lib.rs new file mode 100644 index 0000000..aa888dd --- /dev/null +++ b/antares/src/lib.rs @@ -0,0 +1,16 @@ +//! # Radar Simulation Library +//! +//! `radar-simulation` is a library for simulating radar data. +//! + +mod config; +mod controller; +mod radar; +mod simulation; +mod utils; + +use radar::{Radar, RadarConfig}; +use simulation::{Simulation, SimulationConfig, Wave}; + +pub use config::Config; +pub use controller::Controller; diff --git a/antares/src/main.rs b/antares/src/main.rs new file mode 100644 index 0000000..dc6c420 --- /dev/null +++ b/antares/src/main.rs @@ -0,0 +1,24 @@ +use naval_radar_simulator::{Config, Controller}; +use std::{env, fs, process}; + +fn main() { + let args: Vec = env::args().collect(); + + if args.len() != 2 { + eprintln!("Usage: naval_radar "); + process::exit(1); + } + + let config_content = fs::read_to_string(&args[1]).unwrap_or_else(|err| { + eprintln!("Problem reading the config file: {err}"); + process::exit(1); + }); + + let config: Config = toml::from_str(&config_content).unwrap_or_else(|err| { + eprintln!("Problem parsing the config file: {err}"); + process::exit(1); + }); + + let controller = Controller::new(config); + controller.run(); +} diff --git a/antares/src/radar/config.rs b/antares/src/radar/config.rs new file mode 100644 index 0000000..5a1cadf --- /dev/null +++ b/antares/src/radar/config.rs @@ -0,0 +1,22 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct RadarConfig { + pub detector: DetectorConfig, + pub protocol: ProtocolConfig, +} + +#[derive(Debug, Deserialize)] +pub struct ProtocolConfig { + pub host: String, + pub num_workers_tci: usize, + pub num_workers_tdi: usize, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct DetectorConfig { + pub range: f64, + pub speed: f64, + pub angle: f64, + pub start_coordinates: (f64, f64), +} diff --git a/antares/src/radar/detector/detector.rs b/antares/src/radar/detector/detector.rs new file mode 100644 index 0000000..afa47c0 --- /dev/null +++ b/antares/src/radar/detector/detector.rs @@ -0,0 +1,71 @@ +use crate::radar::config::DetectorConfig; + +use super::{Plot, Wave}; +use chrono::{DateTime, Utc}; +use std::sync::mpsc::{Receiver, Sender}; +use std::thread; + +pub struct Detector { + pub range: f64, + pub speed: f64, + pub angle: f64, + pub start_coordinates: (f64, f64), + pub start_time: DateTime, +} + +impl Detector { + pub fn new(config: DetectorConfig) -> Detector { + Detector { + range: config.range, + speed: config.speed, + angle: config.angle, + start_coordinates: config.start_coordinates, + start_time: chrono::Utc::now(), + } + } + + pub fn start(self, wave_receiver: Receiver, plot_sender: Sender) { + thread::spawn(move || loop { + for wave in wave_receiver.iter() { + let (range, azimuth) = self.calculate_range_azimuth(&wave); + if range > self.range { + continue; + } + + let (latitude, longitude) = self.meters_to_lat_lng(wave.position); + let plot = Plot { + id: wave.id, + range, + azimuth, + timestamp: wave.timestamp, + latitude, + longitude, + }; + plot_sender.send(plot).unwrap(); + } + }); + } + + fn calculate_range_azimuth(&self, wave: &Wave) -> (f64, f64) { + let time_delta = (wave.timestamp - self.start_time).num_milliseconds() as f64 / 1000.0; + let current_position = ( + self.speed * time_delta * self.angle.cos(), + self.speed * time_delta * self.angle.sin(), + ); + let delta_position = ( + wave.position.0 - current_position.0, + wave.position.1 - current_position.1, + ); + let range = (delta_position.0.powi(2) + delta_position.1.powi(2)).sqrt(); + let azimuth = delta_position.1.atan2(delta_position.0); + (range, azimuth) + } + + fn meters_to_lat_lng(&self, position: (f64, f64)) -> (f64, f64) { + let (lat, lng) = self.start_coordinates; + let (dx, dy) = position; + let new_lat = lat + (dy / 111320.0); + let new_lng = lng + (dx / (111320.0 * lat.to_radians().cos())); + (new_lat, new_lng) + } +} diff --git a/antares/src/radar/detector/mod.rs b/antares/src/radar/detector/mod.rs new file mode 100644 index 0000000..bfd1c22 --- /dev/null +++ b/antares/src/radar/detector/mod.rs @@ -0,0 +1,6 @@ +mod detector; +mod plot; + +use super::Wave; +pub use detector::Detector; +pub use plot::Plot; diff --git a/antares/src/radar/detector/plot.rs b/antares/src/radar/detector/plot.rs new file mode 100644 index 0000000..d761ac0 --- /dev/null +++ b/antares/src/radar/detector/plot.rs @@ -0,0 +1,10 @@ +use chrono::{DateTime, Utc}; + +pub struct Plot { + pub id: u64, + pub range: f64, + pub azimuth: f64, + pub latitude: f64, + pub longitude: f64, + pub timestamp: DateTime, +} diff --git a/antares/src/radar/mod.rs b/antares/src/radar/mod.rs new file mode 100644 index 0000000..8650297 --- /dev/null +++ b/antares/src/radar/mod.rs @@ -0,0 +1,18 @@ +//! # Radar module +//! +//! This module contains the radar detector, tracker and communication protocol. +//! + +mod config; +mod detector; +mod protocol; +mod radar; +mod tracker; + +use super::Wave; +use detector::{Detector, Plot}; +use protocol::{TrackControlInterface, TrackDataInterface}; +use tracker::{Track, Tracker}; + +pub use config::RadarConfig; +pub use radar::Radar; diff --git a/antares/src/radar/protocol/constants/client_command.rs b/antares/src/radar/protocol/constants/client_command.rs new file mode 100644 index 0000000..89d11df --- /dev/null +++ b/antares/src/radar/protocol/constants/client_command.rs @@ -0,0 +1,72 @@ +use std::fmt::{Display, Formatter, Result}; + +/// Commands that the client can send to the server +pub enum ClientCommand { + Get, + Ping, + Bye, + Set, + TrackCreate, + TrackDelete, + TrackSwap, + TrackSelect, + TrackMove, + NaazCreate, + NaazDelete, + NtzCreate, + NtzDelete, + AtonCreate, + AtonDelete, + EchoCreate, + EchoDelete, +} + +impl ClientCommand { + pub fn from_str(command: &str) -> Option { + match command.trim().to_ascii_lowercase().as_str() { + "get" => Some(ClientCommand::Get), + "ping" => Some(ClientCommand::Ping), + "bye" => Some(ClientCommand::Bye), + "set" => Some(ClientCommand::Set), + "trackcreate" => Some(ClientCommand::TrackCreate), + "trackdelete" => Some(ClientCommand::TrackDelete), + "trackswap" => Some(ClientCommand::TrackSwap), + "trackselect" => Some(ClientCommand::TrackSelect), + "trackmove" => Some(ClientCommand::TrackMove), + "naazcreate" => Some(ClientCommand::NaazCreate), + "naazdelete" => Some(ClientCommand::NaazDelete), + "ntzcreate" => Some(ClientCommand::NtzCreate), + "ntzdelete" => Some(ClientCommand::NtzDelete), + "atoncreate" => Some(ClientCommand::AtonCreate), + "atondelete" => Some(ClientCommand::AtonDelete), + "echocreate" => Some(ClientCommand::EchoCreate), + "echodelete" => Some(ClientCommand::EchoDelete), + _ => None, + } + } +} + +impl Display for ClientCommand { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + let message = match self { + ClientCommand::Get => "get", + ClientCommand::Ping => "ping", + ClientCommand::Bye => "bye", + ClientCommand::Set => "set", + ClientCommand::TrackCreate => "trackcreate", + ClientCommand::TrackDelete => "trackdelete", + ClientCommand::TrackSwap => "trackswap", + ClientCommand::TrackSelect => "trackselect", + ClientCommand::TrackMove => "trackmove", + ClientCommand::NaazCreate => "naazcreate", + ClientCommand::NaazDelete => "naazdelete", + ClientCommand::NtzCreate => "ntzcreate", + ClientCommand::NtzDelete => "ntzdelete", + ClientCommand::AtonCreate => "atoncreate", + ClientCommand::AtonDelete => "atondelete", + ClientCommand::EchoCreate => "echocreate", + ClientCommand::EchoDelete => "echodelete", + }; + write!(f, "{}", message) + } +} diff --git a/antares/src/radar/protocol/constants/error_message.rs b/antares/src/radar/protocol/constants/error_message.rs new file mode 100644 index 0000000..b36a714 --- /dev/null +++ b/antares/src/radar/protocol/constants/error_message.rs @@ -0,0 +1,50 @@ +use std::fmt::{Display, Formatter, Result}; + +pub enum ErrorMessage { + UnknownCommand, + _IncorrectNumberOfArguments, + _OutOfRadarScope, + _TypeMismatch, + _IllegalValue, + _InternalError, + _NotATrack, + _NaazAlreadyExists, + _NotANaaz, + _NtzAlreadyExists, + _NotANtz, + _PolygonLimitReached, + _AtonAlreadyExists, + _NotAnAton, + _AtonLimitReached, + _EchoAlreadyExists, + _NotAnEcho, + _EchoLimitReached, + _UnsupportedProtocolRevision, +} + +impl Display for ErrorMessage { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + let message = match self { + ErrorMessage::UnknownCommand => "Unknown command", + ErrorMessage::_IncorrectNumberOfArguments => "Incorrect number of arguments", + ErrorMessage::_OutOfRadarScope => "Out of radar scope", + ErrorMessage::_TypeMismatch => "Type mismatch", + ErrorMessage::_IllegalValue => "Illegal value", + ErrorMessage::_InternalError => "Internal error", + ErrorMessage::_NotATrack => "Not a track", + ErrorMessage::_NaazAlreadyExists => "NAAZ already exists", + ErrorMessage::_NotANaaz => "Not a NAAZ", + ErrorMessage::_NtzAlreadyExists => "NTZ already exists", + ErrorMessage::_NotANtz => "Not a NTZ", + ErrorMessage::_PolygonLimitReached => "Polygon limit reached", + ErrorMessage::_AtonAlreadyExists => "AtoN already exists", + ErrorMessage::_NotAnAton => "Not an AtoN", + ErrorMessage::_AtonLimitReached => "AtoN limit reached", + ErrorMessage::_EchoAlreadyExists => "Echo already exists", + ErrorMessage::_NotAnEcho => "Not an Echo", + ErrorMessage::_EchoLimitReached => "Echo limit reached", + ErrorMessage::_UnsupportedProtocolRevision => "Unsupported protocol revision", + }; + write!(f, "{}", message) + } +} diff --git a/antares/src/radar/protocol/constants/interface_ports.rs b/antares/src/radar/protocol/constants/interface_ports.rs new file mode 100644 index 0000000..f503b84 --- /dev/null +++ b/antares/src/radar/protocol/constants/interface_ports.rs @@ -0,0 +1,2 @@ +pub const TCI_PORT: u16 = 17394; +pub const TDI_PORT: u16 = 17396; diff --git a/antares/src/radar/protocol/constants/mod.rs b/antares/src/radar/protocol/constants/mod.rs new file mode 100644 index 0000000..b6525eb --- /dev/null +++ b/antares/src/radar/protocol/constants/mod.rs @@ -0,0 +1,15 @@ +//! # Constants for the radar protocol. +//! +//! This module contains the constants for the radar protocol. The constants are used to define the +//! different types of commands that can be sent between the client and the server. The constants +//! are defined as enums, with each enum variant representing a different command type. + +mod client_command; +mod error_message; +mod interface_ports; +mod server_command; + +pub use client_command::ClientCommand; +pub use error_message::ErrorMessage; +pub use interface_ports::{TCI_PORT, TDI_PORT}; +pub use server_command::ServerCommand; diff --git a/antares/src/radar/protocol/constants/server_command.rs b/antares/src/radar/protocol/constants/server_command.rs new file mode 100644 index 0000000..e5c756c --- /dev/null +++ b/antares/src/radar/protocol/constants/server_command.rs @@ -0,0 +1,63 @@ +use std::fmt::{Display, Formatter, Result}; + +/// Commands that the server can send to the client +pub enum ServerCommand { + CurrVal, + MsgErr, + Pong, + Bye, + NaazCreated, + NaazDeleted, + NtzCreated, + NtzDeleted, + AtonCreated, + AtonDeleted, + EchoCreated, + EchoDeleted, + Track, + Corr, +} + +impl ServerCommand { + pub fn from_str(command: &str) -> Option { + match command.to_ascii_lowercase().as_str() { + "currval" => Some(ServerCommand::CurrVal), + "msgerr" => Some(ServerCommand::MsgErr), + "pong" => Some(ServerCommand::Pong), + "bye" => Some(ServerCommand::Bye), + "naazcreated" => Some(ServerCommand::NaazCreated), + "naazdeleted" => Some(ServerCommand::NaazDeleted), + "ntzcreated" => Some(ServerCommand::NtzCreated), + "ntzdeleted" => Some(ServerCommand::NtzDeleted), + "atoncreated" => Some(ServerCommand::AtonCreated), + "atondeleted" => Some(ServerCommand::AtonDeleted), + "echocreated" => Some(ServerCommand::EchoCreated), + "echodeleted" => Some(ServerCommand::EchoDeleted), + "track" => Some(ServerCommand::Track), + "corr" => Some(ServerCommand::Corr), + _ => None, + } + } +} + +impl Display for ServerCommand { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + let message = match self { + ServerCommand::CurrVal => "currval", + ServerCommand::MsgErr => "msgerr", + ServerCommand::Pong => "pong", + ServerCommand::Bye => "bye", + ServerCommand::NaazCreated => "naazcreated", + ServerCommand::NaazDeleted => "naazdeleted", + ServerCommand::NtzCreated => "ntzcreated", + ServerCommand::NtzDeleted => "ntzdeleted", + ServerCommand::AtonCreated => "atoncreated", + ServerCommand::AtonDeleted => "atondeleted", + ServerCommand::EchoCreated => "echocreated", + ServerCommand::EchoDeleted => "echodeleted", + ServerCommand::Track => "track", + ServerCommand::Corr => "corr", + }; + write!(f, "{}", message) + } +} diff --git a/antares/src/radar/protocol/mod.rs b/antares/src/radar/protocol/mod.rs new file mode 100644 index 0000000..455f432 --- /dev/null +++ b/antares/src/radar/protocol/mod.rs @@ -0,0 +1,5 @@ +mod constants; +mod tcp_interfaces; + +use super::Track; +pub use tcp_interfaces::{TrackControlInterface, TrackDataInterface}; diff --git a/antares/src/radar/protocol/tcp_interfaces/base_track_interface.rs b/antares/src/radar/protocol/tcp_interfaces/base_track_interface.rs new file mode 100644 index 0000000..96bdd9f --- /dev/null +++ b/antares/src/radar/protocol/tcp_interfaces/base_track_interface.rs @@ -0,0 +1,150 @@ +use super::constants::ClientCommand; +use super::constants::ErrorMessage; +use super::constants::ServerCommand; +use crate::utils::{escape_text, unescape_text, ThreadPool}; +use std::io::{prelude::*, BufReader}; +use std::net::{TcpListener, TcpStream}; +use std::sync::{Arc, Mutex}; +use std::thread; + +type Clients = Arc>>; + +/// The server struct +/// This struct contains the server configuration and the list of connected clients +pub struct Server { + pub host: String, + pub port: u16, + pub num_workers: usize, + pub clients: Clients, +} + +impl Server { + /// Create a new server. + /// + /// # Arguments + /// - `host`: The host address to bind the server to + /// - `port`: The port to bind the server to + /// - `num_workers`: The number of worker threads to use + pub fn new(host: String, port: u16, num_workers: usize) -> Self { + Server { + host, + port, + num_workers, + clients: Arc::new(Mutex::new(Vec::with_capacity(num_workers))), + } + } +} + +/// Base interface for the track server +/// This trait defines the basic functionality that a track server should implement +pub trait BaseTrackInterface { + /// Start the server + /// This function should bind to the given host and port and listen for incoming connections + /// For each incoming connection, a new thread should be spawned to handle the client + fn start(server: &Server) { + let ip_addr = server.host.to_string() + ":" + &server.port.to_string(); + println!("Initializing server on {}", ip_addr); + + let listener = TcpListener::bind(ip_addr).unwrap(); + let pool = ThreadPool::new(server.num_workers); + let clients = Arc::clone(&server.clients); + + thread::spawn(move || { + for stream in listener.incoming() { + match stream { + Ok(mut stream) => { + let clients = Arc::clone(&clients); + pool.execute(move || { + Self::handle_client(&mut stream, clients); + }); + } + Err(_) => eprintln!("Failed to accept connection"), + } + } + }); + } + + /// Handle a client connection + /// This function should read requests from the client and send responses back + /// The function should continue reading requests until the client sends a "bye" command or disconnects + fn handle_client(stream: &mut TcpStream, clients: Clients) { + let peer_addr = stream.peer_addr().unwrap(); + println!("New client connected: {}", peer_addr); + + { + // Add the client to the list of clients in a thread-safe way + let mut clients = clients.lock().unwrap(); + clients.push(stream.try_clone().unwrap()); + } + + let reader = BufReader::new(stream.try_clone().unwrap()); + for request in reader.lines() { + match request { + Ok(request) => { + println!("Received request from client {}: {:?}", peer_addr, request); + let operation_index = request.find(',').unwrap_or(request.len()); + let (operation_str, args) = request.split_at(operation_index); + let operation = ClientCommand::from_str(operation_str); + let args = args.split(',').map(|s| unescape_text(s.trim())).collect(); + let result = match operation { + None => Err(ErrorMessage::UnknownCommand), + Some(ClientCommand::Bye) => break, + Some(ClientCommand::Ping) => Ok(Self::ping()), + Some(operation) => Self::handle_operation(operation, &args), + } + .unwrap_or_else(|error_message| { + Self::msgerr(error_message, operation_str.to_string(), &args) + }); + if result.len() > 0 { + stream.write_all(result.as_bytes()).unwrap(); + } + } + Err(_) => eprintln!("Failed to read request from client"), + } + } + + { + // Remove the client from the list of clients in a thread-safe way + let mut clients = clients.lock().unwrap(); + clients.retain(|client| client.peer_addr().unwrap() != peer_addr); + println!("Client {} disconnected", peer_addr); + } + } + + /// Handle a client operation + /// This function should handle the given operation and return the response + /// If the operation fails, an error message should be returned + fn handle_operation( + operation: ClientCommand, + args: &Vec, + ) -> Result; + + /// Broadcast a message to all clients + /// This function should send the given message to all clients in the list + /// The message should be sent as a byte array + fn broadcast(clients: &Vec, message: String) { + for mut client in clients { + client.write_all(message.as_bytes()).unwrap(); + } + } + + /// Create a message error response + /// The response should be in the format "msgerr,," + /// The error message and original command should be escaped + fn msgerr(error_message: ErrorMessage, operation: String, args: &Vec) -> String { + let original_command = operation + &args.join(","); + format!( + "{},{},{}", + ServerCommand::MsgErr, + escape_text(&error_message.to_string()), + escape_text(&original_command) + ) + } + + /// Create a ping command response + /// The response should be "pong" + /// The ping command should be ignored + fn ping() -> String { + ServerCommand::Pong.to_string() + } +} diff --git a/antares/src/radar/protocol/tcp_interfaces/mod.rs b/antares/src/radar/protocol/tcp_interfaces/mod.rs new file mode 100644 index 0000000..961bcce --- /dev/null +++ b/antares/src/radar/protocol/tcp_interfaces/mod.rs @@ -0,0 +1,12 @@ +//! This module contains the interfaces for the TCP communication with the radar. +//! The interfaces define the basic functionality that a track server should implement. + +mod base_track_interface; +mod track_control_interface; +mod track_data_interface; + +use super::{constants, Track}; +use base_track_interface::{BaseTrackInterface, Server}; + +pub use track_control_interface::TrackControlInterface; +pub use track_data_interface::TrackDataInterface; diff --git a/antares/src/radar/protocol/tcp_interfaces/track_control_interface.rs b/antares/src/radar/protocol/tcp_interfaces/track_control_interface.rs new file mode 100644 index 0000000..64e19cd --- /dev/null +++ b/antares/src/radar/protocol/tcp_interfaces/track_control_interface.rs @@ -0,0 +1,46 @@ +use super::constants::{ClientCommand, ErrorMessage, TCI_PORT}; +use super::{BaseTrackInterface, Server}; + +/// The track control interface +pub struct TrackControlInterface; + +impl TrackControlInterface { + /// Create and runs a new track control interface + /// + /// # Arguments + /// - `host`: The host address to bind the server to + /// - `num_workers`: The number of worker threads to use + pub fn new(host: String, num_workers: usize) -> Self { + let server = Server::new(host, TCI_PORT, num_workers); + ::start(&server); + TrackControlInterface {} + } +} + +impl BaseTrackInterface for TrackControlInterface { + fn handle_operation( + operation: ClientCommand, + _args: &Vec, + ) -> Result { + match operation { + ClientCommand::Get => todo!(), + ClientCommand::Set => todo!(), + ClientCommand::TrackCreate => todo!(), + ClientCommand::TrackDelete => todo!(), + ClientCommand::TrackSwap => todo!(), + ClientCommand::TrackSelect => todo!(), + ClientCommand::TrackMove => todo!(), + ClientCommand::NaazCreate => todo!(), + ClientCommand::NaazDelete => todo!(), + ClientCommand::NtzCreate => todo!(), + ClientCommand::NtzDelete => todo!(), + ClientCommand::AtonCreate => todo!(), + ClientCommand::AtonDelete => todo!(), + ClientCommand::EchoCreate => todo!(), + ClientCommand::EchoDelete => todo!(), + // The following commands are already handled in the base interface + ClientCommand::Ping => panic!("Ping should be handled in the base interface"), + ClientCommand::Bye => panic!("Bye should be handled in the base interface"), + } + } +} diff --git a/antares/src/radar/protocol/tcp_interfaces/track_data_interface.rs b/antares/src/radar/protocol/tcp_interfaces/track_data_interface.rs new file mode 100644 index 0000000..ce6b822 --- /dev/null +++ b/antares/src/radar/protocol/tcp_interfaces/track_data_interface.rs @@ -0,0 +1,54 @@ +use super::constants::{ClientCommand, ErrorMessage, TDI_PORT}; +use super::{BaseTrackInterface, Server, Track}; + +/// The track data interface +pub struct TrackDataInterface { + server: Server, +} + +impl TrackDataInterface { + /// Creates and runs a new track data interface + /// + /// # Arguments + /// - `host`: The host address to bind the server to + /// - `num_workers`: The number of worker threads to use + pub fn new(host: String, num_workers: usize) -> Self { + let server = Server::new(host, TDI_PORT, num_workers); + ::start(&server); + TrackDataInterface { server } + } + + /// Broadcast a message to all clients + pub fn broadcast(&self, track: Track) { + let clients = self.server.clients.lock().unwrap(); + ::broadcast(&clients, track.serialize()); + } +} + +impl BaseTrackInterface for TrackDataInterface { + fn handle_operation( + operation: ClientCommand, + _args: &Vec, + ) -> Result { + match operation { + ClientCommand::Get => todo!(), + ClientCommand::Set => todo!(), + ClientCommand::TrackCreate => todo!(), + ClientCommand::TrackDelete => todo!(), + ClientCommand::TrackSwap => todo!(), + ClientCommand::TrackSelect => todo!(), + ClientCommand::TrackMove => todo!(), + ClientCommand::NaazCreate => todo!(), + ClientCommand::NaazDelete => todo!(), + ClientCommand::NtzCreate => todo!(), + ClientCommand::NtzDelete => todo!(), + ClientCommand::AtonCreate => todo!(), + ClientCommand::AtonDelete => todo!(), + ClientCommand::EchoCreate => todo!(), + ClientCommand::EchoDelete => todo!(), + // The following commands are already handled in the base interface + ClientCommand::Ping => panic!("Ping should be handled in the base interface"), + ClientCommand::Bye => panic!("Bye should be handled in the base interface"), + } + } +} diff --git a/antares/src/radar/radar.rs b/antares/src/radar/radar.rs new file mode 100644 index 0000000..c347944 --- /dev/null +++ b/antares/src/radar/radar.rs @@ -0,0 +1,36 @@ +use super::{Detector, RadarConfig, TrackControlInterface, TrackDataInterface, Tracker, Wave}; +use std::sync::mpsc; +use std::thread; + +pub struct Radar { + config: RadarConfig, +} + +impl Radar { + pub fn new(config: RadarConfig) -> Radar { + Radar { config } + } + + pub fn start(&self, wave_receiver: mpsc::Receiver) { + let (plot_sender, plot_receiver) = mpsc::channel(); + let (track_sender, track_receiver) = mpsc::channel(); + + let detector = Detector::new(self.config.detector.clone()); + detector.start(wave_receiver, plot_sender); + Tracker::start(plot_receiver, track_sender); + TrackControlInterface::new( + self.config.protocol.host.clone(), + self.config.protocol.num_workers_tci, + ); + let tdi = TrackDataInterface::new( + self.config.protocol.host.clone(), + self.config.protocol.num_workers_tdi, + ); + + thread::spawn(move || { + while let Ok(track) = track_receiver.recv() { + tdi.broadcast(track); + } + }); + } +} diff --git a/antares/src/radar/tracker/mod.rs b/antares/src/radar/tracker/mod.rs new file mode 100644 index 0000000..4be4e43 --- /dev/null +++ b/antares/src/radar/tracker/mod.rs @@ -0,0 +1,6 @@ +mod track; +mod tracker; + +use super::Plot; +pub use track::Track; +pub use tracker::Tracker; diff --git a/antares/src/radar/tracker/track.rs b/antares/src/radar/tracker/track.rs new file mode 100644 index 0000000..b4959ab --- /dev/null +++ b/antares/src/radar/tracker/track.rs @@ -0,0 +1,84 @@ +/// Represents a radar track with relevant information captured by the radar system. +/// +/// # Arguments +/// - `id`: Unique track identifier (0 ≤ id ≤ 9999) +/// - `timestamp`: Date and time of detection (year, month, day, hour, minute, second, millisecond) +/// - `stat`: Track status (CA, CS, FA, FS, LA, LS) +/// - `type_`: Type of object (TARGET, ATON) +/// - `name`: Name of the object if `ATON`, empty if `TARGET` +/// - `linemask`: Proprietary service information (0 ≤ linemask ≤ 63) +/// - `size`: Size or plot area of the track +/// - `range`: Slant range of the track in meters relative to the radar +/// - `azimuth`: Track azimuth in radians (0 ≤ azimuth ≤ 6.28319) +/// - `lat`: Latitude of the track in decimal degrees (-90 ≤ lat ≤ 90) +/// - `long`: Longitude of the track in decimal degrees (-180 ≤ long ≤ 180) +/// - `speed`: Speed in m/s +/// - `course`: Direction of the speed vector in radians (0 ≤ course ≤ 6.28319) +/// - `quality`: Track quality (0 ≤ quality ≤ 30) +/// - `l16quality`: STANAG5516 track quality (0 ≤ l16quality ≤ 15) +/// - `lacks`: Track detection gaps or misses +/// - `winrgw`: Track search window range width in meters +/// - `winazw`: Track search window azimuth width in radians +/// - `stderr`: The tracker’s calculated standard error on the filtered track position in meters +#[derive(Debug)] +pub struct Track { + pub id: u64, + pub year: u32, + pub month: u32, + pub day: u32, + pub hour: u32, + pub minute: u32, + pub second: u32, + pub millisecond: u32, + pub stat: String, + pub type_: String, + pub name: String, + pub linemask: u32, + pub size: u32, + pub range: f64, + pub azimuth: f64, + pub lat: f64, + pub long: f64, + pub speed: f64, + pub course: f64, + pub quality: u32, + pub l16quality: u32, + pub lacks: u32, + pub winrgw: u32, + pub winazw: f64, + pub stderr: f64, +} + +impl Track { + /// Serializes the Track into a CSV-formatted string. + pub fn serialize(&self) -> String { + format!( + "{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}", + self.id, + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.millisecond, + self.stat, + self.type_, + self.name.clone(), + self.linemask, + self.size, + self.range, + self.azimuth, + self.lat, + self.long, + self.speed, + self.course, + self.quality, + self.l16quality, + self.lacks, + self.winrgw, + self.winazw, + self.stderr + ) + } +} diff --git a/antares/src/radar/tracker/tracker.rs b/antares/src/radar/tracker/tracker.rs new file mode 100644 index 0000000..bbeaf48 --- /dev/null +++ b/antares/src/radar/tracker/tracker.rs @@ -0,0 +1,65 @@ +use super::{Plot, Track}; +use chrono::{Datelike, Timelike}; +use std::collections::HashMap; +use std::sync::mpsc; +use std::thread; + +pub struct Tracker; + +impl Tracker { + pub fn start(plot_receiver: mpsc::Receiver, track_sender: mpsc::Sender) { + thread::spawn(move || { + let mut last_plot_by_id = HashMap::new(); + loop { + let plot = plot_receiver.recv().expect("Failed to receive plot"); + let (speed, course) = if let Some(last_plot) = last_plot_by_id.get(&plot.id) { + Tracker::calculate_speed_vector(last_plot, &plot) + } else { + (0.0, 0.0) + }; + + let track = Track { + id: plot.id, + year: plot.timestamp.year() as u32, + month: plot.timestamp.month(), + day: plot.timestamp.day(), + hour: plot.timestamp.hour(), + minute: plot.timestamp.minute(), + second: plot.timestamp.second(), + millisecond: plot.timestamp.timestamp_subsec_millis(), + stat: "CA".to_string(), + type_: "TARGET".to_string(), + name: "".to_string(), + linemask: 0, + size: 0, + range: plot.range, + azimuth: plot.azimuth, + lat: plot.latitude, + long: plot.longitude, + speed, + course, + quality: 0, + l16quality: 0, + lacks: 0, + winrgw: 0, + winazw: 0.0, + stderr: 0.0, + }; + track_sender.send(track).unwrap(); + last_plot_by_id.insert(plot.id, plot); + } + }); + } + + fn calculate_speed_vector(old_plot: &Plot, new_plot: &Plot) -> (f64, f64) { + let time_diff = new_plot.timestamp - old_plot.timestamp; + let delta_x = + new_plot.range * new_plot.azimuth.cos() - old_plot.range * old_plot.azimuth.cos(); + let delta_y = + new_plot.range * new_plot.azimuth.sin() - old_plot.range * old_plot.azimuth.sin(); + let speed = (delta_x.powi(2) + delta_y.powi(2)).sqrt() * 1000.0 + / time_diff.num_milliseconds() as f64; + let course = delta_y.atan2(delta_x); + (speed, course) + } +} diff --git a/antares/src/simulation/config.rs b/antares/src/simulation/config.rs new file mode 100644 index 0000000..e58c00f --- /dev/null +++ b/antares/src/simulation/config.rs @@ -0,0 +1,40 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct SimulationConfig { + pub emission_interval: u64, + pub ships: ShipsConfig, +} + +#[derive(Debug, Deserialize)] +pub struct ShipsConfig { + pub line: Vec, + pub circle: Vec, + pub random: Vec, + pub stationary: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct LineMovementConfig { + pub initial_position: (f64, f64), + pub angle: f64, + pub speed: f64, +} + +#[derive(Debug, Deserialize)] +pub struct CircleMovementConfig { + pub initial_position: (f64, f64), + pub radius: f64, + pub speed: f64, +} + +#[derive(Debug, Deserialize)] +pub struct RandomMovementConfig { + pub initial_position: (f64, f64), + pub max_speed: f64, +} + +#[derive(Debug, Deserialize)] +pub struct StationaryMovementConfig { + pub initial_position: (f64, f64), +} diff --git a/antares/src/simulation/emitters/emitter.rs b/antares/src/simulation/emitters/emitter.rs new file mode 100644 index 0000000..66c34f8 --- /dev/null +++ b/antares/src/simulation/emitters/emitter.rs @@ -0,0 +1,5 @@ +use super::Wave; + +pub trait Emitter { + fn emit(&mut self) -> Wave; +} diff --git a/antares/src/simulation/emitters/mod.rs b/antares/src/simulation/emitters/mod.rs new file mode 100644 index 0000000..43f695c --- /dev/null +++ b/antares/src/simulation/emitters/mod.rs @@ -0,0 +1,8 @@ +mod emitter; +mod ship; + +use super::MovementStrategy; +use super::Wave; + +pub use emitter::Emitter; +pub use ship::Ship; diff --git a/antares/src/simulation/emitters/ship.rs b/antares/src/simulation/emitters/ship.rs new file mode 100644 index 0000000..d979198 --- /dev/null +++ b/antares/src/simulation/emitters/ship.rs @@ -0,0 +1,29 @@ +use super::{Emitter, MovementStrategy, Wave}; + +pub struct Ship { + pub id: u64, + pub position: (f64, f64), + pub emission_interval: u64, + pub movement_strategy: Box, +} + +impl Ship { + pub fn update(&mut self) { + let (x, y) = self.position; + let movement_command = self.movement_strategy.next_movement(); + let emission_interval = self.emission_interval as f64; + let dx = movement_command.speed * movement_command.angle.cos() * emission_interval / 1000.0; + let dy = movement_command.speed * movement_command.angle.sin() * emission_interval / 1000.0; + self.position = (x + dx, y + dy); + } +} + +impl Emitter for Ship { + fn emit(&mut self) -> Wave { + Wave { + id: self.id, + position: self.position, + timestamp: chrono::Utc::now(), + } + } +} diff --git a/antares/src/simulation/environment/mod.rs b/antares/src/simulation/environment/mod.rs new file mode 100644 index 0000000..22e125b --- /dev/null +++ b/antares/src/simulation/environment/mod.rs @@ -0,0 +1,2 @@ +mod wave; +pub use wave::Wave; diff --git a/antares/src/simulation/environment/wave.rs b/antares/src/simulation/environment/wave.rs new file mode 100644 index 0000000..1eb0b6a --- /dev/null +++ b/antares/src/simulation/environment/wave.rs @@ -0,0 +1,11 @@ +/// Represents an electromagnetic (radio) wave in the simulation +/// For now, it only carries the information that the radar uses to detect objects +/// In the future, it could be used to simulate the wave propagation and reflection +use chrono::{DateTime, Utc}; + +#[derive(Debug)] +pub struct Wave { + pub id: u64, + pub position: (f64, f64), + pub timestamp: DateTime, +} diff --git a/antares/src/simulation/mod.rs b/antares/src/simulation/mod.rs new file mode 100644 index 0000000..e0a9fce --- /dev/null +++ b/antares/src/simulation/mod.rs @@ -0,0 +1,19 @@ +//! Simulation module. +//! +//! This module contains the simulation environment, emitters and movement strategies. +//! + +mod config; +mod emitters; +mod environment; +mod movement; +mod simulation; + +use emitters::Emitter; +use movement::{ + CircleMovement, LineMovement, MovementStrategy, RandomMovement, StationaryMovement, +}; + +pub use config::SimulationConfig; +pub use environment::Wave; +pub use simulation::Simulation; diff --git a/antares/src/simulation/movement/circle.rs b/antares/src/simulation/movement/circle.rs new file mode 100644 index 0000000..7da6263 --- /dev/null +++ b/antares/src/simulation/movement/circle.rs @@ -0,0 +1,34 @@ +use super::{MovementCommand, MovementStrategy}; +use std::f64::consts::PI; + +pub struct CircleMovement { + speed: f64, + radius: f64, + time_delta: u64, + current_angle: f64, +} + +impl CircleMovement { + pub fn new(radius: f64, speed: f64, time_delta: u64) -> Self { + CircleMovement { + speed, + radius, + time_delta, + current_angle: 0.0, + } + } +} + +impl MovementStrategy for CircleMovement { + fn next_movement(&mut self) -> MovementCommand { + let time_step = self.time_delta as f64 / 1000.0; + let distance = self.speed * time_step; + let angle_step = distance / self.radius; + self.current_angle = (self.current_angle + angle_step) % (2.0 * PI); + + MovementCommand { + angle: self.current_angle, + speed: self.speed, + } + } +} diff --git a/antares/src/simulation/movement/line.rs b/antares/src/simulation/movement/line.rs new file mode 100644 index 0000000..44087aa --- /dev/null +++ b/antares/src/simulation/movement/line.rs @@ -0,0 +1,21 @@ +use super::{MovementCommand, MovementStrategy}; + +pub struct LineMovement { + angle: f64, + speed: f64, +} + +impl LineMovement { + pub fn new(angle: f64, speed: f64) -> Self { + LineMovement { angle, speed } + } +} + +impl MovementStrategy for LineMovement { + fn next_movement(&mut self) -> MovementCommand { + MovementCommand { + angle: self.angle, + speed: self.speed, + } + } +} diff --git a/antares/src/simulation/movement/mod.rs b/antares/src/simulation/movement/mod.rs new file mode 100644 index 0000000..1663a6d --- /dev/null +++ b/antares/src/simulation/movement/mod.rs @@ -0,0 +1,11 @@ +mod circle; +mod line; +mod random; +mod stationary; +mod strategy; + +pub use circle::CircleMovement; +pub use line::LineMovement; +pub use random::RandomMovement; +pub use stationary::StationaryMovement; +pub use strategy::{MovementCommand, MovementStrategy}; diff --git a/antares/src/simulation/movement/random.rs b/antares/src/simulation/movement/random.rs new file mode 100644 index 0000000..ab866f5 --- /dev/null +++ b/antares/src/simulation/movement/random.rs @@ -0,0 +1,22 @@ +use super::{MovementCommand, MovementStrategy}; +use rand::Rng; +use std::f64::consts::PI; + +pub struct RandomMovement { + max_speed: f64, +} + +impl RandomMovement { + pub fn new(max_speed: f64) -> RandomMovement { + RandomMovement { max_speed } + } +} + +impl MovementStrategy for RandomMovement { + fn next_movement(&mut self) -> MovementCommand { + let mut rng = rand::thread_rng(); + let angle = rng.gen_range(0.0..(2.0 * PI)); + let speed = rng.gen_range(0.0..self.max_speed); + MovementCommand { angle, speed } + } +} diff --git a/antares/src/simulation/movement/stationary.rs b/antares/src/simulation/movement/stationary.rs new file mode 100644 index 0000000..1b24246 --- /dev/null +++ b/antares/src/simulation/movement/stationary.rs @@ -0,0 +1,12 @@ +use super::{MovementCommand, MovementStrategy}; + +pub struct StationaryMovement; + +impl MovementStrategy for StationaryMovement { + fn next_movement(&mut self) -> MovementCommand { + MovementCommand { + angle: 0.0, + speed: 0.0, + } + } +} diff --git a/antares/src/simulation/movement/strategy.rs b/antares/src/simulation/movement/strategy.rs new file mode 100644 index 0000000..6f41447 --- /dev/null +++ b/antares/src/simulation/movement/strategy.rs @@ -0,0 +1,14 @@ +/// # Movement Command +/// A movement command is a struct that contains the angle and speed of a movement. +/// +/// # Parameters +/// - `angle`: The angle of the movement in radians. +/// - `speed`: The speed of the movement in m/s. +pub struct MovementCommand { + pub angle: f64, + pub speed: f64, +} + +pub trait MovementStrategy: Send + Sync { + fn next_movement(&mut self) -> MovementCommand; +} diff --git a/antares/src/simulation/simulation.rs b/antares/src/simulation/simulation.rs new file mode 100644 index 0000000..af666f4 --- /dev/null +++ b/antares/src/simulation/simulation.rs @@ -0,0 +1,92 @@ +use crate::simulation::emitters::Ship; +use std::time::Duration; + +use super::{ + CircleMovement, Emitter, LineMovement, RandomMovement, SimulationConfig, StationaryMovement, + Wave, +}; +use std::sync::mpsc::Sender; +use std::thread; + +pub struct Simulation { + config: SimulationConfig, +} + +impl Simulation { + /// Create a new Simulation. + pub fn new(config: SimulationConfig) -> Simulation { + Simulation { config } + } + + /// Start the simulation. + pub fn start(&self, wave_sender: Sender) { + let emission_interval = self.config.emission_interval; + let mut ships = Vec::new(); + let mut ship_id = 0; + + // Create ships with line movement + for line_ship in &self.config.ships.line { + let movement_strategy = Box::new(LineMovement::new(line_ship.angle, line_ship.speed)); + ships.push(Ship { + id: ship_id, + position: line_ship.initial_position, + emission_interval, + movement_strategy, + }); + ship_id += 1; + } + + // Create ships with circle movement + for circle_ship in &self.config.ships.circle { + let movement_strategy = Box::new(CircleMovement::new( + circle_ship.radius, + circle_ship.speed, + emission_interval, + )); + ships.push(Ship { + id: ship_id, + position: circle_ship.initial_position, + emission_interval, + movement_strategy, + }); + ship_id += 1; + } + + // Create ships with random movement + for random_ship in &self.config.ships.random { + let movement_strategy = Box::new(RandomMovement::new(random_ship.max_speed)); + ships.push(Ship { + id: ship_id, + position: random_ship.initial_position, + emission_interval, + movement_strategy, + }); + ship_id += 1; + } + + // Create ships with stationary movement + for stationary_ship in &self.config.ships.stationary { + let movement_strategy = Box::new(StationaryMovement {}); + ships.push(Ship { + id: ship_id, + position: stationary_ship.initial_position, + emission_interval, + movement_strategy, + }); + ship_id += 1; + } + + thread::spawn(move || loop { + let mut waves = Vec::with_capacity(ships.len()); + for ship in ships.iter_mut() { + let wave = ship.emit(); + waves.push(wave); + ship.update(); + } + for wave in waves { + wave_sender.send(wave).expect("Failed to send wave"); + } + thread::sleep(Duration::from_millis(emission_interval)); + }); + } +} diff --git a/antares/src/utils/escape_ascii.rs b/antares/src/utils/escape_ascii.rs new file mode 100644 index 0000000..7533160 --- /dev/null +++ b/antares/src/utils/escape_ascii.rs @@ -0,0 +1,43 @@ +/// Escape special characters in TCP communication text. +/// +/// # Examples +/// ``` +/// let s = "Hello, world!"; +/// let escaped = escape_text(s); +/// assert_eq!(escaped, "hello\\2c world!"); +/// ``` +pub fn escape_text(s: &str) -> String { + s.to_ascii_lowercase() + .chars() + .map(|c| match c { + ',' => "\\2c".to_string(), + '\\' => "\\5c".to_string(), + c if c as u8 <= 31 || c as u8 >= 128 => format!("\\{:02x}", c as u8), + _ => c.to_string(), + }) + .collect() +} + +/// Unescape special characters in TCP communication text. +/// +/// # Examples +/// ``` +/// let s = "hello\\2c world!"; +/// let unescaped = unescape_text(s); +/// assert_eq!(unescaped, "hello, world!"); +/// ``` +pub fn unescape_text(s: &str) -> String { + let mut result = String::new(); + let mut chars = s.chars(); + while let Some(c) = chars.next() { + if c == '\\' { + let hex = chars.next().unwrap(); + let hex = hex.to_string() + &chars.next().unwrap().to_string(); + let c = u8::from_str_radix(&hex, 16).unwrap_or(b' ') as char; + result.push(c as char); + } else { + result.push(c); + } + } + result.to_ascii_lowercase() +} diff --git a/antares/src/utils/mod.rs b/antares/src/utils/mod.rs new file mode 100644 index 0000000..519bdbb --- /dev/null +++ b/antares/src/utils/mod.rs @@ -0,0 +1,9 @@ +//! # Utility module +//! +//! This module contains utility functions and structures that are used throughout the project. +//! + +mod thread_pool; +pub use thread_pool::ThreadPool; +mod escape_ascii; +pub use escape_ascii::{escape_text, unescape_text}; diff --git a/antares/src/utils/thread_pool.rs b/antares/src/utils/thread_pool.rs new file mode 100644 index 0000000..245b50a --- /dev/null +++ b/antares/src/utils/thread_pool.rs @@ -0,0 +1,93 @@ +use std::sync::{mpsc, Arc, Mutex}; +use std::thread; + +/// A thread pool. +/// +/// The `ThreadPool` struct creates a number of threads and maintains a queue of jobs to be executed. +pub struct ThreadPool { + workers: Vec, + sender: Option>, +} + +type Job = Box; + +impl ThreadPool { + /// Create a new ThreadPool. + /// + /// The size is the number of threads in the pool. + /// + /// # Panics + /// + /// The `new` function will panic if the size is zero. + pub fn new(size: usize) -> ThreadPool { + assert!(size > 0, "ThreadPool size must be greater than zero."); + + let (sender, receiver) = mpsc::channel(); + let receiver = Arc::new(Mutex::new(receiver)); + let mut workers = Vec::with_capacity(size); + + for _ in 0..size { + workers.push(Worker::new(Arc::clone(&receiver))); + } + + ThreadPool { + workers, + sender: Some(sender), + } + } + + /// Execute a function on the thread pool. + /// + /// The function must be `FnOnce`, `Send` and `'static`. + pub fn execute(&self, f: F) + where + F: FnOnce() + Send + 'static, + { + let job = Box::new(f); + self.sender.as_ref().unwrap().send(job).unwrap(); + } +} + +impl Drop for ThreadPool { + /// Drop all threads in the ThreadPool. + /// + /// This method will wait for all threads to finish their work before returning. + fn drop(&mut self) { + drop(self.sender.take()); + + for worker in &mut self.workers { + if let Some(thread) = worker.thread.take() { + thread.join().unwrap(); + } + } + } +} + +/// A worker in the thread pool. +/// +/// The `Worker` struct is responsible for executing jobs on the thread pool. +struct Worker { + thread: Option>, +} + +impl Worker { + /// Create a new Worker. + /// + /// The `new` function creates a new worker that will execute jobs from the receiver. + /// `receiver` is a `mpsc::Receiver` that contains the jobs to be executed. + /// + /// The worker will continue to execute jobs until the receiver is closed. + fn new(receiver: Arc>>) -> Worker { + let thread = thread::spawn(move || loop { + let message = receiver.lock().unwrap().recv(); + match message { + Ok(job) => job(), + Err(_) => break, + } + }); + + Worker { + thread: Some(thread), + } + } +} From e1d8f424c62bf63201b08d343b1d7be0cc56cf75 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Urrea <68788933+jsurrea@users.noreply.github.com> Date: Fri, 11 Apr 2025 01:24:17 -0500 Subject: [PATCH 02/17] scaffold the antares-python project with dev tools --- .github/workflows/python-ci.yml | 56 ++ .github/workflows/python-publish.yml | 42 ++ antares-python/.gitignore | 9 + antares-python/.pre-commit-config.yaml | 18 + antares-python/.python-version | 1 + antares-python/LICENSE | 21 + antares-python/README.md | 68 +++ antares-python/main.py | 6 + antares-python/pyproject.toml | 40 ++ antares-python/src/antares/__init__.py | 3 + antares-python/src/antares/core.py | 2 + antares-python/tests/test_core.py | 5 + naval-radar-simulator/.gitignore | 1 - naval-radar-simulator/Cargo.lock | 486 ------------------ naval-radar-simulator/Cargo.toml | 10 - naval-radar-simulator/README.md | 151 ------ naval-radar-simulator/assets/config.toml | 21 - naval-radar-simulator/src/config.rs | 14 - naval-radar-simulator/src/controller.rs | 28 - naval-radar-simulator/src/lib.rs | 16 - naval-radar-simulator/src/main.rs | 24 - naval-radar-simulator/src/radar/config.rs | 22 - .../src/radar/detector/detector.rs | 71 --- .../src/radar/detector/mod.rs | 6 - .../src/radar/detector/plot.rs | 10 - naval-radar-simulator/src/radar/mod.rs | 18 - .../protocol/constants/client_command.rs | 72 --- .../radar/protocol/constants/error_message.rs | 50 -- .../protocol/constants/interface_ports.rs | 2 - .../src/radar/protocol/constants/mod.rs | 15 - .../protocol/constants/server_command.rs | 63 --- .../src/radar/protocol/mod.rs | 5 - .../tcp_interfaces/base_track_interface.rs | 150 ------ .../src/radar/protocol/tcp_interfaces/mod.rs | 12 - .../tcp_interfaces/track_control_interface.rs | 46 -- .../tcp_interfaces/track_data_interface.rs | 54 -- naval-radar-simulator/src/radar/radar.rs | 36 -- .../src/radar/tracker/mod.rs | 6 - .../src/radar/tracker/track.rs | 84 --- .../src/radar/tracker/tracker.rs | 65 --- .../src/simulation/config.rs | 40 -- .../src/simulation/emitters/emitter.rs | 5 - .../src/simulation/emitters/mod.rs | 8 - .../src/simulation/emitters/ship.rs | 29 -- .../src/simulation/environment/mod.rs | 2 - .../src/simulation/environment/wave.rs | 11 - naval-radar-simulator/src/simulation/mod.rs | 19 - .../src/simulation/movement/circle.rs | 34 -- .../src/simulation/movement/line.rs | 21 - .../src/simulation/movement/mod.rs | 11 - .../src/simulation/movement/random.rs | 22 - .../src/simulation/movement/stationary.rs | 12 - .../src/simulation/movement/strategy.rs | 14 - .../src/simulation/simulation.rs | 92 ---- .../src/utils/escape_ascii.rs | 43 -- naval-radar-simulator/src/utils/mod.rs | 9 - .../src/utils/thread_pool.rs | 93 ---- 57 files changed, 271 insertions(+), 2003 deletions(-) create mode 100644 .github/workflows/python-ci.yml create mode 100644 .github/workflows/python-publish.yml create mode 100644 antares-python/.gitignore create mode 100644 antares-python/.pre-commit-config.yaml create mode 100644 antares-python/.python-version create mode 100644 antares-python/LICENSE create mode 100644 antares-python/README.md create mode 100644 antares-python/main.py create mode 100644 antares-python/pyproject.toml create mode 100644 antares-python/src/antares/__init__.py create mode 100644 antares-python/src/antares/core.py create mode 100644 antares-python/tests/test_core.py delete mode 100644 naval-radar-simulator/.gitignore delete mode 100644 naval-radar-simulator/Cargo.lock delete mode 100644 naval-radar-simulator/Cargo.toml delete mode 100644 naval-radar-simulator/README.md delete mode 100644 naval-radar-simulator/assets/config.toml delete mode 100644 naval-radar-simulator/src/config.rs delete mode 100644 naval-radar-simulator/src/controller.rs delete mode 100644 naval-radar-simulator/src/lib.rs delete mode 100644 naval-radar-simulator/src/main.rs delete mode 100644 naval-radar-simulator/src/radar/config.rs delete mode 100644 naval-radar-simulator/src/radar/detector/detector.rs delete mode 100644 naval-radar-simulator/src/radar/detector/mod.rs delete mode 100644 naval-radar-simulator/src/radar/detector/plot.rs delete mode 100644 naval-radar-simulator/src/radar/mod.rs delete mode 100644 naval-radar-simulator/src/radar/protocol/constants/client_command.rs delete mode 100644 naval-radar-simulator/src/radar/protocol/constants/error_message.rs delete mode 100644 naval-radar-simulator/src/radar/protocol/constants/interface_ports.rs delete mode 100644 naval-radar-simulator/src/radar/protocol/constants/mod.rs delete mode 100644 naval-radar-simulator/src/radar/protocol/constants/server_command.rs delete mode 100644 naval-radar-simulator/src/radar/protocol/mod.rs delete mode 100644 naval-radar-simulator/src/radar/protocol/tcp_interfaces/base_track_interface.rs delete mode 100644 naval-radar-simulator/src/radar/protocol/tcp_interfaces/mod.rs delete mode 100644 naval-radar-simulator/src/radar/protocol/tcp_interfaces/track_control_interface.rs delete mode 100644 naval-radar-simulator/src/radar/protocol/tcp_interfaces/track_data_interface.rs delete mode 100644 naval-radar-simulator/src/radar/radar.rs delete mode 100644 naval-radar-simulator/src/radar/tracker/mod.rs delete mode 100644 naval-radar-simulator/src/radar/tracker/track.rs delete mode 100644 naval-radar-simulator/src/radar/tracker/tracker.rs delete mode 100644 naval-radar-simulator/src/simulation/config.rs delete mode 100644 naval-radar-simulator/src/simulation/emitters/emitter.rs delete mode 100644 naval-radar-simulator/src/simulation/emitters/mod.rs delete mode 100644 naval-radar-simulator/src/simulation/emitters/ship.rs delete mode 100644 naval-radar-simulator/src/simulation/environment/mod.rs delete mode 100644 naval-radar-simulator/src/simulation/environment/wave.rs delete mode 100644 naval-radar-simulator/src/simulation/mod.rs delete mode 100644 naval-radar-simulator/src/simulation/movement/circle.rs delete mode 100644 naval-radar-simulator/src/simulation/movement/line.rs delete mode 100644 naval-radar-simulator/src/simulation/movement/mod.rs delete mode 100644 naval-radar-simulator/src/simulation/movement/random.rs delete mode 100644 naval-radar-simulator/src/simulation/movement/stationary.rs delete mode 100644 naval-radar-simulator/src/simulation/movement/strategy.rs delete mode 100644 naval-radar-simulator/src/simulation/simulation.rs delete mode 100644 naval-radar-simulator/src/utils/escape_ascii.rs delete mode 100644 naval-radar-simulator/src/utils/mod.rs delete mode 100644 naval-radar-simulator/src/utils/thread_pool.rs diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000..f23a595 --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,56 @@ +name: Antares Python CI + +on: + push: + paths: + - 'antares-python/**' + pull_request: + paths: + - 'antares-python/**' + +jobs: + test: + name: Run Tests, Lint, Type-check + runs-on: ubuntu-latest + + defaults: + run: + working-directory: antares-python + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install uv + run: | + curl -Ls https://astral.sh/uv/install.sh | bash + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Create and activate venv + run: | + uv venv + source .venv/bin/activate + uv pip install -U uv + + - name: Install dependencies + run: | + uv pip install -r requirements.txt || true + uv pip install -e . + uv pip install pytest mypy ruff + + - name: Run linters + run: ruff check . + + - name: Run formatters + run: ruff format --check . + + - name: Run mypy + run: mypy src/ + + - name: Run tests with coverage + run: pytest --cov=src --cov-report=term-missing --cov-fail-under=80 + diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000..68c6d1c --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,42 @@ +name: Publish antares-python to PyPI + +on: + push: + tags: + - "v*.*.*" + +jobs: + publish: + name: Build and Publish + runs-on: ubuntu-latest + + defaults: + run: + working-directory: antares-python + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install uv + run: | + curl -Ls https://astral.sh/uv/install.sh | bash + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Build package + run: | + uv venv + source .venv/bin/activate + uv pip install -U build + python -m build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} + packages-dir: antares-python/dist/ diff --git a/antares-python/.gitignore b/antares-python/.gitignore new file mode 100644 index 0000000..573d4be --- /dev/null +++ b/antares-python/.gitignore @@ -0,0 +1,9 @@ +__pycache__/ +*.pyc +.venv/ +dist/ +build/ +*.egg-info/ +.coverage +.coverage.* +htmlcov/ diff --git a/antares-python/.pre-commit-config.yaml b/antares-python/.pre-commit-config.yaml new file mode 100644 index 0000000..59399b4 --- /dev/null +++ b/antares-python/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.5 + hooks: + - id: ruff-format + - id: ruff + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.15.0 + hooks: + - id: mypy + - repo: local + hooks: + - id: check-coverage + name: Check minimum coverage + entry: python antares-python/scripts/check_coverage.py + language: system + types: [python] + diff --git a/antares-python/.python-version b/antares-python/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/antares-python/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/antares-python/LICENSE b/antares-python/LICENSE new file mode 100644 index 0000000..23cda02 --- /dev/null +++ b/antares-python/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 The Software Design Lab + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/antares-python/README.md b/antares-python/README.md new file mode 100644 index 0000000..e346cc6 --- /dev/null +++ b/antares-python/README.md @@ -0,0 +1,68 @@ +# antares-python + +[![CI](https://github.com/ANTARES/antares-python/actions/workflows/python-ci.yml/badge.svg)](https://github.com/ANTARES/antares-python/actions/workflows/python-ci.yml) +[![codecov](https://img.shields.io/badge/coverage-80%25-brightgreen)](https://github.com/ANTARES/antares-python) +[![PyPI version](https://img.shields.io/pypi/v/antares-python.svg)](https://pypi.org/project/antares-python/) +[![Python version](https://img.shields.io/pypi/pyversions/antares-python)](https://pypi.org/project/antares-python/) +[![License](https://img.shields.io/github/license/ANTARES/antares-python)](LICENSE) + +> Python interface for the [Antares](https://github.com/ANTARES/antares) simulation software + +`antares-python` is a facade library that allows Python developers to interact with the Antares simulation engine via HTTP. It provides a clean, user-friendly API for submitting simulations, retrieving results, and managing scenarios — similar to how `pyspark` interfaces with Apache Spark. + +--- + +## 🚀 Features + +- 🔁 Async + sync HTTP client +- 🔒 Typed schema validation (coming soon) +- 📦 Built-in support for data serialization +- 🧪 Fully testable with mocks +- 🛠️ First-class CLI support (planned) + +--- + +## 📦 Installation + +```bash +pip install antares-python +``` + +--- + +## ⚡ Quickstart + +```python +from antares import AntaresClient + +client = AntaresClient(base_url="http://localhost:8000") + +# Submit a simulation +result = client.run_simulation(config={...}) +print(result.metrics) +``` + +--- + +## 📚 Documentation + +_Work in progress — full API docs coming soon._ + +--- + +## 🧪 Development + +To set up a local development environment: + +```bash +uv venv +source .venv/bin/activate +uv pip install -e .[dev] +task check +``` + +--- + +## 🧾 License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. diff --git a/antares-python/main.py b/antares-python/main.py new file mode 100644 index 0000000..1ac9e4c --- /dev/null +++ b/antares-python/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from antares-python!") + + +if __name__ == "__main__": + main() diff --git a/antares-python/pyproject.toml b/antares-python/pyproject.toml new file mode 100644 index 0000000..2a4660f --- /dev/null +++ b/antares-python/pyproject.toml @@ -0,0 +1,40 @@ +[project] +name = "antares-python" +version = "0.1.0" +description = "Python interface for the Antares simulation software" +authors = [{ name = "Juan Sebastian Urrea-Lopez", email = "js.urrea@uniandes.edu.co" }] +readme = "README.md" +requires-python = ">=3.13" +dependencies = [] + +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.ruff] +line-length = 88 +lint.select = ["E", "F", "I", "UP", "B", "PL"] +exclude = ["dist", "build"] + +[tool.mypy] +strict = true +python_version = "3.13" +files = ["src"] + +[tool.pytest.ini_options] +pythonpath = "src" +addopts = "-ra -q -ra -q --cov=src --cov-report=term-missing" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.taskipy.tasks] +lint = "ruff check . --fix" +format = "ruff format ." +typecheck = "mypy src/" +test = "pytest" +coverage = "pytest --cov=src --cov-report=term-missing" +build = "python -m build" +publish = "twine upload dist/* --repository antares-python" +check = "task lint && task typecheck && task test" +release = "task check && task build && task publish" diff --git a/antares-python/src/antares/__init__.py b/antares-python/src/antares/__init__.py new file mode 100644 index 0000000..f30e812 --- /dev/null +++ b/antares-python/src/antares/__init__.py @@ -0,0 +1,3 @@ +from .core import saludar + +__all__ = ["saludar"] diff --git a/antares-python/src/antares/core.py b/antares-python/src/antares/core.py new file mode 100644 index 0000000..59c3fd2 --- /dev/null +++ b/antares-python/src/antares/core.py @@ -0,0 +1,2 @@ +def saludar(nombre: str) -> str: + return f"Hola, {nombre}!" diff --git a/antares-python/tests/test_core.py b/antares-python/tests/test_core.py new file mode 100644 index 0000000..582ca21 --- /dev/null +++ b/antares-python/tests/test_core.py @@ -0,0 +1,5 @@ +from antares import saludar + + +def test_saludar(): + assert saludar("Luna") == "Hola, Luna!" diff --git a/naval-radar-simulator/.gitignore b/naval-radar-simulator/.gitignore deleted file mode 100644 index ea8c4bf..0000000 --- a/naval-radar-simulator/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/naval-radar-simulator/Cargo.lock b/naval-radar-simulator/Cargo.lock deleted file mode 100644 index f1d4c3c..0000000 --- a/naval-radar-simulator/Cargo.lock +++ /dev/null @@ -1,486 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "bumpalo" -version = "3.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "cc" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-targets", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "hashbrown" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" - -[[package]] -name = "iana-time-zone" -version = "0.1.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "indexmap" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "js-sys" -version = "0.3.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.158" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" - -[[package]] -name = "log" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "naval-radar-simulator" -version = "0.1.0" -dependencies = [ - "chrono", - "rand", - "serde", - "toml", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" - -[[package]] -name = "ppv-lite86" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "serde" -version = "1.0.214" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.214" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_spanned" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" -dependencies = [ - "serde", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "syn" -version = "2.0.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "toml" -version = "0.8.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - -[[package]] -name = "unicode-ident" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" -dependencies = [ - "cfg-if", - "once_cell", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winnow" -version = "0.6.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" -dependencies = [ - "memchr", -] - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/naval-radar-simulator/Cargo.toml b/naval-radar-simulator/Cargo.toml deleted file mode 100644 index 7f330ce..0000000 --- a/naval-radar-simulator/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "naval-radar-simulator" -version = "0.1.0" -edition = "2021" - -[dependencies] -chrono = "0.4.38" -rand = "0.8.5" -serde = { version = "1.0.214", features = ["derive"] } -toml = "0.8.19" diff --git a/naval-radar-simulator/README.md b/naval-radar-simulator/README.md deleted file mode 100644 index 13e6b0d..0000000 --- a/naval-radar-simulator/README.md +++ /dev/null @@ -1,151 +0,0 @@ -# Naval Radar - Simulator - -The Naval Radar Simulator is a robust project designed to simulate radar systems, generate radar data, and emulate real-world scenarios involving radar detection and tracking. - -## **Features** - -- **Configurable Radar Simulation**: - - Define radar parameters like range, resolution, and target detection. -- **Dynamic Simulation**: - - Simulates target movements (linear, random, circular, stationary) and environmental effects. -- **Communication Protocol**: - - Implements TCP interfaces for communication between radar components. -- **Realistic Tracking**: - - Includes tracking algorithms for managing targets. - -## **Setup Instructions** - -1. **Install Rust** - Make sure you have Rust and cargo installed. If not, you can install them following the instructions on the [Rust website](https://www.rust-lang.org/tools/install). - -2. **Go to the Project Directory** - - ```bash - cd naval-radar-simulator - ``` - -3. **Build the Project** - There are 2 ways to build the project: - - - **Development Build**: - ```bash - cargo build - ``` - - **Release Build**: - ```bash - cargo build --release - ``` - - It is recommended to use the release build for better performance. Use the development build for debugging and testing. - -4. **Run the Simulator** - Run the simulator with a configuration file: - - For the release build: - - ```bash - ./target/release/naval-radar-simulator - ``` - - For the development build: - - ```bash - cargo run -- - ``` - - Replace `` with the path to your TOML configuration file. - -## **Configuration File** - -The simulator uses a TOML configuration file to define settings such as radar range, simulation parameters, and environment details. A sample configuration file might look like this: - -```toml -[radar] - -[radar.protocol] -host = "0.0.0.0" -num_workers_tci = 1 -num_workers_tdi = 1 - -[radar.detector] -range = 100.0 -speed = 10.0 -angle = 0.0 -start_coordinates = [4.0, -72.0] - -[simulation] -emission_interval = 20 - -[simulation.ships] -line = [{ initial_position = [-50.0, 50.0], angle = 0.785, speed = 5.0 }] -circle = [{ initial_position = [50.0, -50.0], radius = 20.0, speed = 5.0 }] -random = [{ initial_position = [-50.0, -50.0], max_speed = 20.0 }] -stationary = [{ initial_position = [50.0, 50.0] }] -``` - -## **Directory Structure** - -The directory structure organizes the project for clarity and scalability: - -```plaintext -src -├── config.rs # Manages configuration structures and settings -├── controller.rs # Manages the Controller struct, starting the radar and simulation -├── lib.rs # Main library file with module definitions -├── main.rs # Entry point for the simulator -├── radar/ # Radar simulation logic -│ ├── config.rs # Configuration for radar-specific settings -│ ├── detector/ # Radar detection logic -│ │ ├── detector.rs # Core detection algorithms -│ │ ├── mod.rs # Detector module entry point -│ │ └── plot.rs # Handles radar plot generation -│ ├── mod.rs # Radar module entry point -│ ├── protocol/ # Radar communication protocol -│ │ ├── constants/ # Constants used in protocol definitions -│ │ │ ├── client_command.rs -│ │ │ ├── error_message.rs -│ │ │ ├── interface_ports.rs -│ │ │ ├── mod.rs -│ │ │ └── server_command.rs -│ │ ├── mod.rs # Protocol module entry point -│ │ └── tcp_interfaces/ -│ │ ├── base_track_interface.rs -│ │ ├── mod.rs -│ │ ├── track_control_interface.rs -│ │ └── track_data_interface.rs -│ ├── radar.rs # Core radar logic -│ └── tracker/ # Radar tracking algorithms -│ ├── mod.rs -│ ├── track.rs -│ └── tracker.rs -├── simulation/ # Simulation logic for the radar -│ ├── config.rs # Configuration for the simulation module -│ ├── emitters/ # Handles simulated emitters like ships or targets -│ │ ├── emitter.rs -│ │ ├── mod.rs -│ │ └── ship.rs -│ ├── environment/ # Simulated environmental effects -│ │ ├── mod.rs -│ │ └── wave.rs -│ ├── mod.rs # Simulation module entry point -│ ├── movement/ # Movement strategies for targets -│ │ ├── circle.rs -│ │ ├── line.rs -│ │ ├── mod.rs -│ │ ├── random.rs -│ │ ├── stationary.rs -│ │ └── strategy.rs -│ └── simulation.rs # Core simulation logic -└── utils/ # Utility functions and reusable structures - ├── escape_ascii.rs # ASCII character processing utilities - ├── mod.rs # Utils module entry point - └── thread_pool.rs # Thread pool implementation for concurrency -``` - -## **Other Dependencies** - -- **`chrono`**: Handles date and time functionality. -- **`rand`**: Generates random numbers for simulation randomness. -- **`serde` and `serde_derive`**: Serializes and deserializes data structures. -- **`toml`**: Parses TOML configuration files. -- **`std::thread`**: For multi-threaded processing. diff --git a/naval-radar-simulator/assets/config.toml b/naval-radar-simulator/assets/config.toml deleted file mode 100644 index 8d05dc6..0000000 --- a/naval-radar-simulator/assets/config.toml +++ /dev/null @@ -1,21 +0,0 @@ -[radar] - -[radar.protocol] -host = "0.0.0.0" -num_workers_tci = 1 -num_workers_tdi = 1 - -[radar.detector] -range = 100.0 -speed = 0.0 -angle = 0.0 -start_coordinates = [4.0, -72.0] - -[simulation] -emission_interval = 20 - -[simulation.ships] -line = [{ initial_position = [-50.0, 50.0], angle = 0.785, speed = 5.0 }] -circle = [{ initial_position = [50.0, -50.0], radius = 20.0, speed = 5.0 }] -random = [{ initial_position = [-50.0, -50.0], max_speed = 20.0 }] -stationary = [{ initial_position = [50.0, 50.0] }] diff --git a/naval-radar-simulator/src/config.rs b/naval-radar-simulator/src/config.rs deleted file mode 100644 index 102d97b..0000000 --- a/naval-radar-simulator/src/config.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! # Config module -//! -//! This module contains the configuration struct for the simulation and radar. -//! The configuration is loaded from a TOML file. -//! - -use super::{RadarConfig, SimulationConfig}; -use serde::Deserialize; - -#[derive(Debug, Deserialize)] -pub struct Config { - pub simulation: SimulationConfig, - pub radar: RadarConfig, -} diff --git a/naval-radar-simulator/src/controller.rs b/naval-radar-simulator/src/controller.rs deleted file mode 100644 index 05b2492..0000000 --- a/naval-radar-simulator/src/controller.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! Controller module -//! -//! This module contains the Controller struct which is responsible for starting the simulation and radar. -//! - -use super::{Config, Radar, Simulation}; -use std::sync::mpsc::channel; - -pub struct Controller { - radar: Radar, - simulation: Simulation, -} - -impl Controller { - pub fn new(config: Config) -> Controller { - Controller { - radar: Radar::new(config.radar), - simulation: Simulation::new(config.simulation), - } - } - - pub fn run(self) { - let (wave_sender, wave_receiver) = channel(); - self.simulation.start(wave_sender); - self.radar.start(wave_receiver); - loop {} - } -} diff --git a/naval-radar-simulator/src/lib.rs b/naval-radar-simulator/src/lib.rs deleted file mode 100644 index aa888dd..0000000 --- a/naval-radar-simulator/src/lib.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! # Radar Simulation Library -//! -//! `radar-simulation` is a library for simulating radar data. -//! - -mod config; -mod controller; -mod radar; -mod simulation; -mod utils; - -use radar::{Radar, RadarConfig}; -use simulation::{Simulation, SimulationConfig, Wave}; - -pub use config::Config; -pub use controller::Controller; diff --git a/naval-radar-simulator/src/main.rs b/naval-radar-simulator/src/main.rs deleted file mode 100644 index dc6c420..0000000 --- a/naval-radar-simulator/src/main.rs +++ /dev/null @@ -1,24 +0,0 @@ -use naval_radar_simulator::{Config, Controller}; -use std::{env, fs, process}; - -fn main() { - let args: Vec = env::args().collect(); - - if args.len() != 2 { - eprintln!("Usage: naval_radar "); - process::exit(1); - } - - let config_content = fs::read_to_string(&args[1]).unwrap_or_else(|err| { - eprintln!("Problem reading the config file: {err}"); - process::exit(1); - }); - - let config: Config = toml::from_str(&config_content).unwrap_or_else(|err| { - eprintln!("Problem parsing the config file: {err}"); - process::exit(1); - }); - - let controller = Controller::new(config); - controller.run(); -} diff --git a/naval-radar-simulator/src/radar/config.rs b/naval-radar-simulator/src/radar/config.rs deleted file mode 100644 index 5a1cadf..0000000 --- a/naval-radar-simulator/src/radar/config.rs +++ /dev/null @@ -1,22 +0,0 @@ -use serde::Deserialize; - -#[derive(Debug, Deserialize)] -pub struct RadarConfig { - pub detector: DetectorConfig, - pub protocol: ProtocolConfig, -} - -#[derive(Debug, Deserialize)] -pub struct ProtocolConfig { - pub host: String, - pub num_workers_tci: usize, - pub num_workers_tdi: usize, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct DetectorConfig { - pub range: f64, - pub speed: f64, - pub angle: f64, - pub start_coordinates: (f64, f64), -} diff --git a/naval-radar-simulator/src/radar/detector/detector.rs b/naval-radar-simulator/src/radar/detector/detector.rs deleted file mode 100644 index afa47c0..0000000 --- a/naval-radar-simulator/src/radar/detector/detector.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crate::radar::config::DetectorConfig; - -use super::{Plot, Wave}; -use chrono::{DateTime, Utc}; -use std::sync::mpsc::{Receiver, Sender}; -use std::thread; - -pub struct Detector { - pub range: f64, - pub speed: f64, - pub angle: f64, - pub start_coordinates: (f64, f64), - pub start_time: DateTime, -} - -impl Detector { - pub fn new(config: DetectorConfig) -> Detector { - Detector { - range: config.range, - speed: config.speed, - angle: config.angle, - start_coordinates: config.start_coordinates, - start_time: chrono::Utc::now(), - } - } - - pub fn start(self, wave_receiver: Receiver, plot_sender: Sender) { - thread::spawn(move || loop { - for wave in wave_receiver.iter() { - let (range, azimuth) = self.calculate_range_azimuth(&wave); - if range > self.range { - continue; - } - - let (latitude, longitude) = self.meters_to_lat_lng(wave.position); - let plot = Plot { - id: wave.id, - range, - azimuth, - timestamp: wave.timestamp, - latitude, - longitude, - }; - plot_sender.send(plot).unwrap(); - } - }); - } - - fn calculate_range_azimuth(&self, wave: &Wave) -> (f64, f64) { - let time_delta = (wave.timestamp - self.start_time).num_milliseconds() as f64 / 1000.0; - let current_position = ( - self.speed * time_delta * self.angle.cos(), - self.speed * time_delta * self.angle.sin(), - ); - let delta_position = ( - wave.position.0 - current_position.0, - wave.position.1 - current_position.1, - ); - let range = (delta_position.0.powi(2) + delta_position.1.powi(2)).sqrt(); - let azimuth = delta_position.1.atan2(delta_position.0); - (range, azimuth) - } - - fn meters_to_lat_lng(&self, position: (f64, f64)) -> (f64, f64) { - let (lat, lng) = self.start_coordinates; - let (dx, dy) = position; - let new_lat = lat + (dy / 111320.0); - let new_lng = lng + (dx / (111320.0 * lat.to_radians().cos())); - (new_lat, new_lng) - } -} diff --git a/naval-radar-simulator/src/radar/detector/mod.rs b/naval-radar-simulator/src/radar/detector/mod.rs deleted file mode 100644 index bfd1c22..0000000 --- a/naval-radar-simulator/src/radar/detector/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod detector; -mod plot; - -use super::Wave; -pub use detector::Detector; -pub use plot::Plot; diff --git a/naval-radar-simulator/src/radar/detector/plot.rs b/naval-radar-simulator/src/radar/detector/plot.rs deleted file mode 100644 index d761ac0..0000000 --- a/naval-radar-simulator/src/radar/detector/plot.rs +++ /dev/null @@ -1,10 +0,0 @@ -use chrono::{DateTime, Utc}; - -pub struct Plot { - pub id: u64, - pub range: f64, - pub azimuth: f64, - pub latitude: f64, - pub longitude: f64, - pub timestamp: DateTime, -} diff --git a/naval-radar-simulator/src/radar/mod.rs b/naval-radar-simulator/src/radar/mod.rs deleted file mode 100644 index 8650297..0000000 --- a/naval-radar-simulator/src/radar/mod.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! # Radar module -//! -//! This module contains the radar detector, tracker and communication protocol. -//! - -mod config; -mod detector; -mod protocol; -mod radar; -mod tracker; - -use super::Wave; -use detector::{Detector, Plot}; -use protocol::{TrackControlInterface, TrackDataInterface}; -use tracker::{Track, Tracker}; - -pub use config::RadarConfig; -pub use radar::Radar; diff --git a/naval-radar-simulator/src/radar/protocol/constants/client_command.rs b/naval-radar-simulator/src/radar/protocol/constants/client_command.rs deleted file mode 100644 index 89d11df..0000000 --- a/naval-radar-simulator/src/radar/protocol/constants/client_command.rs +++ /dev/null @@ -1,72 +0,0 @@ -use std::fmt::{Display, Formatter, Result}; - -/// Commands that the client can send to the server -pub enum ClientCommand { - Get, - Ping, - Bye, - Set, - TrackCreate, - TrackDelete, - TrackSwap, - TrackSelect, - TrackMove, - NaazCreate, - NaazDelete, - NtzCreate, - NtzDelete, - AtonCreate, - AtonDelete, - EchoCreate, - EchoDelete, -} - -impl ClientCommand { - pub fn from_str(command: &str) -> Option { - match command.trim().to_ascii_lowercase().as_str() { - "get" => Some(ClientCommand::Get), - "ping" => Some(ClientCommand::Ping), - "bye" => Some(ClientCommand::Bye), - "set" => Some(ClientCommand::Set), - "trackcreate" => Some(ClientCommand::TrackCreate), - "trackdelete" => Some(ClientCommand::TrackDelete), - "trackswap" => Some(ClientCommand::TrackSwap), - "trackselect" => Some(ClientCommand::TrackSelect), - "trackmove" => Some(ClientCommand::TrackMove), - "naazcreate" => Some(ClientCommand::NaazCreate), - "naazdelete" => Some(ClientCommand::NaazDelete), - "ntzcreate" => Some(ClientCommand::NtzCreate), - "ntzdelete" => Some(ClientCommand::NtzDelete), - "atoncreate" => Some(ClientCommand::AtonCreate), - "atondelete" => Some(ClientCommand::AtonDelete), - "echocreate" => Some(ClientCommand::EchoCreate), - "echodelete" => Some(ClientCommand::EchoDelete), - _ => None, - } - } -} - -impl Display for ClientCommand { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - let message = match self { - ClientCommand::Get => "get", - ClientCommand::Ping => "ping", - ClientCommand::Bye => "bye", - ClientCommand::Set => "set", - ClientCommand::TrackCreate => "trackcreate", - ClientCommand::TrackDelete => "trackdelete", - ClientCommand::TrackSwap => "trackswap", - ClientCommand::TrackSelect => "trackselect", - ClientCommand::TrackMove => "trackmove", - ClientCommand::NaazCreate => "naazcreate", - ClientCommand::NaazDelete => "naazdelete", - ClientCommand::NtzCreate => "ntzcreate", - ClientCommand::NtzDelete => "ntzdelete", - ClientCommand::AtonCreate => "atoncreate", - ClientCommand::AtonDelete => "atondelete", - ClientCommand::EchoCreate => "echocreate", - ClientCommand::EchoDelete => "echodelete", - }; - write!(f, "{}", message) - } -} diff --git a/naval-radar-simulator/src/radar/protocol/constants/error_message.rs b/naval-radar-simulator/src/radar/protocol/constants/error_message.rs deleted file mode 100644 index b36a714..0000000 --- a/naval-radar-simulator/src/radar/protocol/constants/error_message.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::fmt::{Display, Formatter, Result}; - -pub enum ErrorMessage { - UnknownCommand, - _IncorrectNumberOfArguments, - _OutOfRadarScope, - _TypeMismatch, - _IllegalValue, - _InternalError, - _NotATrack, - _NaazAlreadyExists, - _NotANaaz, - _NtzAlreadyExists, - _NotANtz, - _PolygonLimitReached, - _AtonAlreadyExists, - _NotAnAton, - _AtonLimitReached, - _EchoAlreadyExists, - _NotAnEcho, - _EchoLimitReached, - _UnsupportedProtocolRevision, -} - -impl Display for ErrorMessage { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - let message = match self { - ErrorMessage::UnknownCommand => "Unknown command", - ErrorMessage::_IncorrectNumberOfArguments => "Incorrect number of arguments", - ErrorMessage::_OutOfRadarScope => "Out of radar scope", - ErrorMessage::_TypeMismatch => "Type mismatch", - ErrorMessage::_IllegalValue => "Illegal value", - ErrorMessage::_InternalError => "Internal error", - ErrorMessage::_NotATrack => "Not a track", - ErrorMessage::_NaazAlreadyExists => "NAAZ already exists", - ErrorMessage::_NotANaaz => "Not a NAAZ", - ErrorMessage::_NtzAlreadyExists => "NTZ already exists", - ErrorMessage::_NotANtz => "Not a NTZ", - ErrorMessage::_PolygonLimitReached => "Polygon limit reached", - ErrorMessage::_AtonAlreadyExists => "AtoN already exists", - ErrorMessage::_NotAnAton => "Not an AtoN", - ErrorMessage::_AtonLimitReached => "AtoN limit reached", - ErrorMessage::_EchoAlreadyExists => "Echo already exists", - ErrorMessage::_NotAnEcho => "Not an Echo", - ErrorMessage::_EchoLimitReached => "Echo limit reached", - ErrorMessage::_UnsupportedProtocolRevision => "Unsupported protocol revision", - }; - write!(f, "{}", message) - } -} diff --git a/naval-radar-simulator/src/radar/protocol/constants/interface_ports.rs b/naval-radar-simulator/src/radar/protocol/constants/interface_ports.rs deleted file mode 100644 index f503b84..0000000 --- a/naval-radar-simulator/src/radar/protocol/constants/interface_ports.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub const TCI_PORT: u16 = 17394; -pub const TDI_PORT: u16 = 17396; diff --git a/naval-radar-simulator/src/radar/protocol/constants/mod.rs b/naval-radar-simulator/src/radar/protocol/constants/mod.rs deleted file mode 100644 index b6525eb..0000000 --- a/naval-radar-simulator/src/radar/protocol/constants/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! # Constants for the radar protocol. -//! -//! This module contains the constants for the radar protocol. The constants are used to define the -//! different types of commands that can be sent between the client and the server. The constants -//! are defined as enums, with each enum variant representing a different command type. - -mod client_command; -mod error_message; -mod interface_ports; -mod server_command; - -pub use client_command::ClientCommand; -pub use error_message::ErrorMessage; -pub use interface_ports::{TCI_PORT, TDI_PORT}; -pub use server_command::ServerCommand; diff --git a/naval-radar-simulator/src/radar/protocol/constants/server_command.rs b/naval-radar-simulator/src/radar/protocol/constants/server_command.rs deleted file mode 100644 index e5c756c..0000000 --- a/naval-radar-simulator/src/radar/protocol/constants/server_command.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::fmt::{Display, Formatter, Result}; - -/// Commands that the server can send to the client -pub enum ServerCommand { - CurrVal, - MsgErr, - Pong, - Bye, - NaazCreated, - NaazDeleted, - NtzCreated, - NtzDeleted, - AtonCreated, - AtonDeleted, - EchoCreated, - EchoDeleted, - Track, - Corr, -} - -impl ServerCommand { - pub fn from_str(command: &str) -> Option { - match command.to_ascii_lowercase().as_str() { - "currval" => Some(ServerCommand::CurrVal), - "msgerr" => Some(ServerCommand::MsgErr), - "pong" => Some(ServerCommand::Pong), - "bye" => Some(ServerCommand::Bye), - "naazcreated" => Some(ServerCommand::NaazCreated), - "naazdeleted" => Some(ServerCommand::NaazDeleted), - "ntzcreated" => Some(ServerCommand::NtzCreated), - "ntzdeleted" => Some(ServerCommand::NtzDeleted), - "atoncreated" => Some(ServerCommand::AtonCreated), - "atondeleted" => Some(ServerCommand::AtonDeleted), - "echocreated" => Some(ServerCommand::EchoCreated), - "echodeleted" => Some(ServerCommand::EchoDeleted), - "track" => Some(ServerCommand::Track), - "corr" => Some(ServerCommand::Corr), - _ => None, - } - } -} - -impl Display for ServerCommand { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - let message = match self { - ServerCommand::CurrVal => "currval", - ServerCommand::MsgErr => "msgerr", - ServerCommand::Pong => "pong", - ServerCommand::Bye => "bye", - ServerCommand::NaazCreated => "naazcreated", - ServerCommand::NaazDeleted => "naazdeleted", - ServerCommand::NtzCreated => "ntzcreated", - ServerCommand::NtzDeleted => "ntzdeleted", - ServerCommand::AtonCreated => "atoncreated", - ServerCommand::AtonDeleted => "atondeleted", - ServerCommand::EchoCreated => "echocreated", - ServerCommand::EchoDeleted => "echodeleted", - ServerCommand::Track => "track", - ServerCommand::Corr => "corr", - }; - write!(f, "{}", message) - } -} diff --git a/naval-radar-simulator/src/radar/protocol/mod.rs b/naval-radar-simulator/src/radar/protocol/mod.rs deleted file mode 100644 index 455f432..0000000 --- a/naval-radar-simulator/src/radar/protocol/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod constants; -mod tcp_interfaces; - -use super::Track; -pub use tcp_interfaces::{TrackControlInterface, TrackDataInterface}; diff --git a/naval-radar-simulator/src/radar/protocol/tcp_interfaces/base_track_interface.rs b/naval-radar-simulator/src/radar/protocol/tcp_interfaces/base_track_interface.rs deleted file mode 100644 index 96bdd9f..0000000 --- a/naval-radar-simulator/src/radar/protocol/tcp_interfaces/base_track_interface.rs +++ /dev/null @@ -1,150 +0,0 @@ -use super::constants::ClientCommand; -use super::constants::ErrorMessage; -use super::constants::ServerCommand; -use crate::utils::{escape_text, unescape_text, ThreadPool}; -use std::io::{prelude::*, BufReader}; -use std::net::{TcpListener, TcpStream}; -use std::sync::{Arc, Mutex}; -use std::thread; - -type Clients = Arc>>; - -/// The server struct -/// This struct contains the server configuration and the list of connected clients -pub struct Server { - pub host: String, - pub port: u16, - pub num_workers: usize, - pub clients: Clients, -} - -impl Server { - /// Create a new server. - /// - /// # Arguments - /// - `host`: The host address to bind the server to - /// - `port`: The port to bind the server to - /// - `num_workers`: The number of worker threads to use - pub fn new(host: String, port: u16, num_workers: usize) -> Self { - Server { - host, - port, - num_workers, - clients: Arc::new(Mutex::new(Vec::with_capacity(num_workers))), - } - } -} - -/// Base interface for the track server -/// This trait defines the basic functionality that a track server should implement -pub trait BaseTrackInterface { - /// Start the server - /// This function should bind to the given host and port and listen for incoming connections - /// For each incoming connection, a new thread should be spawned to handle the client - fn start(server: &Server) { - let ip_addr = server.host.to_string() + ":" + &server.port.to_string(); - println!("Initializing server on {}", ip_addr); - - let listener = TcpListener::bind(ip_addr).unwrap(); - let pool = ThreadPool::new(server.num_workers); - let clients = Arc::clone(&server.clients); - - thread::spawn(move || { - for stream in listener.incoming() { - match stream { - Ok(mut stream) => { - let clients = Arc::clone(&clients); - pool.execute(move || { - Self::handle_client(&mut stream, clients); - }); - } - Err(_) => eprintln!("Failed to accept connection"), - } - } - }); - } - - /// Handle a client connection - /// This function should read requests from the client and send responses back - /// The function should continue reading requests until the client sends a "bye" command or disconnects - fn handle_client(stream: &mut TcpStream, clients: Clients) { - let peer_addr = stream.peer_addr().unwrap(); - println!("New client connected: {}", peer_addr); - - { - // Add the client to the list of clients in a thread-safe way - let mut clients = clients.lock().unwrap(); - clients.push(stream.try_clone().unwrap()); - } - - let reader = BufReader::new(stream.try_clone().unwrap()); - for request in reader.lines() { - match request { - Ok(request) => { - println!("Received request from client {}: {:?}", peer_addr, request); - let operation_index = request.find(',').unwrap_or(request.len()); - let (operation_str, args) = request.split_at(operation_index); - let operation = ClientCommand::from_str(operation_str); - let args = args.split(',').map(|s| unescape_text(s.trim())).collect(); - let result = match operation { - None => Err(ErrorMessage::UnknownCommand), - Some(ClientCommand::Bye) => break, - Some(ClientCommand::Ping) => Ok(Self::ping()), - Some(operation) => Self::handle_operation(operation, &args), - } - .unwrap_or_else(|error_message| { - Self::msgerr(error_message, operation_str.to_string(), &args) - }); - if result.len() > 0 { - stream.write_all(result.as_bytes()).unwrap(); - } - } - Err(_) => eprintln!("Failed to read request from client"), - } - } - - { - // Remove the client from the list of clients in a thread-safe way - let mut clients = clients.lock().unwrap(); - clients.retain(|client| client.peer_addr().unwrap() != peer_addr); - println!("Client {} disconnected", peer_addr); - } - } - - /// Handle a client operation - /// This function should handle the given operation and return the response - /// If the operation fails, an error message should be returned - fn handle_operation( - operation: ClientCommand, - args: &Vec, - ) -> Result; - - /// Broadcast a message to all clients - /// This function should send the given message to all clients in the list - /// The message should be sent as a byte array - fn broadcast(clients: &Vec, message: String) { - for mut client in clients { - client.write_all(message.as_bytes()).unwrap(); - } - } - - /// Create a message error response - /// The response should be in the format "msgerr,," - /// The error message and original command should be escaped - fn msgerr(error_message: ErrorMessage, operation: String, args: &Vec) -> String { - let original_command = operation + &args.join(","); - format!( - "{},{},{}", - ServerCommand::MsgErr, - escape_text(&error_message.to_string()), - escape_text(&original_command) - ) - } - - /// Create a ping command response - /// The response should be "pong" - /// The ping command should be ignored - fn ping() -> String { - ServerCommand::Pong.to_string() - } -} diff --git a/naval-radar-simulator/src/radar/protocol/tcp_interfaces/mod.rs b/naval-radar-simulator/src/radar/protocol/tcp_interfaces/mod.rs deleted file mode 100644 index 961bcce..0000000 --- a/naval-radar-simulator/src/radar/protocol/tcp_interfaces/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! This module contains the interfaces for the TCP communication with the radar. -//! The interfaces define the basic functionality that a track server should implement. - -mod base_track_interface; -mod track_control_interface; -mod track_data_interface; - -use super::{constants, Track}; -use base_track_interface::{BaseTrackInterface, Server}; - -pub use track_control_interface::TrackControlInterface; -pub use track_data_interface::TrackDataInterface; diff --git a/naval-radar-simulator/src/radar/protocol/tcp_interfaces/track_control_interface.rs b/naval-radar-simulator/src/radar/protocol/tcp_interfaces/track_control_interface.rs deleted file mode 100644 index 64e19cd..0000000 --- a/naval-radar-simulator/src/radar/protocol/tcp_interfaces/track_control_interface.rs +++ /dev/null @@ -1,46 +0,0 @@ -use super::constants::{ClientCommand, ErrorMessage, TCI_PORT}; -use super::{BaseTrackInterface, Server}; - -/// The track control interface -pub struct TrackControlInterface; - -impl TrackControlInterface { - /// Create and runs a new track control interface - /// - /// # Arguments - /// - `host`: The host address to bind the server to - /// - `num_workers`: The number of worker threads to use - pub fn new(host: String, num_workers: usize) -> Self { - let server = Server::new(host, TCI_PORT, num_workers); - ::start(&server); - TrackControlInterface {} - } -} - -impl BaseTrackInterface for TrackControlInterface { - fn handle_operation( - operation: ClientCommand, - _args: &Vec, - ) -> Result { - match operation { - ClientCommand::Get => todo!(), - ClientCommand::Set => todo!(), - ClientCommand::TrackCreate => todo!(), - ClientCommand::TrackDelete => todo!(), - ClientCommand::TrackSwap => todo!(), - ClientCommand::TrackSelect => todo!(), - ClientCommand::TrackMove => todo!(), - ClientCommand::NaazCreate => todo!(), - ClientCommand::NaazDelete => todo!(), - ClientCommand::NtzCreate => todo!(), - ClientCommand::NtzDelete => todo!(), - ClientCommand::AtonCreate => todo!(), - ClientCommand::AtonDelete => todo!(), - ClientCommand::EchoCreate => todo!(), - ClientCommand::EchoDelete => todo!(), - // The following commands are already handled in the base interface - ClientCommand::Ping => panic!("Ping should be handled in the base interface"), - ClientCommand::Bye => panic!("Bye should be handled in the base interface"), - } - } -} diff --git a/naval-radar-simulator/src/radar/protocol/tcp_interfaces/track_data_interface.rs b/naval-radar-simulator/src/radar/protocol/tcp_interfaces/track_data_interface.rs deleted file mode 100644 index ce6b822..0000000 --- a/naval-radar-simulator/src/radar/protocol/tcp_interfaces/track_data_interface.rs +++ /dev/null @@ -1,54 +0,0 @@ -use super::constants::{ClientCommand, ErrorMessage, TDI_PORT}; -use super::{BaseTrackInterface, Server, Track}; - -/// The track data interface -pub struct TrackDataInterface { - server: Server, -} - -impl TrackDataInterface { - /// Creates and runs a new track data interface - /// - /// # Arguments - /// - `host`: The host address to bind the server to - /// - `num_workers`: The number of worker threads to use - pub fn new(host: String, num_workers: usize) -> Self { - let server = Server::new(host, TDI_PORT, num_workers); - ::start(&server); - TrackDataInterface { server } - } - - /// Broadcast a message to all clients - pub fn broadcast(&self, track: Track) { - let clients = self.server.clients.lock().unwrap(); - ::broadcast(&clients, track.serialize()); - } -} - -impl BaseTrackInterface for TrackDataInterface { - fn handle_operation( - operation: ClientCommand, - _args: &Vec, - ) -> Result { - match operation { - ClientCommand::Get => todo!(), - ClientCommand::Set => todo!(), - ClientCommand::TrackCreate => todo!(), - ClientCommand::TrackDelete => todo!(), - ClientCommand::TrackSwap => todo!(), - ClientCommand::TrackSelect => todo!(), - ClientCommand::TrackMove => todo!(), - ClientCommand::NaazCreate => todo!(), - ClientCommand::NaazDelete => todo!(), - ClientCommand::NtzCreate => todo!(), - ClientCommand::NtzDelete => todo!(), - ClientCommand::AtonCreate => todo!(), - ClientCommand::AtonDelete => todo!(), - ClientCommand::EchoCreate => todo!(), - ClientCommand::EchoDelete => todo!(), - // The following commands are already handled in the base interface - ClientCommand::Ping => panic!("Ping should be handled in the base interface"), - ClientCommand::Bye => panic!("Bye should be handled in the base interface"), - } - } -} diff --git a/naval-radar-simulator/src/radar/radar.rs b/naval-radar-simulator/src/radar/radar.rs deleted file mode 100644 index c347944..0000000 --- a/naval-radar-simulator/src/radar/radar.rs +++ /dev/null @@ -1,36 +0,0 @@ -use super::{Detector, RadarConfig, TrackControlInterface, TrackDataInterface, Tracker, Wave}; -use std::sync::mpsc; -use std::thread; - -pub struct Radar { - config: RadarConfig, -} - -impl Radar { - pub fn new(config: RadarConfig) -> Radar { - Radar { config } - } - - pub fn start(&self, wave_receiver: mpsc::Receiver) { - let (plot_sender, plot_receiver) = mpsc::channel(); - let (track_sender, track_receiver) = mpsc::channel(); - - let detector = Detector::new(self.config.detector.clone()); - detector.start(wave_receiver, plot_sender); - Tracker::start(plot_receiver, track_sender); - TrackControlInterface::new( - self.config.protocol.host.clone(), - self.config.protocol.num_workers_tci, - ); - let tdi = TrackDataInterface::new( - self.config.protocol.host.clone(), - self.config.protocol.num_workers_tdi, - ); - - thread::spawn(move || { - while let Ok(track) = track_receiver.recv() { - tdi.broadcast(track); - } - }); - } -} diff --git a/naval-radar-simulator/src/radar/tracker/mod.rs b/naval-radar-simulator/src/radar/tracker/mod.rs deleted file mode 100644 index 4be4e43..0000000 --- a/naval-radar-simulator/src/radar/tracker/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod track; -mod tracker; - -use super::Plot; -pub use track::Track; -pub use tracker::Tracker; diff --git a/naval-radar-simulator/src/radar/tracker/track.rs b/naval-radar-simulator/src/radar/tracker/track.rs deleted file mode 100644 index b4959ab..0000000 --- a/naval-radar-simulator/src/radar/tracker/track.rs +++ /dev/null @@ -1,84 +0,0 @@ -/// Represents a radar track with relevant information captured by the radar system. -/// -/// # Arguments -/// - `id`: Unique track identifier (0 ≤ id ≤ 9999) -/// - `timestamp`: Date and time of detection (year, month, day, hour, minute, second, millisecond) -/// - `stat`: Track status (CA, CS, FA, FS, LA, LS) -/// - `type_`: Type of object (TARGET, ATON) -/// - `name`: Name of the object if `ATON`, empty if `TARGET` -/// - `linemask`: Proprietary service information (0 ≤ linemask ≤ 63) -/// - `size`: Size or plot area of the track -/// - `range`: Slant range of the track in meters relative to the radar -/// - `azimuth`: Track azimuth in radians (0 ≤ azimuth ≤ 6.28319) -/// - `lat`: Latitude of the track in decimal degrees (-90 ≤ lat ≤ 90) -/// - `long`: Longitude of the track in decimal degrees (-180 ≤ long ≤ 180) -/// - `speed`: Speed in m/s -/// - `course`: Direction of the speed vector in radians (0 ≤ course ≤ 6.28319) -/// - `quality`: Track quality (0 ≤ quality ≤ 30) -/// - `l16quality`: STANAG5516 track quality (0 ≤ l16quality ≤ 15) -/// - `lacks`: Track detection gaps or misses -/// - `winrgw`: Track search window range width in meters -/// - `winazw`: Track search window azimuth width in radians -/// - `stderr`: The tracker’s calculated standard error on the filtered track position in meters -#[derive(Debug)] -pub struct Track { - pub id: u64, - pub year: u32, - pub month: u32, - pub day: u32, - pub hour: u32, - pub minute: u32, - pub second: u32, - pub millisecond: u32, - pub stat: String, - pub type_: String, - pub name: String, - pub linemask: u32, - pub size: u32, - pub range: f64, - pub azimuth: f64, - pub lat: f64, - pub long: f64, - pub speed: f64, - pub course: f64, - pub quality: u32, - pub l16quality: u32, - pub lacks: u32, - pub winrgw: u32, - pub winazw: f64, - pub stderr: f64, -} - -impl Track { - /// Serializes the Track into a CSV-formatted string. - pub fn serialize(&self) -> String { - format!( - "{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}", - self.id, - self.year, - self.month, - self.day, - self.hour, - self.minute, - self.second, - self.millisecond, - self.stat, - self.type_, - self.name.clone(), - self.linemask, - self.size, - self.range, - self.azimuth, - self.lat, - self.long, - self.speed, - self.course, - self.quality, - self.l16quality, - self.lacks, - self.winrgw, - self.winazw, - self.stderr - ) - } -} diff --git a/naval-radar-simulator/src/radar/tracker/tracker.rs b/naval-radar-simulator/src/radar/tracker/tracker.rs deleted file mode 100644 index bbeaf48..0000000 --- a/naval-radar-simulator/src/radar/tracker/tracker.rs +++ /dev/null @@ -1,65 +0,0 @@ -use super::{Plot, Track}; -use chrono::{Datelike, Timelike}; -use std::collections::HashMap; -use std::sync::mpsc; -use std::thread; - -pub struct Tracker; - -impl Tracker { - pub fn start(plot_receiver: mpsc::Receiver, track_sender: mpsc::Sender) { - thread::spawn(move || { - let mut last_plot_by_id = HashMap::new(); - loop { - let plot = plot_receiver.recv().expect("Failed to receive plot"); - let (speed, course) = if let Some(last_plot) = last_plot_by_id.get(&plot.id) { - Tracker::calculate_speed_vector(last_plot, &plot) - } else { - (0.0, 0.0) - }; - - let track = Track { - id: plot.id, - year: plot.timestamp.year() as u32, - month: plot.timestamp.month(), - day: plot.timestamp.day(), - hour: plot.timestamp.hour(), - minute: plot.timestamp.minute(), - second: plot.timestamp.second(), - millisecond: plot.timestamp.timestamp_subsec_millis(), - stat: "CA".to_string(), - type_: "TARGET".to_string(), - name: "".to_string(), - linemask: 0, - size: 0, - range: plot.range, - azimuth: plot.azimuth, - lat: plot.latitude, - long: plot.longitude, - speed, - course, - quality: 0, - l16quality: 0, - lacks: 0, - winrgw: 0, - winazw: 0.0, - stderr: 0.0, - }; - track_sender.send(track).unwrap(); - last_plot_by_id.insert(plot.id, plot); - } - }); - } - - fn calculate_speed_vector(old_plot: &Plot, new_plot: &Plot) -> (f64, f64) { - let time_diff = new_plot.timestamp - old_plot.timestamp; - let delta_x = - new_plot.range * new_plot.azimuth.cos() - old_plot.range * old_plot.azimuth.cos(); - let delta_y = - new_plot.range * new_plot.azimuth.sin() - old_plot.range * old_plot.azimuth.sin(); - let speed = (delta_x.powi(2) + delta_y.powi(2)).sqrt() * 1000.0 - / time_diff.num_milliseconds() as f64; - let course = delta_y.atan2(delta_x); - (speed, course) - } -} diff --git a/naval-radar-simulator/src/simulation/config.rs b/naval-radar-simulator/src/simulation/config.rs deleted file mode 100644 index e58c00f..0000000 --- a/naval-radar-simulator/src/simulation/config.rs +++ /dev/null @@ -1,40 +0,0 @@ -use serde::Deserialize; - -#[derive(Debug, Deserialize)] -pub struct SimulationConfig { - pub emission_interval: u64, - pub ships: ShipsConfig, -} - -#[derive(Debug, Deserialize)] -pub struct ShipsConfig { - pub line: Vec, - pub circle: Vec, - pub random: Vec, - pub stationary: Vec, -} - -#[derive(Debug, Deserialize)] -pub struct LineMovementConfig { - pub initial_position: (f64, f64), - pub angle: f64, - pub speed: f64, -} - -#[derive(Debug, Deserialize)] -pub struct CircleMovementConfig { - pub initial_position: (f64, f64), - pub radius: f64, - pub speed: f64, -} - -#[derive(Debug, Deserialize)] -pub struct RandomMovementConfig { - pub initial_position: (f64, f64), - pub max_speed: f64, -} - -#[derive(Debug, Deserialize)] -pub struct StationaryMovementConfig { - pub initial_position: (f64, f64), -} diff --git a/naval-radar-simulator/src/simulation/emitters/emitter.rs b/naval-radar-simulator/src/simulation/emitters/emitter.rs deleted file mode 100644 index 66c34f8..0000000 --- a/naval-radar-simulator/src/simulation/emitters/emitter.rs +++ /dev/null @@ -1,5 +0,0 @@ -use super::Wave; - -pub trait Emitter { - fn emit(&mut self) -> Wave; -} diff --git a/naval-radar-simulator/src/simulation/emitters/mod.rs b/naval-radar-simulator/src/simulation/emitters/mod.rs deleted file mode 100644 index 43f695c..0000000 --- a/naval-radar-simulator/src/simulation/emitters/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod emitter; -mod ship; - -use super::MovementStrategy; -use super::Wave; - -pub use emitter::Emitter; -pub use ship::Ship; diff --git a/naval-radar-simulator/src/simulation/emitters/ship.rs b/naval-radar-simulator/src/simulation/emitters/ship.rs deleted file mode 100644 index d979198..0000000 --- a/naval-radar-simulator/src/simulation/emitters/ship.rs +++ /dev/null @@ -1,29 +0,0 @@ -use super::{Emitter, MovementStrategy, Wave}; - -pub struct Ship { - pub id: u64, - pub position: (f64, f64), - pub emission_interval: u64, - pub movement_strategy: Box, -} - -impl Ship { - pub fn update(&mut self) { - let (x, y) = self.position; - let movement_command = self.movement_strategy.next_movement(); - let emission_interval = self.emission_interval as f64; - let dx = movement_command.speed * movement_command.angle.cos() * emission_interval / 1000.0; - let dy = movement_command.speed * movement_command.angle.sin() * emission_interval / 1000.0; - self.position = (x + dx, y + dy); - } -} - -impl Emitter for Ship { - fn emit(&mut self) -> Wave { - Wave { - id: self.id, - position: self.position, - timestamp: chrono::Utc::now(), - } - } -} diff --git a/naval-radar-simulator/src/simulation/environment/mod.rs b/naval-radar-simulator/src/simulation/environment/mod.rs deleted file mode 100644 index 22e125b..0000000 --- a/naval-radar-simulator/src/simulation/environment/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod wave; -pub use wave::Wave; diff --git a/naval-radar-simulator/src/simulation/environment/wave.rs b/naval-radar-simulator/src/simulation/environment/wave.rs deleted file mode 100644 index 1eb0b6a..0000000 --- a/naval-radar-simulator/src/simulation/environment/wave.rs +++ /dev/null @@ -1,11 +0,0 @@ -/// Represents an electromagnetic (radio) wave in the simulation -/// For now, it only carries the information that the radar uses to detect objects -/// In the future, it could be used to simulate the wave propagation and reflection -use chrono::{DateTime, Utc}; - -#[derive(Debug)] -pub struct Wave { - pub id: u64, - pub position: (f64, f64), - pub timestamp: DateTime, -} diff --git a/naval-radar-simulator/src/simulation/mod.rs b/naval-radar-simulator/src/simulation/mod.rs deleted file mode 100644 index e0a9fce..0000000 --- a/naval-radar-simulator/src/simulation/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Simulation module. -//! -//! This module contains the simulation environment, emitters and movement strategies. -//! - -mod config; -mod emitters; -mod environment; -mod movement; -mod simulation; - -use emitters::Emitter; -use movement::{ - CircleMovement, LineMovement, MovementStrategy, RandomMovement, StationaryMovement, -}; - -pub use config::SimulationConfig; -pub use environment::Wave; -pub use simulation::Simulation; diff --git a/naval-radar-simulator/src/simulation/movement/circle.rs b/naval-radar-simulator/src/simulation/movement/circle.rs deleted file mode 100644 index 7da6263..0000000 --- a/naval-radar-simulator/src/simulation/movement/circle.rs +++ /dev/null @@ -1,34 +0,0 @@ -use super::{MovementCommand, MovementStrategy}; -use std::f64::consts::PI; - -pub struct CircleMovement { - speed: f64, - radius: f64, - time_delta: u64, - current_angle: f64, -} - -impl CircleMovement { - pub fn new(radius: f64, speed: f64, time_delta: u64) -> Self { - CircleMovement { - speed, - radius, - time_delta, - current_angle: 0.0, - } - } -} - -impl MovementStrategy for CircleMovement { - fn next_movement(&mut self) -> MovementCommand { - let time_step = self.time_delta as f64 / 1000.0; - let distance = self.speed * time_step; - let angle_step = distance / self.radius; - self.current_angle = (self.current_angle + angle_step) % (2.0 * PI); - - MovementCommand { - angle: self.current_angle, - speed: self.speed, - } - } -} diff --git a/naval-radar-simulator/src/simulation/movement/line.rs b/naval-radar-simulator/src/simulation/movement/line.rs deleted file mode 100644 index 44087aa..0000000 --- a/naval-radar-simulator/src/simulation/movement/line.rs +++ /dev/null @@ -1,21 +0,0 @@ -use super::{MovementCommand, MovementStrategy}; - -pub struct LineMovement { - angle: f64, - speed: f64, -} - -impl LineMovement { - pub fn new(angle: f64, speed: f64) -> Self { - LineMovement { angle, speed } - } -} - -impl MovementStrategy for LineMovement { - fn next_movement(&mut self) -> MovementCommand { - MovementCommand { - angle: self.angle, - speed: self.speed, - } - } -} diff --git a/naval-radar-simulator/src/simulation/movement/mod.rs b/naval-radar-simulator/src/simulation/movement/mod.rs deleted file mode 100644 index 1663a6d..0000000 --- a/naval-radar-simulator/src/simulation/movement/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod circle; -mod line; -mod random; -mod stationary; -mod strategy; - -pub use circle::CircleMovement; -pub use line::LineMovement; -pub use random::RandomMovement; -pub use stationary::StationaryMovement; -pub use strategy::{MovementCommand, MovementStrategy}; diff --git a/naval-radar-simulator/src/simulation/movement/random.rs b/naval-radar-simulator/src/simulation/movement/random.rs deleted file mode 100644 index ab866f5..0000000 --- a/naval-radar-simulator/src/simulation/movement/random.rs +++ /dev/null @@ -1,22 +0,0 @@ -use super::{MovementCommand, MovementStrategy}; -use rand::Rng; -use std::f64::consts::PI; - -pub struct RandomMovement { - max_speed: f64, -} - -impl RandomMovement { - pub fn new(max_speed: f64) -> RandomMovement { - RandomMovement { max_speed } - } -} - -impl MovementStrategy for RandomMovement { - fn next_movement(&mut self) -> MovementCommand { - let mut rng = rand::thread_rng(); - let angle = rng.gen_range(0.0..(2.0 * PI)); - let speed = rng.gen_range(0.0..self.max_speed); - MovementCommand { angle, speed } - } -} diff --git a/naval-radar-simulator/src/simulation/movement/stationary.rs b/naval-radar-simulator/src/simulation/movement/stationary.rs deleted file mode 100644 index 1b24246..0000000 --- a/naval-radar-simulator/src/simulation/movement/stationary.rs +++ /dev/null @@ -1,12 +0,0 @@ -use super::{MovementCommand, MovementStrategy}; - -pub struct StationaryMovement; - -impl MovementStrategy for StationaryMovement { - fn next_movement(&mut self) -> MovementCommand { - MovementCommand { - angle: 0.0, - speed: 0.0, - } - } -} diff --git a/naval-radar-simulator/src/simulation/movement/strategy.rs b/naval-radar-simulator/src/simulation/movement/strategy.rs deleted file mode 100644 index 6f41447..0000000 --- a/naval-radar-simulator/src/simulation/movement/strategy.rs +++ /dev/null @@ -1,14 +0,0 @@ -/// # Movement Command -/// A movement command is a struct that contains the angle and speed of a movement. -/// -/// # Parameters -/// - `angle`: The angle of the movement in radians. -/// - `speed`: The speed of the movement in m/s. -pub struct MovementCommand { - pub angle: f64, - pub speed: f64, -} - -pub trait MovementStrategy: Send + Sync { - fn next_movement(&mut self) -> MovementCommand; -} diff --git a/naval-radar-simulator/src/simulation/simulation.rs b/naval-radar-simulator/src/simulation/simulation.rs deleted file mode 100644 index af666f4..0000000 --- a/naval-radar-simulator/src/simulation/simulation.rs +++ /dev/null @@ -1,92 +0,0 @@ -use crate::simulation::emitters::Ship; -use std::time::Duration; - -use super::{ - CircleMovement, Emitter, LineMovement, RandomMovement, SimulationConfig, StationaryMovement, - Wave, -}; -use std::sync::mpsc::Sender; -use std::thread; - -pub struct Simulation { - config: SimulationConfig, -} - -impl Simulation { - /// Create a new Simulation. - pub fn new(config: SimulationConfig) -> Simulation { - Simulation { config } - } - - /// Start the simulation. - pub fn start(&self, wave_sender: Sender) { - let emission_interval = self.config.emission_interval; - let mut ships = Vec::new(); - let mut ship_id = 0; - - // Create ships with line movement - for line_ship in &self.config.ships.line { - let movement_strategy = Box::new(LineMovement::new(line_ship.angle, line_ship.speed)); - ships.push(Ship { - id: ship_id, - position: line_ship.initial_position, - emission_interval, - movement_strategy, - }); - ship_id += 1; - } - - // Create ships with circle movement - for circle_ship in &self.config.ships.circle { - let movement_strategy = Box::new(CircleMovement::new( - circle_ship.radius, - circle_ship.speed, - emission_interval, - )); - ships.push(Ship { - id: ship_id, - position: circle_ship.initial_position, - emission_interval, - movement_strategy, - }); - ship_id += 1; - } - - // Create ships with random movement - for random_ship in &self.config.ships.random { - let movement_strategy = Box::new(RandomMovement::new(random_ship.max_speed)); - ships.push(Ship { - id: ship_id, - position: random_ship.initial_position, - emission_interval, - movement_strategy, - }); - ship_id += 1; - } - - // Create ships with stationary movement - for stationary_ship in &self.config.ships.stationary { - let movement_strategy = Box::new(StationaryMovement {}); - ships.push(Ship { - id: ship_id, - position: stationary_ship.initial_position, - emission_interval, - movement_strategy, - }); - ship_id += 1; - } - - thread::spawn(move || loop { - let mut waves = Vec::with_capacity(ships.len()); - for ship in ships.iter_mut() { - let wave = ship.emit(); - waves.push(wave); - ship.update(); - } - for wave in waves { - wave_sender.send(wave).expect("Failed to send wave"); - } - thread::sleep(Duration::from_millis(emission_interval)); - }); - } -} diff --git a/naval-radar-simulator/src/utils/escape_ascii.rs b/naval-radar-simulator/src/utils/escape_ascii.rs deleted file mode 100644 index 7533160..0000000 --- a/naval-radar-simulator/src/utils/escape_ascii.rs +++ /dev/null @@ -1,43 +0,0 @@ -/// Escape special characters in TCP communication text. -/// -/// # Examples -/// ``` -/// let s = "Hello, world!"; -/// let escaped = escape_text(s); -/// assert_eq!(escaped, "hello\\2c world!"); -/// ``` -pub fn escape_text(s: &str) -> String { - s.to_ascii_lowercase() - .chars() - .map(|c| match c { - ',' => "\\2c".to_string(), - '\\' => "\\5c".to_string(), - c if c as u8 <= 31 || c as u8 >= 128 => format!("\\{:02x}", c as u8), - _ => c.to_string(), - }) - .collect() -} - -/// Unescape special characters in TCP communication text. -/// -/// # Examples -/// ``` -/// let s = "hello\\2c world!"; -/// let unescaped = unescape_text(s); -/// assert_eq!(unescaped, "hello, world!"); -/// ``` -pub fn unescape_text(s: &str) -> String { - let mut result = String::new(); - let mut chars = s.chars(); - while let Some(c) = chars.next() { - if c == '\\' { - let hex = chars.next().unwrap(); - let hex = hex.to_string() + &chars.next().unwrap().to_string(); - let c = u8::from_str_radix(&hex, 16).unwrap_or(b' ') as char; - result.push(c as char); - } else { - result.push(c); - } - } - result.to_ascii_lowercase() -} diff --git a/naval-radar-simulator/src/utils/mod.rs b/naval-radar-simulator/src/utils/mod.rs deleted file mode 100644 index 519bdbb..0000000 --- a/naval-radar-simulator/src/utils/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! # Utility module -//! -//! This module contains utility functions and structures that are used throughout the project. -//! - -mod thread_pool; -pub use thread_pool::ThreadPool; -mod escape_ascii; -pub use escape_ascii::{escape_text, unescape_text}; diff --git a/naval-radar-simulator/src/utils/thread_pool.rs b/naval-radar-simulator/src/utils/thread_pool.rs deleted file mode 100644 index 245b50a..0000000 --- a/naval-radar-simulator/src/utils/thread_pool.rs +++ /dev/null @@ -1,93 +0,0 @@ -use std::sync::{mpsc, Arc, Mutex}; -use std::thread; - -/// A thread pool. -/// -/// The `ThreadPool` struct creates a number of threads and maintains a queue of jobs to be executed. -pub struct ThreadPool { - workers: Vec, - sender: Option>, -} - -type Job = Box; - -impl ThreadPool { - /// Create a new ThreadPool. - /// - /// The size is the number of threads in the pool. - /// - /// # Panics - /// - /// The `new` function will panic if the size is zero. - pub fn new(size: usize) -> ThreadPool { - assert!(size > 0, "ThreadPool size must be greater than zero."); - - let (sender, receiver) = mpsc::channel(); - let receiver = Arc::new(Mutex::new(receiver)); - let mut workers = Vec::with_capacity(size); - - for _ in 0..size { - workers.push(Worker::new(Arc::clone(&receiver))); - } - - ThreadPool { - workers, - sender: Some(sender), - } - } - - /// Execute a function on the thread pool. - /// - /// The function must be `FnOnce`, `Send` and `'static`. - pub fn execute(&self, f: F) - where - F: FnOnce() + Send + 'static, - { - let job = Box::new(f); - self.sender.as_ref().unwrap().send(job).unwrap(); - } -} - -impl Drop for ThreadPool { - /// Drop all threads in the ThreadPool. - /// - /// This method will wait for all threads to finish their work before returning. - fn drop(&mut self) { - drop(self.sender.take()); - - for worker in &mut self.workers { - if let Some(thread) = worker.thread.take() { - thread.join().unwrap(); - } - } - } -} - -/// A worker in the thread pool. -/// -/// The `Worker` struct is responsible for executing jobs on the thread pool. -struct Worker { - thread: Option>, -} - -impl Worker { - /// Create a new Worker. - /// - /// The `new` function creates a new worker that will execute jobs from the receiver. - /// `receiver` is a `mpsc::Receiver` that contains the jobs to be executed. - /// - /// The worker will continue to execute jobs until the receiver is closed. - fn new(receiver: Arc>>) -> Worker { - let thread = thread::spawn(move || loop { - let message = receiver.lock().unwrap().recv(); - match message { - Ok(job) => job(), - Err(_) => break, - } - }); - - Worker { - thread: Some(thread), - } - } -} From 0e72fd704bf6167f8c21c331ced62c5892522be3 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Urrea <68788933+jsurrea@users.noreply.github.com> Date: Fri, 11 Apr 2025 01:27:27 -0500 Subject: [PATCH 03/17] fix ci issues --- .github/workflows/python-ci.yml | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index f23a595..42f3248 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -30,27 +30,19 @@ jobs: curl -Ls https://astral.sh/uv/install.sh | bash echo "$HOME/.cargo/bin" >> $GITHUB_PATH - - name: Create and activate venv - run: | - uv venv - source .venv/bin/activate - uv pip install -U uv - - name: Install dependencies run: | - uv pip install -r requirements.txt || true uv pip install -e . - uv pip install pytest mypy ruff + uv pip install pytest mypy ruff build - name: Run linters - run: ruff check . + run: python -m ruff check . - name: Run formatters - run: ruff format --check . + run: python -m ruff format --check . - name: Run mypy run: mypy src/ - name: Run tests with coverage run: pytest --cov=src --cov-report=term-missing --cov-fail-under=80 - From 86da1e07a966d8bdf2d70f4626b0ea1db77a06f3 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Urrea <68788933+jsurrea@users.noreply.github.com> Date: Fri, 11 Apr 2025 01:29:44 -0500 Subject: [PATCH 04/17] fix pip install in ci --- .github/workflows/python-ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 42f3248..4e3616c 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -32,8 +32,9 @@ jobs: - name: Install dependencies run: | - uv pip install -e . - uv pip install pytest mypy ruff build + uv pip install --system -e . + uv pip install --system pytest mypy ruff build + - name: Run linters run: python -m ruff check . From 98866d78f07cf6ec9ff370138c4182a298883469 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Urrea <68788933+jsurrea@users.noreply.github.com> Date: Fri, 11 Apr 2025 01:31:14 -0500 Subject: [PATCH 05/17] add pytest-cov to ci --- .github/workflows/python-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 4e3616c..d4d49ae 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -33,7 +33,7 @@ jobs: - name: Install dependencies run: | uv pip install --system -e . - uv pip install --system pytest mypy ruff build + uv pip install --system pytest pytest-cov mypy ruff build - name: Run linters From 0f9391d0484afcc89dce13e1224349eec2f46b46 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Urrea <68788933+jsurrea@users.noreply.github.com> Date: Fri, 11 Apr 2025 01:33:09 -0500 Subject: [PATCH 06/17] remove precommit config --- antares-python/.pre-commit-config.yaml | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 antares-python/.pre-commit-config.yaml diff --git a/antares-python/.pre-commit-config.yaml b/antares-python/.pre-commit-config.yaml deleted file mode 100644 index 59399b4..0000000 --- a/antares-python/.pre-commit-config.yaml +++ /dev/null @@ -1,18 +0,0 @@ -repos: - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.5 - hooks: - - id: ruff-format - - id: ruff - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.15.0 - hooks: - - id: mypy - - repo: local - hooks: - - id: check-coverage - name: Check minimum coverage - entry: python antares-python/scripts/check_coverage.py - language: system - types: [python] - From 6250515c4b07ebf03bb5e5267cc2cc9dec5b5698 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Urrea <68788933+jsurrea@users.noreply.github.com> Date: Fri, 11 Apr 2025 14:25:12 -0500 Subject: [PATCH 07/17] Implement API calls and CLI --- .github/workflows/python-ci.yml | 2 +- antares-python/pyproject.toml | 17 +++- antares-python/src/antares/__init__.py | 5 +- antares-python/src/antares/cli.py | 79 +++++++++++++++++++ antares-python/src/antares/client/__init__.py | 61 ++++++++++++++ antares-python/src/antares/client/rest.py | 61 ++++++++++++++ antares-python/src/antares/client/tcp.py | 52 ++++++++++++ antares-python/src/antares/config.py | 18 +++++ antares-python/src/antares/config_loader.py | 20 +++++ antares-python/src/antares/core.py | 2 - antares-python/src/antares/errors.py | 18 +++++ antares-python/src/antares/models/ship.py | 11 +++ antares-python/tests/client/test_client.py | 30 +++++++ antares-python/tests/client/test_rest.py | 36 +++++++++ antares-python/tests/client/test_tcp.py | 35 ++++++++ antares-python/tests/models/test_ship.py | 6 ++ antares-python/tests/test_cli.py | 49 ++++++++++++ antares-python/tests/test_core.py | 5 -- 18 files changed, 494 insertions(+), 13 deletions(-) create mode 100644 antares-python/src/antares/cli.py create mode 100644 antares-python/src/antares/client/__init__.py create mode 100644 antares-python/src/antares/client/rest.py create mode 100644 antares-python/src/antares/client/tcp.py create mode 100644 antares-python/src/antares/config.py create mode 100644 antares-python/src/antares/config_loader.py delete mode 100644 antares-python/src/antares/core.py create mode 100644 antares-python/src/antares/errors.py create mode 100644 antares-python/src/antares/models/ship.py create mode 100644 antares-python/tests/client/test_client.py create mode 100644 antares-python/tests/client/test_rest.py create mode 100644 antares-python/tests/client/test_tcp.py create mode 100644 antares-python/tests/models/test_ship.py create mode 100644 antares-python/tests/test_cli.py delete mode 100644 antares-python/tests/test_core.py diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index d4d49ae..b100eb6 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -33,7 +33,7 @@ jobs: - name: Install dependencies run: | uv pip install --system -e . - uv pip install --system pytest pytest-cov mypy ruff build + uv pip install --system pytest pytest-cov pytest-mock mypy ruff build - name: Run linters diff --git a/antares-python/pyproject.toml b/antares-python/pyproject.toml index 2a4660f..0194158 100644 --- a/antares-python/pyproject.toml +++ b/antares-python/pyproject.toml @@ -2,17 +2,28 @@ name = "antares-python" version = "0.1.0" description = "Python interface for the Antares simulation software" -authors = [{ name = "Juan Sebastian Urrea-Lopez", email = "js.urrea@uniandes.edu.co" }] +authors = [ + { name = "Juan Sebastian Urrea-Lopez", email = "js.urrea@uniandes.edu.co" }, +] readme = "README.md" requires-python = ">=3.13" -dependencies = [] +dependencies = [ + "pydantic>=2.11.3", + "httpx>=0.28.1", + "typer>=0.15.2", + "rich>=13.0", + "tomli>=2.2.1", +] + +[project.scripts] +antares = "antares.cli:app" [build-system] requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [tool.ruff] -line-length = 88 +line-length = 100 lint.select = ["E", "F", "I", "UP", "B", "PL"] exclude = ["dist", "build"] diff --git a/antares-python/src/antares/__init__.py b/antares-python/src/antares/__init__.py index f30e812..ae3939e 100644 --- a/antares-python/src/antares/__init__.py +++ b/antares-python/src/antares/__init__.py @@ -1,3 +1,4 @@ -from .core import saludar +from .client import AntaresClient +from .models.ship import ShipConfig -__all__ = ["saludar"] +__all__ = ["AntaresClient", "ShipConfig"] diff --git a/antares-python/src/antares/cli.py b/antares-python/src/antares/cli.py new file mode 100644 index 0000000..2baecc9 --- /dev/null +++ b/antares-python/src/antares/cli.py @@ -0,0 +1,79 @@ +import asyncio + +import typer +from rich.console import Console +from rich.theme import Theme + +from antares import AntaresClient, ShipConfig +from antares.config_loader import load_config + +app = typer.Typer(help="Antares Simulation CLI") + +console = Console( + theme=Theme( + { + "info": "green", + "warn": "yellow", + "error": "bold red", + } + ) +) + + +def build_client(config_path: str | None, verbose: bool) -> AntaresClient: + settings = load_config(config_path) + + if verbose: + console.print(f"[info]Using settings: {settings.model_dump()}") + + return AntaresClient( + base_url=settings.base_url, + tcp_host=settings.tcp_host, + tcp_port=settings.tcp_port, + timeout=settings.timeout, + auth_token=settings.auth_token, + ) + + +@app.command() +def reset( + config: str = typer.Option(None, help="Path to config TOML file"), + verbose: bool = typer.Option(False, "--verbose", "-v"), +): + """Reset the simulation.""" + client = build_client(config, verbose) + client.reset_simulation() + console.print("[info]✅ Simulation reset.") + + +@app.command() +def add_ship( + x: float, + y: float, + config: str = typer.Option(None, help="Path to config TOML file"), + verbose: bool = typer.Option(False, "--verbose", "-v"), +): + """Add a ship to the simulation at (x, y).""" + client = build_client(config, verbose) + ship = ShipConfig(initial_position=(x, y)) + client.add_ship(ship) + console.print(f"[info]🚢 Added ship at ({x}, {y})") + + +@app.command() +def subscribe( + config: str = typer.Option(None, help="Path to config TOML file"), + verbose: bool = typer.Option(False, "--verbose", "-v"), +): + """Subscribe to simulation data stream.""" + client = build_client(config, verbose) + + async def _sub(): + async for event in client.subscribe(): + console.print_json(data=event) + + asyncio.run(_sub()) + + +if __name__ == "__main__": + app() diff --git a/antares-python/src/antares/client/__init__.py b/antares-python/src/antares/client/__init__.py new file mode 100644 index 0000000..ef646e6 --- /dev/null +++ b/antares-python/src/antares/client/__init__.py @@ -0,0 +1,61 @@ +from collections.abc import AsyncIterator + +from antares.client.rest import RestClient +from antares.client.tcp import TCPSubscriber +from antares.config import AntaresSettings +from antares.models.ship import ShipConfig + + +class AntaresClient: + def __init__( + self, + base_url: str | None = None, + tcp_host: str | None = None, + tcp_port: int | None = None, + timeout: float | None = None, + auth_token: str | None = None, + ) -> None: + """ + Public interface for interacting with the Antares simulation engine. + Accepts config overrides directly or falls back to environment-based configuration. + """ + + # Merge provided arguments with environment/.env via AntaresSettings + self._settings = AntaresSettings( + base_url=base_url, + tcp_host=tcp_host, + tcp_port=tcp_port, + timeout=timeout, + auth_token=auth_token, + ) + + self._rest = RestClient( + base_url=self._settings.base_url, + timeout=self._settings.timeout, + auth_token=self._settings.auth_token, + ) + self._tcp = TCPSubscriber( + host=self._settings.tcp_host, port=self._settings.tcp_port + ) + + def reset_simulation(self) -> None: + """ + Sends a request to reset the current simulation state. + """ + return self._rest.reset_simulation() + + def add_ship(self, ship: ShipConfig) -> None: + """ + Sends a new ship configuration to the simulation engine. + """ + return self._rest.add_ship(ship) + + async def subscribe(self) -> AsyncIterator[dict]: + """ + Subscribes to live simulation data over TCP. + + Yields: + Parsed simulation event data as dictionaries. + """ + async for event in self._tcp.subscribe(): + yield event diff --git a/antares-python/src/antares/client/rest.py b/antares-python/src/antares/client/rest.py new file mode 100644 index 0000000..f7dd9c2 --- /dev/null +++ b/antares-python/src/antares/client/rest.py @@ -0,0 +1,61 @@ +import httpx + +from antares.errors import ConnectionError, SimulationError +from antares.models.ship import ShipConfig + + +class RestClient: + """ + Internal client for interacting with the Antares simulation REST API. + """ + + def __init__( + self, base_url: str, timeout: float = 5.0, auth_token: str | None = None + ) -> None: + """ + Initializes the REST client. + + Args: + base_url: The root URL of the Antares HTTP API. + timeout: Timeout in seconds for each request. + auth_token: Optional bearer token for authentication. + """ + self.base_url = base_url.rstrip("/") + self.timeout = timeout + self.headers = {"Authorization": f"Bearer {auth_token}"} if auth_token else {} + + def reset_simulation(self) -> None: + """ + Sends a request to reset the current simulation state. + """ + try: + response = httpx.post( + f"{self.base_url}/simulation/reset", + headers=self.headers, + timeout=self.timeout, + ) + response.raise_for_status() + except httpx.RequestError as e: + raise ConnectionError(f"Could not reach Antares API: {e}") from e + except httpx.HTTPStatusError as e: + raise SimulationError(f"Reset failed: {e.response.text}") from e + + def add_ship(self, ship: ShipConfig) -> None: + """ + Sends a ship configuration to the simulation engine. + + Args: + ship: A validated ShipConfig instance. + """ + try: + response = httpx.post( + f"{self.base_url}/simulation/ships", + json=ship.model_dump(), + headers=self.headers, + timeout=self.timeout, + ) + response.raise_for_status() + except httpx.RequestError as e: + raise ConnectionError(f"Could not reach Antares API: {e}") from e + except httpx.HTTPStatusError as e: + raise SimulationError(f"Add ship failed: {e.response.text}") from e diff --git a/antares-python/src/antares/client/tcp.py b/antares-python/src/antares/client/tcp.py new file mode 100644 index 0000000..3be0e44 --- /dev/null +++ b/antares-python/src/antares/client/tcp.py @@ -0,0 +1,52 @@ +import asyncio +import json +from collections.abc import AsyncIterator + +from antares.errors import SubscriptionError + + +class TCPSubscriber: + """ + Manages a TCP connection to the Antares simulation for real-time event streaming. + """ + + def __init__(self, host: str, port: int, reconnect: bool = True) -> None: + """ + Initializes the TCP subscriber. + + Args: + host: The hostname or IP of the TCP server. + port: The port number of the TCP server. + reconnect: Whether to automatically reconnect on disconnect. + """ + self.host = host + self.port = port + self.reconnect = reconnect + + async def subscribe(self) -> AsyncIterator[dict]: + """ + Connects to the TCP server and yields simulation events as parsed dictionaries. + This is an infinite async generator until disconnected or cancelled. + + Yields: + Parsed simulation events. + """ + while True: + try: + reader, _ = await asyncio.open_connection(self.host, self.port) + while not reader.at_eof(): + line = await reader.readline() + if line: + yield json.loads(line.decode()) + except ( + ConnectionRefusedError, + asyncio.IncompleteReadError, + json.JSONDecodeError, + ) as e: + raise SubscriptionError(f"Failed to read from TCP stream: {e}") from e + + if not self.reconnect: + break + + # Wait before attempting to reconnect + await asyncio.sleep(1) diff --git a/antares-python/src/antares/config.py b/antares-python/src/antares/config.py new file mode 100644 index 0000000..e39d761 --- /dev/null +++ b/antares-python/src/antares/config.py @@ -0,0 +1,18 @@ +from pydantic import BaseSettings, Field + + +class AntaresSettings(BaseSettings): + """ + Application-level configuration for the Antares Python client. + Supports environment variables and `.env` file loading. + """ + + base_url: str = Field(..., env="ANTARES_BASE_URL") + tcp_host: str = Field("localhost", env="ANTARES_TCP_HOST") + tcp_port: int = Field(9000, env="ANTARES_TCP_PORT") + timeout: float = Field(5.0, env="ANTARES_TIMEOUT") + auth_token: str | None = Field(None, env="ANTARES_AUTH_TOKEN") + + class Config: + env_file = ".env" + case_sensitive = False diff --git a/antares-python/src/antares/config_loader.py b/antares-python/src/antares/config_loader.py new file mode 100644 index 0000000..5adf0f1 --- /dev/null +++ b/antares-python/src/antares/config_loader.py @@ -0,0 +1,20 @@ +from pathlib import Path + +import tomli + +from antares.config import AntaresSettings + + +def load_config(path: str | Path | None = None) -> AntaresSettings: + """Loads AntaresSettings from a TOML config file or defaults to .env + env vars.""" + if path is None: + return AntaresSettings() + + config_path = Path(path).expanduser() + if not config_path.exists(): + raise FileNotFoundError(f"Config file not found: {config_path}") + + with config_path.open("rb") as f: + data = tomli.load(f) + + return AntaresSettings(**data.get("antares", {})) diff --git a/antares-python/src/antares/core.py b/antares-python/src/antares/core.py deleted file mode 100644 index 59c3fd2..0000000 --- a/antares-python/src/antares/core.py +++ /dev/null @@ -1,2 +0,0 @@ -def saludar(nombre: str) -> str: - return f"Hola, {nombre}!" diff --git a/antares-python/src/antares/errors.py b/antares-python/src/antares/errors.py new file mode 100644 index 0000000..9129cb0 --- /dev/null +++ b/antares-python/src/antares/errors.py @@ -0,0 +1,18 @@ +class AntaresError(Exception): + """Base exception for all errors raised by antares-python.""" + + +class ConnectionError(AntaresError): + """Raised when unable to connect to the Antares backend.""" + + +class SimulationError(AntaresError): + """Raised when simulation commands (reset/add_ship) fail.""" + + +class SubscriptionError(AntaresError): + """Raised when subscription to TCP stream fails.""" + + +class ShipConfigError(AntaresError): + """Raised when provided ship configuration is invalid or rejected.""" diff --git a/antares-python/src/antares/models/ship.py b/antares-python/src/antares/models/ship.py new file mode 100644 index 0000000..5d95344 --- /dev/null +++ b/antares-python/src/antares/models/ship.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel, Field + + +class ShipConfig(BaseModel): + """ + Defines the configuration for a ship to be added to the simulation. + """ + + initial_position: tuple[float, float] = Field( + ..., description="Initial (x, y) coordinates of the ship in simulation space." + ) diff --git a/antares-python/tests/client/test_client.py b/antares-python/tests/client/test_client.py new file mode 100644 index 0000000..fbc8cb0 --- /dev/null +++ b/antares-python/tests/client/test_client.py @@ -0,0 +1,30 @@ +import pytest + +from antares.client import AntaresClient +from antares.models.ship import ShipConfig + + +def test_reset_simulation_delegates(mocker): + mock_reset = mocker.patch("antares.client.rest.RestClient.reset_simulation") + client = AntaresClient(base_url="http://localhost") + client.reset_simulation() + mock_reset.assert_called_once() + + +def test_add_ship_delegates(mocker): + mock_add = mocker.patch("antares.client.rest.RestClient.add_ship") + client = AntaresClient(base_url="http://localhost") + ship = ShipConfig(initial_position=(1.0, 2.0)) + client.add_ship(ship) + mock_add.assert_called_once_with(ship) + + +@pytest.mark.asyncio +async def test_subscribe_delegates(monkeypatch): + async def fake_subscribe(): + yield {"event": "test"} + + monkeypatch.setattr("antares.client.tcp.TCPSubscriber.subscribe", fake_subscribe) + client = AntaresClient(base_url="http://localhost") + result = [event async for event in client.subscribe()] + assert result == [{"event": "test"}] diff --git a/antares-python/tests/client/test_rest.py b/antares-python/tests/client/test_rest.py new file mode 100644 index 0000000..1278825 --- /dev/null +++ b/antares-python/tests/client/test_rest.py @@ -0,0 +1,36 @@ +import httpx +import pytest + +from antares.client.rest import RestClient +from antares.errors import ConnectionError, SimulationError +from antares.models.ship import ShipConfig + + +def test_reset_simulation_success(mocker): + mock_post = mocker.patch("httpx.post", return_value=httpx.Response(200)) + client = RestClient(base_url="http://localhost") + client.reset_simulation() + mock_post.assert_called_once() + + +def test_reset_simulation_failure(mocker): + mocker.patch("httpx.post", side_effect=httpx.ConnectTimeout("timeout")) + client = RestClient(base_url="http://localhost") + with pytest.raises(ConnectionError): + client.reset_simulation() + + +def test_add_ship_success(mocker): + mock_post = mocker.patch("httpx.post", return_value=httpx.Response(200)) + ship = ShipConfig(initial_position=(0, 0)) + client = RestClient(base_url="http://localhost") + client.add_ship(ship) + mock_post.assert_called_once() + + +def test_add_ship_invalid_response(mocker): + mocker.patch("httpx.post", return_value=httpx.Response(400, content=b"bad request")) + ship = ShipConfig(initial_position=(0, 0)) + client = RestClient(base_url="http://localhost") + with pytest.raises(SimulationError): + client.add_ship(ship) diff --git a/antares-python/tests/client/test_tcp.py b/antares-python/tests/client/test_tcp.py new file mode 100644 index 0000000..b85526c --- /dev/null +++ b/antares-python/tests/client/test_tcp.py @@ -0,0 +1,35 @@ +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from antares.client.tcp import TCPSubscriber +from antares.errors import SubscriptionError + + +@pytest.mark.asyncio +async def test_subscribe_success(monkeypatch): + fake_reader = AsyncMock() + fake_reader.readline = AsyncMock( + side_effect=[b'{"event": "ok"}\n', b'{"event": "done"}\n', b""] + ) + fake_reader.at_eof = MagicMock(return_value=False) + + monkeypatch.setattr( + "asyncio.open_connection", AsyncMock(return_value=(fake_reader, None)) + ) + + subscriber = TCPSubscriber("localhost", 1234, reconnect=False) + events = [event async for event in subscriber.subscribe()] + assert events == [{"event": "ok"}, {"event": "done"}] + + +@pytest.mark.asyncio +async def test_subscribe_failure(monkeypatch): + monkeypatch.setattr( + "asyncio.open_connection", AsyncMock(side_effect=ConnectionRefusedError()) + ) + + subscriber = TCPSubscriber("localhost", 1234, reconnect=False) + with pytest.raises(SubscriptionError): + async for _ in subscriber.subscribe(): + pass diff --git a/antares-python/tests/models/test_ship.py b/antares-python/tests/models/test_ship.py new file mode 100644 index 0000000..acf85b8 --- /dev/null +++ b/antares-python/tests/models/test_ship.py @@ -0,0 +1,6 @@ +from antares.models.ship import ShipConfig + + +def test_ship_config_validation(): + ship = ShipConfig(initial_position=(10.0, 20.0)) + assert ship.initial_position == (10.0, 20.0) diff --git a/antares-python/tests/test_cli.py b/antares-python/tests/test_cli.py new file mode 100644 index 0000000..831fbd7 --- /dev/null +++ b/antares-python/tests/test_cli.py @@ -0,0 +1,49 @@ +import pytest +from typer.testing import CliRunner + +from antares.cli import app + +runner = CliRunner() + + +@pytest.fixture +def fake_config(tmp_path): + config_file = tmp_path / "config.toml" + config_file.write_text(""" +[antares] +base_url = "http://test.local" +tcp_host = "127.0.0.1" +tcp_port = 9001 +timeout = 2.0 +auth_token = "fake-token" +""") + return str(config_file) + + +def test_cli_reset(mocker, fake_config): + mock_reset = mocker.patch("antares.client.rest.RestClient.reset_simulation") + result = runner.invoke(app, ["reset", "--config", fake_config]) + assert result.exit_code == 0 + assert "Simulation reset" in result.output + mock_reset.assert_called_once() + + +def test_cli_add_ship(mocker, fake_config): + mock_add = mocker.patch("antares.client.rest.RestClient.add_ship") + result = runner.invoke( + app, ["add-ship", "--x", "5.0", "--y", "6.0", "--config", fake_config] + ) + assert result.exit_code == 0 + assert "Added ship at (5.0, 6.0)" in result.output + mock_add.assert_called_once() + + +@pytest.mark.asyncio +async def test_cli_subscribe(monkeypatch, fake_config): + async def fake_sub(): + yield {"event": "test-event"} + + monkeypatch.setattr("antares.client.tcp.TCPSubscriber.subscribe", fake_sub) + result = runner.invoke(app, ["subscribe", "--config", fake_config]) + assert result.exit_code == 0 + assert "test-event" in result.output diff --git a/antares-python/tests/test_core.py b/antares-python/tests/test_core.py deleted file mode 100644 index 582ca21..0000000 --- a/antares-python/tests/test_core.py +++ /dev/null @@ -1,5 +0,0 @@ -from antares import saludar - - -def test_saludar(): - assert saludar("Luna") == "Hola, Luna!" From 565aab3c5fdef14d2653bf5529809031658433a5 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Urrea <68788933+jsurrea@users.noreply.github.com> Date: Fri, 11 Apr 2025 14:38:19 -0500 Subject: [PATCH 08/17] Improve CLI logging --- antares-python/src/antares/cli.py | 129 +++++++++++++++++---------- antares-python/src/antares/logger.py | 17 ++++ 2 files changed, 98 insertions(+), 48 deletions(-) create mode 100644 antares-python/src/antares/logger.py diff --git a/antares-python/src/antares/cli.py b/antares-python/src/antares/cli.py index 2baecc9..34636da 100644 --- a/antares-python/src/antares/cli.py +++ b/antares-python/src/antares/cli.py @@ -1,79 +1,112 @@ +import sys import asyncio - +import json import typer +import logging from rich.console import Console from rich.theme import Theme -from antares import AntaresClient, ShipConfig +from antares import ShipConfig, AntaresClient from antares.config_loader import load_config - -app = typer.Typer(help="Antares Simulation CLI") - -console = Console( - theme=Theme( - { - "info": "green", - "warn": "yellow", - "error": "bold red", - } - ) +from antares.errors import ( + ConnectionError, + SimulationError, + SubscriptionError ) - - -def build_client(config_path: str | None, verbose: bool) -> AntaresClient: - settings = load_config(config_path) - - if verbose: - console.print(f"[info]Using settings: {settings.model_dump()}") - - return AntaresClient( - base_url=settings.base_url, - tcp_host=settings.tcp_host, - tcp_port=settings.tcp_port, - timeout=settings.timeout, - auth_token=settings.auth_token, - ) +from antares.logger import setup_logging + +app = typer.Typer(name="antares", help="Antares CLI for ship simulation") +console = Console(theme=Theme({ + "info": "green", + "warn": "yellow", + "error": "bold red" +})) + + +def handle_error(message: str, code: int, json_output: bool = False): + logger = logging.getLogger("antares.cli") + if json_output: + typer.echo(json.dumps({"error": message}), err=True) + else: + console.print(f"[error]{message}") + logger.error("Exiting with error: %s", message) + raise typer.Exit(code) + + +def build_client(config_path: str | None, verbose: bool, json_output: bool) -> AntaresClient: + setup_logging(level=logging.DEBUG if verbose else logging.INFO) + logger = logging.getLogger("antares.cli") + + try: + settings = load_config(config_path) + if verbose: + console.print(f"[info]Using settings: {settings.model_dump()}") + logger.debug("Loaded settings: %s", settings.model_dump()) + return AntaresClient( + base_url=settings.base_url, + tcp_host=settings.tcp_host, + tcp_port=settings.tcp_port, + timeout=settings.timeout, + auth_token=settings.auth_token, + ) + except Exception as e: + handle_error(f"Failed to load configuration: {e}", code=1, json_output=json_output) @app.command() def reset( - config: str = typer.Option(None, help="Path to config TOML file"), + config: str = typer.Option(None), verbose: bool = typer.Option(False, "--verbose", "-v"), + json_output: bool = typer.Option(False, "--json", help="Output in JSON format") ): - """Reset the simulation.""" - client = build_client(config, verbose) - client.reset_simulation() - console.print("[info]✅ Simulation reset.") + client = build_client(config, verbose, json_output) + try: + client.reset_simulation() + msg = "✅ Simulation reset." + typer.echo(json.dumps({"message": msg}) if json_output else msg) + except (ConnectionError, SimulationError) as e: + handle_error(str(e), code=2, json_output=json_output) @app.command() def add_ship( x: float, y: float, - config: str = typer.Option(None, help="Path to config TOML file"), + config: str = typer.Option(None), verbose: bool = typer.Option(False, "--verbose", "-v"), + json_output: bool = typer.Option(False, "--json", help="Output in JSON format") ): - """Add a ship to the simulation at (x, y).""" - client = build_client(config, verbose) - ship = ShipConfig(initial_position=(x, y)) - client.add_ship(ship) - console.print(f"[info]🚢 Added ship at ({x}, {y})") + client = build_client(config, verbose, json_output) + try: + ship = ShipConfig(initial_position=(x, y)) + client.add_ship(ship) + msg = f"🚢 Added ship at ({x}, {y})" + typer.echo(json.dumps({"message": msg}) if json_output else msg) + except (ConnectionError, SimulationError) as e: + handle_error(str(e), code=2, json_output=json_output) @app.command() def subscribe( - config: str = typer.Option(None, help="Path to config TOML file"), + config: str = typer.Option(None), verbose: bool = typer.Option(False, "--verbose", "-v"), + json_output: bool = typer.Option(False, "--json", help="Output in JSON format"), + log_file: str = typer.Option("antares.log", help="Path to log file") ): - """Subscribe to simulation data stream.""" - client = build_client(config, verbose) + setup_logging(log_file=log_file, level=logging.DEBUG if verbose else logging.INFO) + logger = logging.getLogger("antares.cli") + + client = build_client(config, verbose, json_output) async def _sub(): - async for event in client.subscribe(): - console.print_json(data=event) + try: + async for event in client.subscribe(): + if json_output: + typer.echo(json.dumps(event)) + else: + console.print_json(data=event) + logger.debug("Received event: %s", event) + except SubscriptionError as e: + handle_error(str(e), code=3, json_output=json_output) asyncio.run(_sub()) - - -if __name__ == "__main__": - app() diff --git a/antares-python/src/antares/logger.py b/antares-python/src/antares/logger.py new file mode 100644 index 0000000..55fb139 --- /dev/null +++ b/antares-python/src/antares/logger.py @@ -0,0 +1,17 @@ +import logging +from pathlib import Path +from rich.logging import RichHandler + +def setup_logging(log_file: str = "antares.log", level: int = logging.INFO) -> None: + """Configure logging to both rich console and a file.""" + Path(log_file).parent.mkdir(parents=True, exist_ok=True) + + logging.basicConfig( + level=level, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="[%Y-%m-%d %H:%M:%S]", + handlers=[ + RichHandler(rich_tracebacks=True, show_path=False), + logging.FileHandler(log_file, encoding="utf-8") + ] + ) From 2ad5cadf9123d1362f49c8fd139224f641c79582 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Urrea <68788933+jsurrea@users.noreply.github.com> Date: Fri, 11 Apr 2025 15:50:48 -0500 Subject: [PATCH 09/17] improve tests coverage --- antares-python/antares.log | 0 antares-python/pyproject.toml | 8 ++++- antares-python/src/antares/cli.py | 34 +++++++------------ antares-python/src/antares/client/__init__.py | 21 ++++++------ antares-python/src/antares/client/rest.py | 4 +-- antares-python/src/antares/config.py | 20 ++++++----- antares-python/src/antares/logger.py | 6 ++-- antares-python/tests/client/test_client.py | 2 +- antares-python/tests/client/test_rest.py | 16 +++++++-- antares-python/tests/client/test_tcp.py | 25 ++++++++------ antares-python/tests/test_cli.py | 15 ++++---- 11 files changed, 85 insertions(+), 66 deletions(-) create mode 100644 antares-python/antares.log diff --git a/antares-python/antares.log b/antares-python/antares.log new file mode 100644 index 0000000..e69de29 diff --git a/antares-python/pyproject.toml b/antares-python/pyproject.toml index 0194158..83c269e 100644 --- a/antares-python/pyproject.toml +++ b/antares-python/pyproject.toml @@ -9,12 +9,16 @@ readme = "README.md" requires-python = ">=3.13" dependencies = [ "pydantic>=2.11.3", + "pydantic-settings>=2.8.1", "httpx>=0.28.1", "typer>=0.15.2", "rich>=13.0", "tomli>=2.2.1", ] +[project.optional-dependencies] +dev = ["pytest-asyncio>=0.26.0"] + [project.scripts] antares = "antares.cli:app" @@ -34,6 +38,8 @@ files = ["src"] [tool.pytest.ini_options] pythonpath = "src" +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" addopts = "-ra -q -ra -q --cov=src --cov-report=term-missing" [tool.setuptools.packages.find] @@ -47,5 +53,5 @@ test = "pytest" coverage = "pytest --cov=src --cov-report=term-missing" build = "python -m build" publish = "twine upload dist/* --repository antares-python" -check = "task lint && task typecheck && task test" +check = "task lint && task format && task typecheck && task test" release = "task check && task build && task publish" diff --git a/antares-python/src/antares/cli.py b/antares-python/src/antares/cli.py index 34636da..7070b16 100644 --- a/antares-python/src/antares/cli.py +++ b/antares-python/src/antares/cli.py @@ -1,26 +1,18 @@ -import sys import asyncio import json -import typer import logging + +import typer from rich.console import Console from rich.theme import Theme -from antares import ShipConfig, AntaresClient +from antares import AntaresClient, ShipConfig from antares.config_loader import load_config -from antares.errors import ( - ConnectionError, - SimulationError, - SubscriptionError -) +from antares.errors import ConnectionError, SimulationError, SubscriptionError from antares.logger import setup_logging -app = typer.Typer(name="antares", help="Antares CLI for ship simulation") -console = Console(theme=Theme({ - "info": "green", - "warn": "yellow", - "error": "bold red" -})) +app = typer.Typer(name="antares", help="Antares CLI for ship simulation", no_args_is_help=True) +console = Console(theme=Theme({"info": "green", "warn": "yellow", "error": "bold red"})) def handle_error(message: str, code: int, json_output: bool = False): @@ -57,7 +49,7 @@ def build_client(config_path: str | None, verbose: bool, json_output: bool) -> A def reset( config: str = typer.Option(None), verbose: bool = typer.Option(False, "--verbose", "-v"), - json_output: bool = typer.Option(False, "--json", help="Output in JSON format") + json_output: bool = typer.Option(False, "--json", help="Output in JSON format"), ): client = build_client(config, verbose, json_output) try: @@ -70,11 +62,11 @@ def reset( @app.command() def add_ship( - x: float, - y: float, - config: str = typer.Option(None), - verbose: bool = typer.Option(False, "--verbose", "-v"), - json_output: bool = typer.Option(False, "--json", help="Output in JSON format") + x: float = typer.Option(..., help="X coordinate of the ship"), + y: float = typer.Option(..., help="Y coordinate of the ship"), + config: str = typer.Option(None, help="Path to the configuration file"), + verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output"), + json_output: bool = typer.Option(False, "--json", help="Output in JSON format"), ): client = build_client(config, verbose, json_output) try: @@ -91,7 +83,7 @@ def subscribe( config: str = typer.Option(None), verbose: bool = typer.Option(False, "--verbose", "-v"), json_output: bool = typer.Option(False, "--json", help="Output in JSON format"), - log_file: str = typer.Option("antares.log", help="Path to log file") + log_file: str = typer.Option("antares.log", help="Path to log file"), ): setup_logging(log_file=log_file, level=logging.DEBUG if verbose else logging.INFO) logger = logging.getLogger("antares.cli") diff --git a/antares-python/src/antares/client/__init__.py b/antares-python/src/antares/client/__init__.py index ef646e6..6e1fb1a 100644 --- a/antares-python/src/antares/client/__init__.py +++ b/antares-python/src/antares/client/__init__.py @@ -20,23 +20,24 @@ def __init__( Accepts config overrides directly or falls back to environment-based configuration. """ + overrides = { + "base_url": base_url, + "tcp_host": tcp_host, + "tcp_port": tcp_port, + "timeout": timeout, + "auth_token": auth_token, + } + clean_overrides = {k: v for k, v in overrides.items() if v is not None} + # Merge provided arguments with environment/.env via AntaresSettings - self._settings = AntaresSettings( - base_url=base_url, - tcp_host=tcp_host, - tcp_port=tcp_port, - timeout=timeout, - auth_token=auth_token, - ) + self._settings = AntaresSettings(**clean_overrides) self._rest = RestClient( base_url=self._settings.base_url, timeout=self._settings.timeout, auth_token=self._settings.auth_token, ) - self._tcp = TCPSubscriber( - host=self._settings.tcp_host, port=self._settings.tcp_port - ) + self._tcp = TCPSubscriber(host=self._settings.tcp_host, port=self._settings.tcp_port) def reset_simulation(self) -> None: """ diff --git a/antares-python/src/antares/client/rest.py b/antares-python/src/antares/client/rest.py index f7dd9c2..1a8d423 100644 --- a/antares-python/src/antares/client/rest.py +++ b/antares-python/src/antares/client/rest.py @@ -9,9 +9,7 @@ class RestClient: Internal client for interacting with the Antares simulation REST API. """ - def __init__( - self, base_url: str, timeout: float = 5.0, auth_token: str | None = None - ) -> None: + def __init__(self, base_url: str, timeout: float = 5.0, auth_token: str | None = None) -> None: """ Initializes the REST client. diff --git a/antares-python/src/antares/config.py b/antares-python/src/antares/config.py index e39d761..e504dbd 100644 --- a/antares-python/src/antares/config.py +++ b/antares-python/src/antares/config.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings, Field +from pydantic_settings import BaseSettings, SettingsConfigDict class AntaresSettings(BaseSettings): @@ -7,12 +7,14 @@ class AntaresSettings(BaseSettings): Supports environment variables and `.env` file loading. """ - base_url: str = Field(..., env="ANTARES_BASE_URL") - tcp_host: str = Field("localhost", env="ANTARES_TCP_HOST") - tcp_port: int = Field(9000, env="ANTARES_TCP_PORT") - timeout: float = Field(5.0, env="ANTARES_TIMEOUT") - auth_token: str | None = Field(None, env="ANTARES_AUTH_TOKEN") + base_url: str + tcp_host: str = "localhost" + tcp_port: int = 9000 + timeout: float = 5.0 + auth_token: str | None = None - class Config: - env_file = ".env" - case_sensitive = False + model_config = SettingsConfigDict( + env_file=".env", + env_prefix="ANTARES_", + case_sensitive=False, + ) diff --git a/antares-python/src/antares/logger.py b/antares-python/src/antares/logger.py index 55fb139..f74abff 100644 --- a/antares-python/src/antares/logger.py +++ b/antares-python/src/antares/logger.py @@ -1,7 +1,9 @@ import logging from pathlib import Path + from rich.logging import RichHandler + def setup_logging(log_file: str = "antares.log", level: int = logging.INFO) -> None: """Configure logging to both rich console and a file.""" Path(log_file).parent.mkdir(parents=True, exist_ok=True) @@ -12,6 +14,6 @@ def setup_logging(log_file: str = "antares.log", level: int = logging.INFO) -> N datefmt="[%Y-%m-%d %H:%M:%S]", handlers=[ RichHandler(rich_tracebacks=True, show_path=False), - logging.FileHandler(log_file, encoding="utf-8") - ] + logging.FileHandler(log_file, encoding="utf-8"), + ], ) diff --git a/antares-python/tests/client/test_client.py b/antares-python/tests/client/test_client.py index fbc8cb0..d4ff373 100644 --- a/antares-python/tests/client/test_client.py +++ b/antares-python/tests/client/test_client.py @@ -21,7 +21,7 @@ def test_add_ship_delegates(mocker): @pytest.mark.asyncio async def test_subscribe_delegates(monkeypatch): - async def fake_subscribe(): + async def fake_subscribe(_self): yield {"event": "test"} monkeypatch.setattr("antares.client.tcp.TCPSubscriber.subscribe", fake_subscribe) diff --git a/antares-python/tests/client/test_rest.py b/antares-python/tests/client/test_rest.py index 1278825..c0b1bed 100644 --- a/antares-python/tests/client/test_rest.py +++ b/antares-python/tests/client/test_rest.py @@ -7,7 +7,8 @@ def test_reset_simulation_success(mocker): - mock_post = mocker.patch("httpx.post", return_value=httpx.Response(200)) + mock_request = httpx.Request("POST", "http://localhost/simulation/reset") + mock_post = mocker.patch("httpx.post", return_value=httpx.Response(200, request=mock_request)) client = RestClient(base_url="http://localhost") client.reset_simulation() mock_post.assert_called_once() @@ -21,7 +22,8 @@ def test_reset_simulation_failure(mocker): def test_add_ship_success(mocker): - mock_post = mocker.patch("httpx.post", return_value=httpx.Response(200)) + mock_request = httpx.Request("POST", "http://localhost/simulation/ships") + mock_post = mocker.patch("httpx.post", return_value=httpx.Response(200, request=mock_request)) ship = ShipConfig(initial_position=(0, 0)) client = RestClient(base_url="http://localhost") client.add_ship(ship) @@ -29,7 +31,15 @@ def test_add_ship_success(mocker): def test_add_ship_invalid_response(mocker): - mocker.patch("httpx.post", return_value=httpx.Response(400, content=b"bad request")) + mock_request = httpx.Request("POST", "http://localhost/simulation/ships") + mocker.patch( + "httpx.post", + return_value=httpx.Response( + 400, + content=b"bad request", + request=mock_request, + ), + ) ship = ShipConfig(initial_position=(0, 0)) client = RestClient(base_url="http://localhost") with pytest.raises(SimulationError): diff --git a/antares-python/tests/client/test_tcp.py b/antares-python/tests/client/test_tcp.py index b85526c..98d299a 100644 --- a/antares-python/tests/client/test_tcp.py +++ b/antares-python/tests/client/test_tcp.py @@ -8,26 +8,31 @@ @pytest.mark.asyncio async def test_subscribe_success(monkeypatch): + # Simulated lines returned from the TCP stream + lines = [b'{"event": "ok"}\n', b'{"event": "done"}\n', b""] + + async def fake_readline(): + return lines.pop(0) + + # Simulate EOF after all lines are read + eof_flags = [False, False, True] + fake_reader = AsyncMock() - fake_reader.readline = AsyncMock( - side_effect=[b'{"event": "ok"}\n', b'{"event": "done"}\n', b""] - ) - fake_reader.at_eof = MagicMock(return_value=False) + fake_reader.readline = AsyncMock(side_effect=fake_readline) + fake_reader.at_eof = MagicMock(side_effect=eof_flags) - monkeypatch.setattr( - "asyncio.open_connection", AsyncMock(return_value=(fake_reader, None)) - ) + # Patch asyncio.open_connection to return our mocked reader + monkeypatch.setattr("asyncio.open_connection", AsyncMock(return_value=(fake_reader, None))) subscriber = TCPSubscriber("localhost", 1234, reconnect=False) + events = [event async for event in subscriber.subscribe()] assert events == [{"event": "ok"}, {"event": "done"}] @pytest.mark.asyncio async def test_subscribe_failure(monkeypatch): - monkeypatch.setattr( - "asyncio.open_connection", AsyncMock(side_effect=ConnectionRefusedError()) - ) + monkeypatch.setattr("asyncio.open_connection", AsyncMock(side_effect=ConnectionRefusedError())) subscriber = TCPSubscriber("localhost", 1234, reconnect=False) with pytest.raises(SubscriptionError): diff --git a/antares-python/tests/test_cli.py b/antares-python/tests/test_cli.py index 831fbd7..72b83f8 100644 --- a/antares-python/tests/test_cli.py +++ b/antares-python/tests/test_cli.py @@ -1,3 +1,5 @@ +import asyncio + import pytest from typer.testing import CliRunner @@ -30,20 +32,21 @@ def test_cli_reset(mocker, fake_config): def test_cli_add_ship(mocker, fake_config): mock_add = mocker.patch("antares.client.rest.RestClient.add_ship") - result = runner.invoke( - app, ["add-ship", "--x", "5.0", "--y", "6.0", "--config", fake_config] - ) + result = runner.invoke(app, ["add-ship", "--x", "5.0", "--y", "6.0", "--config", fake_config]) assert result.exit_code == 0 assert "Added ship at (5.0, 6.0)" in result.output mock_add.assert_called_once() -@pytest.mark.asyncio -async def test_cli_subscribe(monkeypatch, fake_config): - async def fake_sub(): +def test_cli_subscribe(monkeypatch, mocker, fake_config): + async def fake_sub(self): yield {"event": "test-event"} monkeypatch.setattr("antares.client.tcp.TCPSubscriber.subscribe", fake_sub) + + # Use a fresh event loop + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) result = runner.invoke(app, ["subscribe", "--config", fake_config]) assert result.exit_code == 0 assert "test-event" in result.output From fa7c7ceff002d93a6fd9eadbe9e8537e09030c3f Mon Sep 17 00:00:00 2001 From: Juan Sebastian Urrea <68788933+jsurrea@users.noreply.github.com> Date: Fri, 11 Apr 2025 18:49:43 -0500 Subject: [PATCH 10/17] reach 100% coverage --- antares-python/src/antares/client/tcp.py | 10 ++- antares-python/tests/client/test_rest.py | 38 +++++++++++ antares-python/tests/client/test_tcp.py | 36 ++++++++++ antares-python/tests/test_cli.py | 84 ++++++++++++++++++++++++ 4 files changed, 166 insertions(+), 2 deletions(-) diff --git a/antares-python/src/antares/client/tcp.py b/antares-python/src/antares/client/tcp.py index 3be0e44..5c9f741 100644 --- a/antares-python/src/antares/client/tcp.py +++ b/antares-python/src/antares/client/tcp.py @@ -1,9 +1,12 @@ import asyncio import json +import logging from collections.abc import AsyncIterator from antares.errors import SubscriptionError +logger = logging.getLogger(__name__) + class TCPSubscriber: """ @@ -43,10 +46,13 @@ async def subscribe(self) -> AsyncIterator[dict]: asyncio.IncompleteReadError, json.JSONDecodeError, ) as e: - raise SubscriptionError(f"Failed to read from TCP stream: {e}") from e + logger.error("TCP stream error: %s", e) + if not self.reconnect: + raise SubscriptionError(f"Failed to read from TCP stream: {e}") from e + # Stop if not reconnecting if not self.reconnect: break - # Wait before attempting to reconnect + logger.info("Waiting 1 second before retrying TCP connection...") await asyncio.sleep(1) diff --git a/antares-python/tests/client/test_rest.py b/antares-python/tests/client/test_rest.py index c0b1bed..860305c 100644 --- a/antares-python/tests/client/test_rest.py +++ b/antares-python/tests/client/test_rest.py @@ -44,3 +44,41 @@ def test_add_ship_invalid_response(mocker): client = RestClient(base_url="http://localhost") with pytest.raises(SimulationError): client.add_ship(ship) + + +def test_reset_simulation_http_error(mocker): + request = httpx.Request("POST", "http://localhost/simulation/reset") + response = httpx.Response(500, content=b"internal error", request=request) + + mock_post = mocker.patch("httpx.post", return_value=response) + + # .raise_for_status() triggers HTTPStatusError + def raise_error(): + raise httpx.HTTPStatusError("error", request=request, response=response) + + response.raise_for_status = raise_error + + client = RestClient(base_url="http://localhost") + + with pytest.raises(SimulationError) as exc: + client.reset_simulation() + + assert "Reset failed" in str(exc.value) + mock_post.assert_called_once() + + +def test_add_ship_request_error(mocker): + mocker.patch( + "httpx.post", + side_effect=httpx.RequestError( + "connection dropped", request=httpx.Request("POST", "http://localhost/simulation/ships") + ), + ) + + ship = ShipConfig(initial_position=(0, 0)) + client = RestClient(base_url="http://localhost") + + with pytest.raises(ConnectionError) as exc: + client.add_ship(ship) + + assert "Could not reach Antares API" in str(exc.value) diff --git a/antares-python/tests/client/test_tcp.py b/antares-python/tests/client/test_tcp.py index 98d299a..44474cf 100644 --- a/antares-python/tests/client/test_tcp.py +++ b/antares-python/tests/client/test_tcp.py @@ -38,3 +38,39 @@ async def test_subscribe_failure(monkeypatch): with pytest.raises(SubscriptionError): async for _ in subscriber.subscribe(): pass + + +@pytest.mark.asyncio +async def test_subscribe_reconnects_on_failure(monkeypatch): + class OneMessageReader: + def __init__(self): + self.called = False + + def at_eof(self): + return self.called + + async def readline(self): + if not self.called: + self.called = True + return b'{"event": "recovered"}\n' + return b"" + + open_calls = [] + + async def fake_open_connection(host, port): + if not open_calls: + open_calls.append("fail") + raise ConnectionRefusedError("initial fail") + return OneMessageReader(), None + + monkeypatch.setattr("asyncio.open_connection", fake_open_connection) + monkeypatch.setattr("asyncio.sleep", AsyncMock()) + + subscriber = TCPSubscriber("localhost", 1234, reconnect=True) + + events = [] + async for event in subscriber.subscribe(): + events.append(event) + break # Exit after one event + + assert events == [{"event": "recovered"}] diff --git a/antares-python/tests/test_cli.py b/antares-python/tests/test_cli.py index 72b83f8..56d7b70 100644 --- a/antares-python/tests/test_cli.py +++ b/antares-python/tests/test_cli.py @@ -4,6 +4,7 @@ from typer.testing import CliRunner from antares.cli import app +from antares.errors import ConnectionError, SimulationError, SubscriptionError runner = CliRunner() @@ -50,3 +51,86 @@ async def fake_sub(self): result = runner.invoke(app, ["subscribe", "--config", fake_config]) assert result.exit_code == 0 assert "test-event" in result.output + + +def test_handle_error_json(monkeypatch): + result = runner.invoke(app, ["reset", "--json"], catch_exceptions=False) + assert result.exit_code in {1, 2} + assert "error" in result.output + + +def test_build_client_fails(mocker): + mocker.patch("antares.config_loader.load_config", side_effect=Exception("broken config")) + result = runner.invoke(app, ["reset", "--config", "invalid.toml"]) + assert result.exit_code == 1 + assert "Failed to load configuration" in result.output + + +def test_cli_reset_error_handling(mocker, fake_config): + mocker.patch( + "antares.client.rest.RestClient.reset_simulation", + side_effect=ConnectionError("cannot connect"), + ) + result = runner.invoke(app, ["reset", "--config", fake_config]) + expected_exit_code = 2 + assert result.exit_code == expected_exit_code + assert "cannot connect" in result.output + + +def test_cli_add_ship_error_handling(mocker, fake_config): + mocker.patch( + "antares.client.rest.RestClient.add_ship", side_effect=SimulationError("ship rejected") + ) + result = runner.invoke(app, ["add-ship", "--x", "1", "--y", "2", "--config", fake_config]) + expected_exit_code = 2 + assert result.exit_code == expected_exit_code + assert "ship rejected" in result.output + + +def test_cli_subscribe_error(monkeypatch, fake_config): + class FailingAsyncGenerator: + def __aiter__(self): + return self + + async def __anext__(self): + raise SubscriptionError("stream failed") + + monkeypatch.setattr( + "antares.client.tcp.TCPSubscriber.subscribe", lambda self: FailingAsyncGenerator() + ) + + result = runner.invoke(app, ["subscribe", "--config", fake_config]) + expected_exit_code = 3 + assert result.exit_code == expected_exit_code + assert "stream failed" in result.output + + +def test_cli_verbose_prints_config(mocker, fake_config): + mocker.patch("antares.client.tcp.TCPSubscriber.subscribe", return_value=iter([])) + mocker.patch("antares.client.rest.RestClient.reset_simulation") + + result = runner.invoke(app, ["reset", "--config", fake_config, "--verbose"]) + assert result.exit_code == 0 + assert "Using settings" in result.output + + +def test_cli_subscribe_json(monkeypatch, fake_config): + class OneEventGen: + def __init__(self): + self.done = False + + def __aiter__(self): + return self + + async def __anext__(self): + if not self.done: + self.done = True + return {"event": "test"} + raise StopAsyncIteration + + monkeypatch.setattr("antares.client.tcp.TCPSubscriber.subscribe", lambda self: OneEventGen()) + + result = runner.invoke(app, ["subscribe", "--config", fake_config, "--json"]) + + assert result.exit_code == 0 + assert '{"event": "test"}' in result.output From 67f1a30b5e65b0c8772ea22856a92cd6981af599 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Urrea <68788933+jsurrea@users.noreply.github.com> Date: Fri, 11 Apr 2025 19:53:26 -0500 Subject: [PATCH 11/17] add tests for track model --- antares-python/src/antares/cli.py | 11 +-- antares-python/src/antares/client/__init__.py | 25 ++---- antares-python/src/antares/client/tcp.py | 11 ++- antares-python/src/antares/config.py | 4 +- antares-python/src/antares/models/track.py | 84 +++++++++++++++++++ antares-python/template.env | 1 + antares-python/tests/client/test_tcp.py | 84 ++++++++++++++++--- antares-python/tests/conftest.py | 34 ++++++++ antares-python/tests/models/test_ship.py | 6 -- 9 files changed, 217 insertions(+), 43 deletions(-) create mode 100644 antares-python/src/antares/models/track.py create mode 100644 antares-python/template.env create mode 100644 antares-python/tests/conftest.py delete mode 100644 antares-python/tests/models/test_ship.py diff --git a/antares-python/src/antares/cli.py b/antares-python/src/antares/cli.py index 7070b16..58f02b3 100644 --- a/antares-python/src/antares/cli.py +++ b/antares-python/src/antares/cli.py @@ -1,6 +1,7 @@ import asyncio import json import logging +from typing import NoReturn import typer from rich.console import Console @@ -15,7 +16,7 @@ console = Console(theme=Theme({"info": "green", "warn": "yellow", "error": "bold red"})) -def handle_error(message: str, code: int, json_output: bool = False): +def handle_error(message: str, code: int, json_output: bool = False) -> NoReturn: logger = logging.getLogger("antares.cli") if json_output: typer.echo(json.dumps({"error": message}), err=True) @@ -50,7 +51,7 @@ def reset( config: str = typer.Option(None), verbose: bool = typer.Option(False, "--verbose", "-v"), json_output: bool = typer.Option(False, "--json", help="Output in JSON format"), -): +) -> None: client = build_client(config, verbose, json_output) try: client.reset_simulation() @@ -67,7 +68,7 @@ def add_ship( config: str = typer.Option(None, help="Path to the configuration file"), verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output"), json_output: bool = typer.Option(False, "--json", help="Output in JSON format"), -): +) -> None: client = build_client(config, verbose, json_output) try: ship = ShipConfig(initial_position=(x, y)) @@ -84,13 +85,13 @@ def subscribe( verbose: bool = typer.Option(False, "--verbose", "-v"), json_output: bool = typer.Option(False, "--json", help="Output in JSON format"), log_file: str = typer.Option("antares.log", help="Path to log file"), -): +) -> None: setup_logging(log_file=log_file, level=logging.DEBUG if verbose else logging.INFO) logger = logging.getLogger("antares.cli") client = build_client(config, verbose, json_output) - async def _sub(): + async def _sub() -> None: try: async for event in client.subscribe(): if json_output: diff --git a/antares-python/src/antares/client/__init__.py b/antares-python/src/antares/client/__init__.py index 6e1fb1a..88691a1 100644 --- a/antares-python/src/antares/client/__init__.py +++ b/antares-python/src/antares/client/__init__.py @@ -1,36 +1,29 @@ from collections.abc import AsyncIterator +from typing import Any from antares.client.rest import RestClient from antares.client.tcp import TCPSubscriber from antares.config import AntaresSettings from antares.models.ship import ShipConfig +from antares.models.track import Track class AntaresClient: def __init__( self, - base_url: str | None = None, - tcp_host: str | None = None, - tcp_port: int | None = None, - timeout: float | None = None, - auth_token: str | None = None, + **kwargs: Any, ) -> None: """ Public interface for interacting with the Antares simulation engine. Accepts config overrides directly or falls back to environment-based configuration. """ - overrides = { - "base_url": base_url, - "tcp_host": tcp_host, - "tcp_port": tcp_port, - "timeout": timeout, - "auth_token": auth_token, - } - clean_overrides = {k: v for k, v in overrides.items() if v is not None} + # Only include kwargs that match AntaresSettings fields + valid_fields = AntaresSettings.model_fields.keys() + filtered_kwargs = {k: v for k, v in kwargs.items() if k in valid_fields and v is not None} # Merge provided arguments with environment/.env via AntaresSettings - self._settings = AntaresSettings(**clean_overrides) + self._settings = AntaresSettings(**filtered_kwargs) self._rest = RestClient( base_url=self._settings.base_url, @@ -51,12 +44,12 @@ def add_ship(self, ship: ShipConfig) -> None: """ return self._rest.add_ship(ship) - async def subscribe(self) -> AsyncIterator[dict]: + async def subscribe(self) -> AsyncIterator[Track]: """ Subscribes to live simulation data over TCP. Yields: - Parsed simulation event data as dictionaries. + Parsed simulation event data as Track objects. """ async for event in self._tcp.subscribe(): yield event diff --git a/antares-python/src/antares/client/tcp.py b/antares-python/src/antares/client/tcp.py index 5c9f741..b3a6494 100644 --- a/antares-python/src/antares/client/tcp.py +++ b/antares-python/src/antares/client/tcp.py @@ -4,6 +4,7 @@ from collections.abc import AsyncIterator from antares.errors import SubscriptionError +from antares.models.track import Track logger = logging.getLogger(__name__) @@ -26,13 +27,13 @@ def __init__(self, host: str, port: int, reconnect: bool = True) -> None: self.port = port self.reconnect = reconnect - async def subscribe(self) -> AsyncIterator[dict]: + async def subscribe(self) -> AsyncIterator[Track]: """ - Connects to the TCP server and yields simulation events as parsed dictionaries. + Connects to the TCP server and yields simulation events as Track objects. This is an infinite async generator until disconnected or cancelled. Yields: - Parsed simulation events. + Parsed simulation events as Track objects. """ while True: try: @@ -40,11 +41,13 @@ async def subscribe(self) -> AsyncIterator[dict]: while not reader.at_eof(): line = await reader.readline() if line: - yield json.loads(line.decode()) + track = Track.from_csv_row(line.decode()) + yield track except ( ConnectionRefusedError, asyncio.IncompleteReadError, json.JSONDecodeError, + ValueError, ) as e: logger.error("TCP stream error: %s", e) if not self.reconnect: diff --git a/antares-python/src/antares/config.py b/antares-python/src/antares/config.py index e504dbd..9634ce1 100644 --- a/antares-python/src/antares/config.py +++ b/antares-python/src/antares/config.py @@ -7,7 +7,7 @@ class AntaresSettings(BaseSettings): Supports environment variables and `.env` file loading. """ - base_url: str + base_url: str = "http://localhost:8000" tcp_host: str = "localhost" tcp_port: int = 9000 timeout: float = 5.0 @@ -15,6 +15,6 @@ class AntaresSettings(BaseSettings): model_config = SettingsConfigDict( env_file=".env", - env_prefix="ANTARES_", + env_prefix="antares_", case_sensitive=False, ) diff --git a/antares-python/src/antares/models/track.py b/antares-python/src/antares/models/track.py new file mode 100644 index 0000000..f740cd3 --- /dev/null +++ b/antares-python/src/antares/models/track.py @@ -0,0 +1,84 @@ +from typing import ClassVar + +from pydantic import BaseModel, Field + + +class Track(BaseModel): + id: int + year: int + month: int + day: int + hour: int + minute: int + second: int + millisecond: int + stat: str + type_: str = Field(alias="type") # maps from "type" input + name: str + linemask: int + size: int + range: float + azimuth: float + lat: float + long: float + speed: float + course: float + quality: int + l16quality: int + lacks: int + winrgw: int + winazw: float + stderr: float + + # expected order of fields from TCP stream + __field_order__: ClassVar[list[str]] = [ + "id", + "year", + "month", + "day", + "hour", + "minute", + "second", + "millisecond", + "stat", + "type_", + "name", + "linemask", + "size", + "range", + "azimuth", + "lat", + "long", + "speed", + "course", + "quality", + "l16quality", + "lacks", + "winrgw", + "winazw", + "stderr", + ] + + @classmethod + def from_csv_row(cls, line: str) -> "Track": + parts = line.strip().split(",") + if len(parts) != len(cls.__field_order__): + raise ValueError(f"Expected {len(cls.__field_order__)} fields, got {len(parts)}") + + converted = {} + for field_name, value in zip(cls.__field_order__, parts, strict=True): + field_info = cls.model_fields[field_name] + field_type = field_info.annotation + + if field_type is None: + raise ValueError(f"Field '{field_name}' has no type annotation") + + # Use alias if defined + key = field_info.alias or field_name + try: + # We trust simple coercion here; Pydantic will do final validation + converted[key] = field_type(value) + except Exception as e: + raise ValueError(f"Invalid value for field '{field_name}': {value} ({e})") from e + + return cls(**converted) diff --git a/antares-python/template.env b/antares-python/template.env new file mode 100644 index 0000000..5734a16 --- /dev/null +++ b/antares-python/template.env @@ -0,0 +1 @@ +ANTARES_BASE_URL = "http://localhost:8080" diff --git a/antares-python/tests/client/test_tcp.py b/antares-python/tests/client/test_tcp.py index 44474cf..bde9427 100644 --- a/antares-python/tests/client/test_tcp.py +++ b/antares-python/tests/client/test_tcp.py @@ -4,18 +4,19 @@ from antares.client.tcp import TCPSubscriber from antares.errors import SubscriptionError +from antares.models.track import Track @pytest.mark.asyncio -async def test_subscribe_success(monkeypatch): - # Simulated lines returned from the TCP stream - lines = [b'{"event": "ok"}\n', b'{"event": "done"}\n', b""] +async def test_subscribe_success(monkeypatch, sample_track_line): + # Simulated CSV lines returned from the TCP stream + encoded_lines = [sample_track_line.encode() + b"\n", b""] async def fake_readline(): - return lines.pop(0) + return encoded_lines.pop(0) # Simulate EOF after all lines are read - eof_flags = [False, False, True] + eof_flags = [False, True] fake_reader = AsyncMock() fake_reader.readline = AsyncMock(side_effect=fake_readline) @@ -27,7 +28,11 @@ async def fake_readline(): subscriber = TCPSubscriber("localhost", 1234, reconnect=False) events = [event async for event in subscriber.subscribe()] - assert events == [{"event": "ok"}, {"event": "done"}] + expected_lat = -33.45 + assert len(events) == 1 + assert isinstance(events[0], Track) + assert events[0].name == "Eagle-1" + assert events[0].lat == expected_lat @pytest.mark.asyncio @@ -41,7 +46,7 @@ async def test_subscribe_failure(monkeypatch): @pytest.mark.asyncio -async def test_subscribe_reconnects_on_failure(monkeypatch): +async def test_subscribe_reconnects_on_failure(monkeypatch, sample_track_line): class OneMessageReader: def __init__(self): self.called = False @@ -52,7 +57,7 @@ def at_eof(self): async def readline(self): if not self.called: self.called = True - return b'{"event": "recovered"}\n' + return sample_track_line.encode() + b"\n" return b"" open_calls = [] @@ -71,6 +76,65 @@ async def fake_open_connection(host, port): events = [] async for event in subscriber.subscribe(): events.append(event) - break # Exit after one event + break # exit after first track - assert events == [{"event": "recovered"}] + assert len(events) == 1 + assert isinstance(events[0], Track) + assert events[0].name == "Eagle-1" + + +@pytest.mark.asyncio +async def test_subscribe_invalid_field_count(monkeypatch): + invalid_line = "1,2025,4,11" + + async def fake_readline(): + return invalid_line.encode() + b"\n" + + fake_reader = AsyncMock() + fake_reader.readline = AsyncMock(side_effect=fake_readline) + fake_reader.at_eof = MagicMock(side_effect=[False, True]) + + monkeypatch.setattr("asyncio.open_connection", AsyncMock(return_value=(fake_reader, None))) + + subscriber = TCPSubscriber("localhost", 1234, reconnect=False) + + with pytest.raises(SubscriptionError) as excinfo: + async for _ in subscriber.subscribe(): + pass + + assert "Expected 25 fields" in str(excinfo.value) + + +@pytest.mark.asyncio +async def test_subscribe_invalid_value(monkeypatch, sample_track_line): + bad_line = sample_track_line.replace("1,", "bad_id,", 1) + + async def fake_readline(): + return bad_line.encode() + b"\n" + + fake_reader = AsyncMock() + fake_reader.readline = AsyncMock(side_effect=fake_readline) + fake_reader.at_eof = MagicMock(side_effect=[False, True]) + + monkeypatch.setattr("asyncio.open_connection", AsyncMock(return_value=(fake_reader, None))) + + subscriber = TCPSubscriber("localhost", 1234, reconnect=False) + + with pytest.raises(SubscriptionError) as excinfo: + async for _ in subscriber.subscribe(): + pass + + assert "Invalid value for field 'id'" in str(excinfo.value) + + +def test_from_csv_field_type_none(monkeypatch): + class FakeTrack(Track): + __field_order__ = ["id"] + id: int + + FakeTrack.model_fields["id"].annotation = None + + with pytest.raises(ValueError) as excinfo: + FakeTrack.from_csv_row("123") + + assert "has no type annotation" in str(excinfo.value) diff --git a/antares-python/tests/conftest.py b/antares-python/tests/conftest.py new file mode 100644 index 0000000..6d60f0e --- /dev/null +++ b/antares-python/tests/conftest.py @@ -0,0 +1,34 @@ +import pytest + + +@pytest.fixture +def sample_track_line() -> str: + return ",".join( + [ + "1", + "2025", + "4", + "11", + "10", + "30", + "15", + "200", + "OK", + "drone", + "Eagle-1", + "255", + "42", + "12.5", + "74.3", + "-33.45", + "-70.66", + "180.0", + "270.0", + "95", + "100", + "0", + "10", + "4.3", + "0.05", + ] + ) diff --git a/antares-python/tests/models/test_ship.py b/antares-python/tests/models/test_ship.py deleted file mode 100644 index acf85b8..0000000 --- a/antares-python/tests/models/test_ship.py +++ /dev/null @@ -1,6 +0,0 @@ -from antares.models.ship import ShipConfig - - -def test_ship_config_validation(): - ship = ShipConfig(initial_position=(10.0, 20.0)) - assert ship.initial_position == (10.0, 20.0) From 9c9d86b83555c9ce0493c2025880f1a2668bb773 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Urrea <68788933+jsurrea@users.noreply.github.com> Date: Fri, 11 Apr 2025 20:01:36 -0500 Subject: [PATCH 12/17] update readme and bump version --- antares-python/README.md | 8 ++++---- antares-python/pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/antares-python/README.md b/antares-python/README.md index e346cc6..517a4c0 100644 --- a/antares-python/README.md +++ b/antares-python/README.md @@ -1,12 +1,12 @@ # antares-python -[![CI](https://github.com/ANTARES/antares-python/actions/workflows/python-ci.yml/badge.svg)](https://github.com/ANTARES/antares-python/actions/workflows/python-ci.yml) -[![codecov](https://img.shields.io/badge/coverage-80%25-brightgreen)](https://github.com/ANTARES/antares-python) +[![CI](https://github.com/TheSoftwareDesignLab/ANTARES/actions/workflows/python-ci.yml/badge.svg)](https://github.com/TheSoftwareDesignLab/ANTARES/actions/workflows/python-ci.yml) +[![codecov](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/TheSoftwareDesignLab/ANTARES) [![PyPI version](https://img.shields.io/pypi/v/antares-python.svg)](https://pypi.org/project/antares-python/) [![Python version](https://img.shields.io/pypi/pyversions/antares-python)](https://pypi.org/project/antares-python/) -[![License](https://img.shields.io/github/license/ANTARES/antares-python)](LICENSE) +[![License](https://img.shields.io/github/license/TheSoftwareDesignLab/ANTARES)](LICENSE) -> Python interface for the [Antares](https://github.com/ANTARES/antares) simulation software +> Python interface for the [Antares](https://github.com/TheSoftwareDesignLab/ANTARES) simulation software `antares-python` is a facade library that allows Python developers to interact with the Antares simulation engine via HTTP. It provides a clean, user-friendly API for submitting simulations, retrieving results, and managing scenarios — similar to how `pyspark` interfaces with Apache Spark. diff --git a/antares-python/pyproject.toml b/antares-python/pyproject.toml index 83c269e..547c13d 100644 --- a/antares-python/pyproject.toml +++ b/antares-python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "antares-python" -version = "0.1.0" +version = "0.1.1" description = "Python interface for the Antares simulation software" authors = [ { name = "Juan Sebastian Urrea-Lopez", email = "js.urrea@uniandes.edu.co" }, From 5361610511a8e6b26bc1e14fdc915f02fd49acb4 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Urrea <68788933+jsurrea@users.noreply.github.com> Date: Fri, 11 Apr 2025 21:02:52 -0500 Subject: [PATCH 13/17] update cli config and include start command --- antares-python/.gitignore | 1 + antares-python/pyproject.toml | 7 +- antares-python/src/antares/cli.py | 125 +++++++++++++----- antares-python/src/antares/client/__init__.py | 8 +- antares-python/src/antares/client/rest.py | 4 +- antares-python/src/antares/config.py | 6 +- antares-python/tests/client/test_rest.py | 4 +- antares-python/tests/test_cli.py | 71 +++++++++- 8 files changed, 180 insertions(+), 46 deletions(-) diff --git a/antares-python/.gitignore b/antares-python/.gitignore index 573d4be..d229fb3 100644 --- a/antares-python/.gitignore +++ b/antares-python/.gitignore @@ -7,3 +7,4 @@ build/ .coverage .coverage.* htmlcov/ +antares.log diff --git a/antares-python/pyproject.toml b/antares-python/pyproject.toml index 547c13d..b5deb97 100644 --- a/antares-python/pyproject.toml +++ b/antares-python/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ dev = ["pytest-asyncio>=0.26.0"] [project.scripts] -antares = "antares.cli:app" +antares-cli = "antares.cli:app" [build-system] requires = ["setuptools>=61.0", "wheel"] @@ -40,7 +40,6 @@ files = ["src"] pythonpath = "src" asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" -addopts = "-ra -q -ra -q --cov=src --cov-report=term-missing" [tool.setuptools.packages.find] where = ["src"] @@ -50,8 +49,8 @@ lint = "ruff check . --fix" format = "ruff format ." typecheck = "mypy src/" test = "pytest" -coverage = "pytest --cov=src --cov-report=term-missing" +coverage = "pytest -ra -q --cov=src --cov-report=term-missing" build = "python -m build" publish = "twine upload dist/* --repository antares-python" -check = "task lint && task format && task typecheck && task test" +check = "task lint && task format && task typecheck && task coverage" release = "task check && task build && task publish" diff --git a/antares-python/src/antares/cli.py b/antares-python/src/antares/cli.py index 58f02b3..e770b24 100644 --- a/antares-python/src/antares/cli.py +++ b/antares-python/src/antares/cli.py @@ -1,6 +1,9 @@ import asyncio import json import logging +import shutil +import subprocess +from pathlib import Path from typing import NoReturn import typer @@ -12,46 +15,67 @@ from antares.errors import ConnectionError, SimulationError, SubscriptionError from antares.logger import setup_logging -app = typer.Typer(name="antares", help="Antares CLI for ship simulation", no_args_is_help=True) +app = typer.Typer(name="antares-cli", help="Antares CLI for ship simulation", no_args_is_help=True) console = Console(theme=Theme({"info": "green", "warn": "yellow", "error": "bold red"})) -def handle_error(message: str, code: int, json_output: bool = False) -> NoReturn: - logger = logging.getLogger("antares.cli") - if json_output: - typer.echo(json.dumps({"error": message}), err=True) - else: - console.print(f"[error]{message}") - logger.error("Exiting with error: %s", message) - raise typer.Exit(code) - - -def build_client(config_path: str | None, verbose: bool, json_output: bool) -> AntaresClient: - setup_logging(level=logging.DEBUG if verbose else logging.INFO) - logger = logging.getLogger("antares.cli") +@app.command() +def start( + executable: str = typer.Option("antares", help="Path to the Antares executable"), + config: str | None = typer.Option(None, help="Path to the TOML configuration file"), + verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output"), + json_output: bool = typer.Option(False, "--json", help="Output in JSON format"), +) -> None: + """ + Start the Antares simulation engine in the background. + + This command attempts to locate and launch the Antares executable either from the system's PATH + or from the provided path using the --executable option. If a config path is provided, it is + passed to the executable via --config. + + This command does not use the Python client and directly invokes the native binary. + """ + # Locate executable (either absolute path or in system PATH) + path = shutil.which(executable) if not Path(executable).exists() else executable + if path is None: + msg = f"Executable '{executable}' not found in PATH or at specified location." + console.print(f"[error]{msg}") + raise typer.Exit(1) + + # Prepare command + command = [path] + if config: + command += ["--config", config] + + if verbose: + console.print(f"[info]Starting Antares with command: {command}") try: - settings = load_config(config_path) - if verbose: - console.print(f"[info]Using settings: {settings.model_dump()}") - logger.debug("Loaded settings: %s", settings.model_dump()) - return AntaresClient( - base_url=settings.base_url, - tcp_host=settings.tcp_host, - tcp_port=settings.tcp_port, - timeout=settings.timeout, - auth_token=settings.auth_token, - ) + process = subprocess.Popen(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except Exception as e: - handle_error(f"Failed to load configuration: {e}", code=1, json_output=json_output) + msg = f"Failed to start Antares: {e}" + if json_output: + typer.echo(json.dumps({"error": msg}), err=True) + else: + console.print(f"[error]{msg}") + raise typer.Exit(2) from e + + msg = f"Antares started in background with PID {process.pid}" + if json_output: + typer.echo(json.dumps({"message": msg, "pid": process.pid})) + else: + console.print(f"[success]{msg}") @app.command() def reset( - config: str = typer.Option(None), - verbose: bool = typer.Option(False, "--verbose", "-v"), + config: str = typer.Option(None, help="Path to the TOML configuration file"), + verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output"), json_output: bool = typer.Option(False, "--json", help="Output in JSON format"), ) -> None: + """ + Reset the current simulation state. + """ client = build_client(config, verbose, json_output) try: client.reset_simulation() @@ -65,10 +89,13 @@ def reset( def add_ship( x: float = typer.Option(..., help="X coordinate of the ship"), y: float = typer.Option(..., help="Y coordinate of the ship"), - config: str = typer.Option(None, help="Path to the configuration file"), + config: str = typer.Option(None, help="Path to the TOML configuration file"), verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output"), json_output: bool = typer.Option(False, "--json", help="Output in JSON format"), ) -> None: + """ + Add a ship to the simulation with the specified parameters. + """ client = build_client(config, verbose, json_output) try: ship = ShipConfig(initial_position=(x, y)) @@ -81,11 +108,14 @@ def add_ship( @app.command() def subscribe( - config: str = typer.Option(None), - verbose: bool = typer.Option(False, "--verbose", "-v"), + config: str = typer.Option(None, help="Path to the TOML configuration file"), + verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output"), json_output: bool = typer.Option(False, "--json", help="Output in JSON format"), log_file: str = typer.Option("antares.log", help="Path to log file"), ) -> None: + """ + Subscribe to simulation events and print them to the console. + """ setup_logging(log_file=log_file, level=logging.DEBUG if verbose else logging.INFO) logger = logging.getLogger("antares.cli") @@ -103,3 +133,36 @@ async def _sub() -> None: handle_error(str(e), code=3, json_output=json_output) asyncio.run(_sub()) + + +def handle_error(message: str, code: int, json_output: bool = False) -> NoReturn: + """ + Handle errors by logging and printing them to the console. + """ + logger = logging.getLogger("antares.cli") + if json_output: + typer.echo(json.dumps({"error": message}), err=True) + else: + console.print(f"[error]{message}") + logger.error("Exiting with error: %s", message) + raise typer.Exit(code) + + +def build_client(config_path: str | None, verbose: bool, json_output: bool) -> AntaresClient: + """ + Build the Antares client using the provided configuration file. + """ + + try: + settings = load_config(config_path) + if verbose: + console.print(f"[info]Using settings: {settings.model_dump()}") + return AntaresClient( + host=settings.host, + http_port=settings.http_port, + tcp_port=settings.tcp_port, + timeout=settings.timeout, + auth_token=settings.auth_token, + ) + except Exception as e: + handle_error(f"Failed to load configuration: {e}", code=1, json_output=json_output) diff --git a/antares-python/src/antares/client/__init__.py b/antares-python/src/antares/client/__init__.py index 88691a1..ef86a33 100644 --- a/antares-python/src/antares/client/__init__.py +++ b/antares-python/src/antares/client/__init__.py @@ -25,12 +25,16 @@ def __init__( # Merge provided arguments with environment/.env via AntaresSettings self._settings = AntaresSettings(**filtered_kwargs) + base_url = f"http://{self._settings.host}:{self._settings.http_port}" self._rest = RestClient( - base_url=self._settings.base_url, + base_url=base_url, timeout=self._settings.timeout, auth_token=self._settings.auth_token, ) - self._tcp = TCPSubscriber(host=self._settings.tcp_host, port=self._settings.tcp_port) + self._tcp = TCPSubscriber( + host=self._settings.host, + port=self._settings.tcp_port, + ) def reset_simulation(self) -> None: """ diff --git a/antares-python/src/antares/client/rest.py b/antares-python/src/antares/client/rest.py index 1a8d423..bd25a15 100644 --- a/antares-python/src/antares/client/rest.py +++ b/antares-python/src/antares/client/rest.py @@ -1,6 +1,6 @@ import httpx -from antares.errors import ConnectionError, SimulationError +from antares.errors import ConnectionError, ShipConfigError, SimulationError from antares.models.ship import ShipConfig @@ -56,4 +56,4 @@ def add_ship(self, ship: ShipConfig) -> None: except httpx.RequestError as e: raise ConnectionError(f"Could not reach Antares API: {e}") from e except httpx.HTTPStatusError as e: - raise SimulationError(f"Add ship failed: {e.response.text}") from e + raise ShipConfigError(f"Add ship failed: {e.response.text}") from e diff --git a/antares-python/src/antares/config.py b/antares-python/src/antares/config.py index 9634ce1..feaef48 100644 --- a/antares-python/src/antares/config.py +++ b/antares-python/src/antares/config.py @@ -7,9 +7,9 @@ class AntaresSettings(BaseSettings): Supports environment variables and `.env` file loading. """ - base_url: str = "http://localhost:8000" - tcp_host: str = "localhost" - tcp_port: int = 9000 + host: str = "localhost" + http_port: int = 9000 + tcp_port: int = 9001 timeout: float = 5.0 auth_token: str | None = None diff --git a/antares-python/tests/client/test_rest.py b/antares-python/tests/client/test_rest.py index 860305c..53013a2 100644 --- a/antares-python/tests/client/test_rest.py +++ b/antares-python/tests/client/test_rest.py @@ -2,7 +2,7 @@ import pytest from antares.client.rest import RestClient -from antares.errors import ConnectionError, SimulationError +from antares.errors import ConnectionError, ShipConfigError, SimulationError from antares.models.ship import ShipConfig @@ -42,7 +42,7 @@ def test_add_ship_invalid_response(mocker): ) ship = ShipConfig(initial_position=(0, 0)) client = RestClient(base_url="http://localhost") - with pytest.raises(SimulationError): + with pytest.raises(ShipConfigError): client.add_ship(ship) diff --git a/antares-python/tests/test_cli.py b/antares-python/tests/test_cli.py index 56d7b70..6dd419e 100644 --- a/antares-python/tests/test_cli.py +++ b/antares-python/tests/test_cli.py @@ -1,4 +1,5 @@ import asyncio +import subprocess import pytest from typer.testing import CliRunner @@ -14,8 +15,8 @@ def fake_config(tmp_path): config_file = tmp_path / "config.toml" config_file.write_text(""" [antares] -base_url = "http://test.local" -tcp_host = "127.0.0.1" +host = "localhost" +http_port = 9000 tcp_port = 9001 timeout = 2.0 auth_token = "fake-token" @@ -134,3 +135,69 @@ async def __anext__(self): assert result.exit_code == 0 assert '{"event": "test"}' in result.output + + +def test_start_success(mocker): + mock_which = mocker.patch("shutil.which", return_value="/usr/local/bin/antares") + mock_popen = mocker.patch("subprocess.Popen", return_value=mocker.Mock(pid=1234)) + + result = runner.invoke(app, ["start"]) + assert result.exit_code == 0 + assert "Antares started in background with PID 1234" in result.output + mock_which.assert_called_once() + mock_popen.assert_called_once_with( + ["/usr/local/bin/antares"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + + +def test_start_executable_not_found(mocker): + mocker.patch("shutil.which", return_value=None) + + result = runner.invoke(app, ["start", "--executable", "fake-antares"]) + assert result.exit_code == 1 + assert "Executable 'fake-antares' not found" in result.output + + +def test_start_popen_failure(mocker): + mocker.patch("shutil.which", return_value="/usr/bin/antares") + mocker.patch("subprocess.Popen", side_effect=OSError("boom")) + + result = runner.invoke(app, ["start"]) + expected_exit_code = 2 + assert result.exit_code == expected_exit_code + assert "Failed to start Antares" in result.output + + +def test_start_popen_failure_with_json_verbose(mocker): + mocker.patch("shutil.which", return_value="/usr/bin/antares") + mocker.patch("subprocess.Popen", side_effect=OSError("boom")) + + result = runner.invoke(app, ["start", "--json", "-v"]) + + expected_exit_code = 2 + assert result.exit_code == expected_exit_code + assert '{"error":' in result.stdout or result.stderr + assert "Failed to start Antares: boom" in result.output + + +def test_start_with_json_output(mocker): + mocker.patch("shutil.which", return_value="/usr/bin/antares") + mocker.patch("subprocess.Popen", return_value=mocker.Mock(pid=4321)) + + result = runner.invoke(app, ["start", "--json"]) + assert result.exit_code == 0 + assert '{"message":' in result.output + assert '"pid": 4321' in result.output + + +def test_start_with_config(mocker): + mocker.patch("shutil.which", return_value="/usr/local/bin/antares") + mock_popen = mocker.patch("subprocess.Popen", return_value=mocker.Mock(pid=5678)) + + result = runner.invoke(app, ["start", "--config", "config.toml"]) + assert result.exit_code == 0 + mock_popen.assert_called_once_with( + ["/usr/local/bin/antares", "--config", "config.toml"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) From ea0912fd04321769b504bfab6fa1d06d71f169c3 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Urrea <68788933+jsurrea@users.noreply.github.com> Date: Fri, 11 Apr 2025 21:17:55 -0500 Subject: [PATCH 14/17] add support for ship types --- antares-python/src/antares/cli.py | 40 ++++++++++++++---- antares-python/src/antares/models/ship.py | 51 +++++++++++++++++++++-- 2 files changed, 81 insertions(+), 10 deletions(-) diff --git a/antares-python/src/antares/cli.py b/antares-python/src/antares/cli.py index e770b24..041b2fe 100644 --- a/antares-python/src/antares/cli.py +++ b/antares-python/src/antares/cli.py @@ -7,13 +7,15 @@ from typing import NoReturn import typer +from pydantic import ValidationError from rich.console import Console from rich.theme import Theme -from antares import AntaresClient, ShipConfig +from antares import AntaresClient from antares.config_loader import load_config from antares.errors import ConnectionError, SimulationError, SubscriptionError from antares.logger import setup_logging +from antares.models.ship import CircleShip, LineShip, RandomShip, ShipConfig, StationaryShip app = typer.Typer(name="antares-cli", help="Antares CLI for ship simulation", no_args_is_help=True) console = Console(theme=Theme({"info": "green", "warn": "yellow", "error": "bold red"})) @@ -86,21 +88,45 @@ def reset( @app.command() -def add_ship( - x: float = typer.Option(..., help="X coordinate of the ship"), - y: float = typer.Option(..., help="Y coordinate of the ship"), +def add_ship( # noqa: PLR0913 + type: str = typer.Option(..., help="Type of ship: 'line', 'circle', 'random', or 'stationary'"), + x: float = typer.Option(..., help="Initial X coordinate of the ship"), + y: float = typer.Option(..., help="Initial Y coordinate of the ship"), + angle: float = typer.Option(None, help="(line) Movement angle in radians"), + speed: float = typer.Option(None, help="(line/circle) Constant speed"), + radius: float = typer.Option(None, help="(circle) Radius of the circular path"), + max_speed: float = typer.Option(None, help="(random) Maximum possible speed"), config: str = typer.Option(None, help="Path to the TOML configuration file"), verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output"), json_output: bool = typer.Option(False, "--json", help="Output in JSON format"), ) -> None: """ - Add a ship to the simulation with the specified parameters. + Add a ship to the simulation, specifying its motion pattern and parameters. """ client = build_client(config, verbose, json_output) + + base_args = {"initial_position": (x, y)} + + ship: ShipConfig | None = None + try: + if type == "line": + ship = LineShip(**base_args, angle=angle, speed=speed) # type: ignore[arg-type] + elif type == "circle": + ship = CircleShip(**base_args, radius=radius, speed=speed) # type: ignore[arg-type] + elif type == "random": + ship = RandomShip(**base_args, max_speed=max_speed) # type: ignore[arg-type] + elif type == "stationary": + ship = StationaryShip(**base_args) # type: ignore[arg-type] + else: + raise ValueError(f"Invalid ship type: {type!r}") + + except (ValidationError, ValueError, TypeError) as e: + handle_error(f"Invalid ship parameters: {e}", code=2, json_output=json_output) + return + try: - ship = ShipConfig(initial_position=(x, y)) client.add_ship(ship) - msg = f"🚢 Added ship at ({x}, {y})" + msg = f"🚢 Added {type} ship at ({x}, {y})" typer.echo(json.dumps({"message": msg}) if json_output else msg) except (ConnectionError, SimulationError) as e: handle_error(str(e), code=2, json_output=json_output) diff --git a/antares-python/src/antares/models/ship.py b/antares-python/src/antares/models/ship.py index 5d95344..3960499 100644 --- a/antares-python/src/antares/models/ship.py +++ b/antares-python/src/antares/models/ship.py @@ -1,11 +1,56 @@ +from typing import Annotated, Literal + from pydantic import BaseModel, Field -class ShipConfig(BaseModel): +class BaseShip(BaseModel): """ - Defines the configuration for a ship to be added to the simulation. + Base class for ship configurations. """ initial_position: tuple[float, float] = Field( - ..., description="Initial (x, y) coordinates of the ship in simulation space." + ..., description="Initial (x, y) coordinates of the ship." ) + + +class LineShip(BaseShip): + """ + Ship that moves in a straight line at a constant speed. + """ + + type: Literal["line"] = "line" + angle: float = Field(..., description="Angle in radians.") + speed: float = Field(..., description="Constant speed.") + + +class CircleShip(BaseShip): + """ + Ship that moves in a circular path at a constant speed. + """ + + type: Literal["circle"] = "circle" + radius: float = Field(..., description="Radius of circular path.") + speed: float = Field(..., description="Constant speed.") + + +class RandomShip(BaseShip): + """ + Ship that moves in a random direction at a constant speed. + """ + + type: Literal["random"] = "random" + max_speed: float = Field(..., description="Maximum possible speed.") + + +class StationaryShip(BaseShip): + """ + Ship that does not move. + """ + + type: Literal["stationary"] = "stationary" + + +# Union of all ship configs +ShipConfig = Annotated[ + LineShip | CircleShip | RandomShip | StationaryShip, Field(discriminator="type") +] From 5acfdd3b31993766392585db6070e43ad1b53987 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Urrea <68788933+jsurrea@users.noreply.github.com> Date: Fri, 11 Apr 2025 21:29:56 -0500 Subject: [PATCH 15/17] add tests for different types of ships --- antares-python/src/antares/cli.py | 1 - antares-python/tests/client/test_client.py | 4 +- antares-python/tests/client/test_rest.py | 8 +- antares-python/tests/test_cli.py | 201 ++++++++++++++++++++- 4 files changed, 203 insertions(+), 11 deletions(-) diff --git a/antares-python/src/antares/cli.py b/antares-python/src/antares/cli.py index 041b2fe..febd8f2 100644 --- a/antares-python/src/antares/cli.py +++ b/antares-python/src/antares/cli.py @@ -122,7 +122,6 @@ def add_ship( # noqa: PLR0913 except (ValidationError, ValueError, TypeError) as e: handle_error(f"Invalid ship parameters: {e}", code=2, json_output=json_output) - return try: client.add_ship(ship) diff --git a/antares-python/tests/client/test_client.py b/antares-python/tests/client/test_client.py index d4ff373..d716647 100644 --- a/antares-python/tests/client/test_client.py +++ b/antares-python/tests/client/test_client.py @@ -1,7 +1,7 @@ import pytest from antares.client import AntaresClient -from antares.models.ship import ShipConfig +from antares.models.ship import StationaryShip def test_reset_simulation_delegates(mocker): @@ -14,7 +14,7 @@ def test_reset_simulation_delegates(mocker): def test_add_ship_delegates(mocker): mock_add = mocker.patch("antares.client.rest.RestClient.add_ship") client = AntaresClient(base_url="http://localhost") - ship = ShipConfig(initial_position=(1.0, 2.0)) + ship = StationaryShip(initial_position=(1.0, 2.0)) client.add_ship(ship) mock_add.assert_called_once_with(ship) diff --git a/antares-python/tests/client/test_rest.py b/antares-python/tests/client/test_rest.py index 53013a2..d159baa 100644 --- a/antares-python/tests/client/test_rest.py +++ b/antares-python/tests/client/test_rest.py @@ -3,7 +3,7 @@ from antares.client.rest import RestClient from antares.errors import ConnectionError, ShipConfigError, SimulationError -from antares.models.ship import ShipConfig +from antares.models.ship import CircleShip, LineShip, RandomShip def test_reset_simulation_success(mocker): @@ -24,7 +24,7 @@ def test_reset_simulation_failure(mocker): def test_add_ship_success(mocker): mock_request = httpx.Request("POST", "http://localhost/simulation/ships") mock_post = mocker.patch("httpx.post", return_value=httpx.Response(200, request=mock_request)) - ship = ShipConfig(initial_position=(0, 0)) + ship = LineShip(initial_position=(0, 0), angle=0, speed=1) client = RestClient(base_url="http://localhost") client.add_ship(ship) mock_post.assert_called_once() @@ -40,7 +40,7 @@ def test_add_ship_invalid_response(mocker): request=mock_request, ), ) - ship = ShipConfig(initial_position=(0, 0)) + ship = CircleShip(initial_position=(0, 0), radius=1, speed=1) client = RestClient(base_url="http://localhost") with pytest.raises(ShipConfigError): client.add_ship(ship) @@ -75,7 +75,7 @@ def test_add_ship_request_error(mocker): ), ) - ship = ShipConfig(initial_position=(0, 0)) + ship = RandomShip(initial_position=(0, 0), max_speed=1) client = RestClient(base_url="http://localhost") with pytest.raises(ConnectionError) as exc: diff --git a/antares-python/tests/test_cli.py b/antares-python/tests/test_cli.py index 6dd419e..2bc49df 100644 --- a/antares-python/tests/test_cli.py +++ b/antares-python/tests/test_cli.py @@ -32,11 +32,95 @@ def test_cli_reset(mocker, fake_config): mock_reset.assert_called_once() -def test_cli_add_ship(mocker, fake_config): +def test_cli_add_stationary_ship_success(mocker, fake_config): mock_add = mocker.patch("antares.client.rest.RestClient.add_ship") - result = runner.invoke(app, ["add-ship", "--x", "5.0", "--y", "6.0", "--config", fake_config]) + + result = runner.invoke( + app, + ["add-ship", "--type", "stationary", "--x", "5.0", "--y", "6.0", "--config", fake_config], + ) + + assert result.exit_code == 0 + assert "Added stationary ship at (5.0, 6.0)" in result.output + mock_add.assert_called_once() + + +def test_cli_add_line_ship_success(mocker, fake_config): + mock_add = mocker.patch("antares.client.rest.RestClient.add_ship") + + result = runner.invoke( + app, + [ + "add-ship", + "--type", + "line", + "--x", + "10.0", + "--y", + "20.0", + "--angle", + "0.5", + "--speed", + "3.0", + "--config", + fake_config, + ], + ) + + assert result.exit_code == 0 + assert "Added line ship at (10.0, 20.0)" in result.output + mock_add.assert_called_once() + + +def test_cli_add_circle_ship_success(mocker, fake_config): + mock_add = mocker.patch("antares.client.rest.RestClient.add_ship") + + result = runner.invoke( + app, + [ + "add-ship", + "--type", + "circle", + "--x", + "30.0", + "--y", + "40.0", + "--radius", + "15.0", + "--speed", + "2.5", + "--config", + fake_config, + ], + ) + + assert result.exit_code == 0 + assert "Added circle ship at (30.0, 40.0)" in result.output + mock_add.assert_called_once() + + +def test_cli_add_random_ship_success(mocker, fake_config): + mock_add = mocker.patch("antares.client.rest.RestClient.add_ship") + + result = runner.invoke( + app, + [ + "add-ship", + "--type", + "random", + "--x", + "0.0", + "--y", + "0.0", + "--max-speed", + "12.0", + "--config", + fake_config, + ], + ) + assert result.exit_code == 0 - assert "Added ship at (5.0, 6.0)" in result.output + assert "Added random ship at (0.0, 0.0)" in result.output mock_add.assert_called_once() @@ -82,12 +166,121 @@ def test_cli_add_ship_error_handling(mocker, fake_config): mocker.patch( "antares.client.rest.RestClient.add_ship", side_effect=SimulationError("ship rejected") ) - result = runner.invoke(app, ["add-ship", "--x", "1", "--y", "2", "--config", fake_config]) + + result = runner.invoke( + app, + ["add-ship", "--type", "stationary", "--x", "1", "--y", "2", "--config", fake_config], + ) + expected_exit_code = 2 assert result.exit_code == expected_exit_code assert "ship rejected" in result.output +def test_cli_add_ship_invalid_type(mocker, fake_config): + result = runner.invoke( + app, + [ + "add-ship", + "--type", + "invalid_type", + "--x", + "10.0", + "--y", + "20.0", + "--config", + fake_config, + ], + ) + + expected_exit_code = 2 + assert result.exit_code == expected_exit_code + assert "Invalid ship type" in result.output + + +def test_cli_add_stationary_ship_missing_args(fake_config): + result = runner.invoke( + app, + [ + "add-ship", + "--type", + "stationary", + "--x", + "5.0", + "--config", + fake_config, + ], + ) + + expected_exit_code = 2 + assert result.exit_code == expected_exit_code + + +def test_cli_add_line_ship_missing_args(fake_config): + result = runner.invoke( + app, + [ + "add-ship", + "--type", + "line", + "--x", + "10.0", + "--y", + "20.0", + "--config", + fake_config, + ], + ) + + expected_exit_code = 2 + assert result.exit_code == expected_exit_code + assert "Invalid ship parameters" in result.output + + +def test_cli_add_circle_missing_radius(mocker, fake_config): + result = runner.invoke( + app, + [ + "add-ship", + "--type", + "circle", + "--x", + "10.0", + "--y", + "20.0", + "--speed", + "2.0", + "--config", + fake_config, + ], + ) + + expected_exit_code = 2 + assert result.exit_code == expected_exit_code + assert "Invalid ship parameters" in result.output + + +def test_cli_add_random_missing_max_speed(mocker, fake_config): + result = runner.invoke( + app, + [ + "add-ship", + "--type", + "random", + "--x", + "0.0", + "--y", + "0.0", + "--config", + fake_config, + ], + ) + + expected_exit_code = 2 + assert result.exit_code == expected_exit_code + assert "Invalid ship parameters" in result.output + + def test_cli_subscribe_error(monkeypatch, fake_config): class FailingAsyncGenerator: def __aiter__(self): From f0407283cda0a8d56691d6fbd2e998c30a574663 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Urrea <68788933+jsurrea@users.noreply.github.com> Date: Fri, 11 Apr 2025 22:00:12 -0500 Subject: [PATCH 16/17] update docs --- antares-python/README.md | 253 ++++++++++++++++++++++--- antares-python/antares.log | 0 antares-python/config.example.toml | 32 ++++ antares-python/main.py | 46 ++++- antares-python/pyproject.toml | 2 +- antares-python/src/antares/__init__.py | 4 +- antares-python/template.env | 24 ++- 7 files changed, 326 insertions(+), 35 deletions(-) delete mode 100644 antares-python/antares.log create mode 100644 antares-python/config.example.toml diff --git a/antares-python/README.md b/antares-python/README.md index 517a4c0..3b0c75b 100644 --- a/antares-python/README.md +++ b/antares-python/README.md @@ -1,68 +1,265 @@ -# antares-python +# Antares Python Client -[![CI](https://github.com/TheSoftwareDesignLab/ANTARES/actions/workflows/python-ci.yml/badge.svg)](https://github.com/TheSoftwareDesignLab/ANTARES/actions/workflows/python-ci.yml) -[![codecov](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/TheSoftwareDesignLab/ANTARES) -[![PyPI version](https://img.shields.io/pypi/v/antares-python.svg)](https://pypi.org/project/antares-python/) -[![Python version](https://img.shields.io/pypi/pyversions/antares-python)](https://pypi.org/project/antares-python/) -[![License](https://img.shields.io/github/license/TheSoftwareDesignLab/ANTARES)](LICENSE) +> ✨ A modern Python interface for the Antares simulation engine ✨ -> Python interface for the [Antares](https://github.com/TheSoftwareDesignLab/ANTARES) simulation software +Antares Python Client is a developer-friendly library and CLI tool that allows you to interact with the Antares simulation engine via HTTP and TCP protocols. -`antares-python` is a facade library that allows Python developers to interact with the Antares simulation engine via HTTP. It provides a clean, user-friendly API for submitting simulations, retrieving results, and managing scenarios — similar to how `pyspark` interfaces with Apache Spark. +- Provides a high-level Python API to control the simulation +- Automatically maps Python objects to Antares-native requests +- Supports configuration via `.env` and `.toml` files +- Offers a CLI for scripting and manual control +- Built with Pydantic 2, Typer, and fully type-annotated + +Inspired by tools like PySpark, this library acts as a thin but powerful façade over the Antares backend. --- -## 🚀 Features +## 🌟 Features -- 🔁 Async + sync HTTP client -- 🔒 Typed schema validation (coming soon) -- 📦 Built-in support for data serialization -- 🧪 Fully testable with mocks -- 🛠️ First-class CLI support (planned) +- Add ships with complex motion patterns to the simulation +- Subscribe to live simulation events over TCP +- Launch the Antares binary locally with config +- Configure everything via `.env` or `.toml` +- Clean CLI with rich output and JSON support --- -## 📦 Installation +## 🚀 Installation + +### Requirements + +- Python >= 3.13 (tested with 3.13) +- `uv` for isolated dev environments + +### Install from PyPI ```bash pip install antares-python ``` +### Install in editable mode (for development) + +```bash +git clone https://github.com/TheSoftwareDesignLab/ANTARES.git +cd ANTARES/antares-python +uv venv +source .venv/bin/activate +uv pip install -e . +``` + +--- + +## 🚧 CLI Usage (`antares-cli`) + +After installing, the CLI tool `antares-cli` becomes available. + +### Available Commands + +| Command | Description | +|---------------|--------------------------------------------------| +| `add-ship` | Add a ship with specific motion type | +| `reset` | Reset the simulation | +| `subscribe` | Subscribe to simulation event stream | +| `start` | Start the Antares binary with optional config | + +### Common Options + +| Option | Description | +|---------------|-------------------------------------------------| +| `--config` | Path to `.toml` config file | +| `--verbose` | Enable detailed output | +| `--json` | Output results in JSON format | + +Example: + +```bash +antares-cli add-ship --type line --x 0 --y 0 --angle 0.5 --speed 5.0 +``` + --- -## ⚡ Quickstart +## 📚 Python Usage Example ```python -from antares import AntaresClient +import asyncio +from antares.client import AntaresClient +from antares.models.ship import LineShip, CircleShip, RandomShip, StationaryShip +from antares.models.track import Track + + +async def main(): + # Create the Antares client using environment config or .env file + client = AntaresClient() -client = AntaresClient(base_url="http://localhost:8000") + # Define ships of each supported type + ships = [ + StationaryShip(initial_position=(0.0, 0.0), type="stationary"), + RandomShip(initial_position=(10.0, -10.0), max_speed=15.0, type="random"), + CircleShip(initial_position=(-30.0, 20.0), radius=25.0, speed=3.0, type="circle"), + LineShip(initial_position=(5.0, 5.0), angle=0.78, speed=4.0, type="line"), + ] -# Submit a simulation -result = client.run_simulation(config={...}) -print(result.metrics) + # Add each ship to the simulation + for ship in ships: + client.add_ship(ship) + print(f"✅ Added {ship.type} ship at {ship.initial_position}") + + print("\n📡 Subscribing to simulation events...\n") + + # Listen to simulation events (TCP stream) + async for event in client.subscribe(): + if isinstance(event, Track): + print( + f"📍 Track #{event.id} - {event.name} at ({event.lat}, {event.long}) → {event.speed} knots" + ) + + +if __name__ == "__main__": + # Run the main async function + asyncio.run(main()) ``` --- -## 📚 Documentation +## 🧭 Ship Types + +Ships are classified based on their motion pattern. The `type` field determines which parameters are required. Here's a summary: + +| Type | Required Fields | Description | +|-------------|---------------------------------------------|---------------------------------------------| +| `stationary`| `initial_position` | Does not move at all | +| `random` | `initial_position`, `max_speed` | Moves randomly, up to a max speed | +| `circle` | `initial_position`, `radius`, `speed` | Moves in a circular pattern | +| `line` | `initial_position`, `angle`, `speed` | Moves in a straight line | + +Each ship type corresponds to a specific Pydantic model: + +- `StationaryShip` +- `RandomShip` +- `CircleShip` +- `LineShip` + +You can also use the generic `ShipConfig` union to parse from dynamic input like TOML or JSON. + +--- + +## ⚙️ Configuration + +The client supports two configuration methods: + +### `.env` File -_Work in progress — full API docs coming soon._ +The `.env` file allows you to define environment variables: + +```dotenv +ANTARES_HOST=localhost +ANTARES_HTTP_PORT=9000 +ANTARES_TCP_PORT=9001 +ANTARES_TIMEOUT=5.0 +ANTARES_AUTH_TOKEN= +``` + +➡️ See `template.env` for a complete example. + +### `.toml` Config File + +To configure the client and ships via a TOML file: + +```toml +[antares] +host = "localhost" +http_port = 9000 +tcp_port = 9001 +timeout = 5.0 +auth_token = "" + +[[antares.ships.stationary]] +initial_position = [50.0, 50.0] + +[[antares.ships.random]] +initial_position = [-20.0, 20.0] +max_speed = 10.0 + +[[antares.ships.circle]] +initial_position = [30.0, -30.0] +radius = 20.0 +speed = 4.0 + +[[antares.ships.line]] +initial_position = [0.0, 0.0] +angle = 0.785 +speed = 5.0 +``` + +➡️ See `config.example.toml` for a full working example. + +You can pass the config to any CLI command with: + +```bash +antares-cli add-ship --config path/to/config.toml +``` + +Or use it in Python with: + +```python +from antares.config_loader import load_config +settings = load_config("config.toml") +``` --- -## 🧪 Development +## 🧪 Development & Testing + +This project uses modern Python tooling for fast, isolated, and productive workflows. -To set up a local development environment: +### Setup ```bash uv venv source .venv/bin/activate uv pip install -e .[dev] -task check +``` + +### Available Tasks (via [`task`](https://taskfile.dev)) + +| Task | Description | +|----------------|---------------------------------------------| +| `task check` | Run linters (ruff, mypy) and formatter check | +| `task test` | Run full test suite | +| `task format` | Auto-format code with ruff + black | +| `task build` | Build the wheel and source dist | +| `task publish` | Publish to PyPI (requires version bump) | + +### Run tests manually + +```bash +pytest +``` + +### View test coverage + +```bash +pytest --cov=antares --cov-report=term-missing ``` --- -## 🧾 License +## 📄 License + +This project is licensed under the MIT License. See the [LICENSE](../LICENSE) file for details. + +--- + +## 🤝 Contributing + +Contributions are welcome! To contribute: + +1. Fork the repository +2. Create a new branch (`git checkout -b feature/my-feature`) +3. Make your changes +4. Run `task check` and `task test` to ensure quality +5. Submit a pull request 🚀 + +For significant changes, please open an issue first to discuss what you’d like to do. -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. +Happy hacking! 🛠️ diff --git a/antares-python/antares.log b/antares-python/antares.log deleted file mode 100644 index e69de29..0000000 diff --git a/antares-python/config.example.toml b/antares-python/config.example.toml new file mode 100644 index 0000000..980dda8 --- /dev/null +++ b/antares-python/config.example.toml @@ -0,0 +1,32 @@ +# ============================ +# Antares Simulation Config +# Example TOML configuration +# ============================ + +[antares] +host = "localhost" +http_port = 9000 +tcp_port = 9001 +timeout = 5.0 +auth_token = "" + +# ============================ +# Ships to add at startup +# ============================ + +[[antares.ships.line]] +initial_position = [0.0, 0.0] +angle = 0.785 # radians (approx. 45 degrees) +speed = 5.0 + +[[antares.ships.circle]] +initial_position = [30.0, -30.0] +radius = 20.0 +speed = 4.0 + +[[antares.ships.random]] +initial_position = [-20.0, 20.0] +max_speed = 10.0 + +[[antares.ships.stationary]] +initial_position = [50.0, 50.0] diff --git a/antares-python/main.py b/antares-python/main.py index 1ac9e4c..9c8e9dc 100644 --- a/antares-python/main.py +++ b/antares-python/main.py @@ -1,6 +1,46 @@ -def main(): - print("Hello from antares-python!") +import asyncio + +from antares import AntaresClient, CircleShip, LineShip, RandomShip, StationaryShip + + +async def main() -> None: + """ + Example of how to use the Antares Python client to add ships and subscribe to events. + This example demonstrates how to create different types of ships and add them to the Antares + simulation. It also shows how to subscribe to simulation events and print the track information. + """ + + # Initialize the Antares client + client = AntaresClient( + host="localhost", + http_port=9000, + tcp_port=9001, + timeout=5.0, + auth_token="my_secret_auth_token", + ) + + # Add ships + ships = [ + StationaryShip(initial_position=(0.0, 0.0), type="stationary"), + RandomShip(initial_position=(10.0, -10.0), max_speed=15.0, type="random"), + CircleShip(initial_position=(-30.0, 20.0), radius=25.0, speed=3.0, type="circle"), + LineShip(initial_position=(5.0, 5.0), angle=0.78, speed=4.0, type="line"), + ] + + for ship in ships: + client.add_ship(ship) + print(f"✅ Added {ship.type} ship at {ship.initial_position}") + + print("📡 Subscribing to simulation events...\n") + + try: + async for track in client.subscribe(): + print( + f"📍 Track #{track.id} - {track.name} @ ({track.lat}, {track.long}) → {track.speed} knots" # noqa: E501 + ) + except KeyboardInterrupt: + print("\n🛑 Subscription interrupted by user.") if __name__ == "__main__": - main() + asyncio.run(main()) diff --git a/antares-python/pyproject.toml b/antares-python/pyproject.toml index b5deb97..1b43bea 100644 --- a/antares-python/pyproject.toml +++ b/antares-python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "antares-python" -version = "0.1.1" +version = "0.1.2" description = "Python interface for the Antares simulation software" authors = [ { name = "Juan Sebastian Urrea-Lopez", email = "js.urrea@uniandes.edu.co" }, diff --git a/antares-python/src/antares/__init__.py b/antares-python/src/antares/__init__.py index ae3939e..99ffa68 100644 --- a/antares-python/src/antares/__init__.py +++ b/antares-python/src/antares/__init__.py @@ -1,4 +1,4 @@ from .client import AntaresClient -from .models.ship import ShipConfig +from .models.ship import CircleShip, LineShip, RandomShip, StationaryShip -__all__ = ["AntaresClient", "ShipConfig"] +__all__ = ["AntaresClient", "LineShip", "RandomShip", "StationaryShip", "CircleShip"] diff --git a/antares-python/template.env b/antares-python/template.env index 5734a16..c66163e 100644 --- a/antares-python/template.env +++ b/antares-python/template.env @@ -1 +1,23 @@ -ANTARES_BASE_URL = "http://localhost:8080" +# ======================== +# Antares Python Client Environment Template +# To use this template, create a new file named `.env` in the same directory +# and fill in the required values. +# You can use: +# cp template.env .env +# ======================== + +# Host where the Antares simulation engine is running +ANTARES_HOST=localhost + +# Port for the HTTP API +ANTARES_HTTP_PORT=9000 + +# Port for the TCP stream +ANTARES_TCP_PORT=9001 + +# Request timeout in seconds +ANTARES_TIMEOUT=5.0 + +# Optional: Authentication token for the API +# Leave empty if not required +ANTARES_AUTH_TOKEN= From e00f99b25e4c6be29c4395870124366202350a64 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Urrea <68788933+jsurrea@users.noreply.github.com> Date: Fri, 11 Apr 2025 22:01:56 -0500 Subject: [PATCH 17/17] add badges --- antares-python/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/antares-python/README.md b/antares-python/README.md index 3b0c75b..e8b1b23 100644 --- a/antares-python/README.md +++ b/antares-python/README.md @@ -1,5 +1,10 @@ # Antares Python Client +[![CI](https://github.com/TheSoftwareDesignLab/ANTARES/actions/workflows/python-ci.yml/badge.svg)](https://github.com/TheSoftwareDesignLab/ANTARES/actions/workflows/python-ci.yml) +[![codecov](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/TheSoftwareDesignLab/ANTARES) +[![PyPI version](https://img.shields.io/pypi/v/antares-python.svg)](https://pypi.org/project/antares-python/) +[![License](https://img.shields.io/github/license/TheSoftwareDesignLab/ANTARES)](LICENSE) + > ✨ A modern Python interface for the Antares simulation engine ✨ Antares Python Client is a developer-friendly library and CLI tool that allows you to interact with the Antares simulation engine via HTTP and TCP protocols.