From 788dd10a91048a58ea0777251989062af288f83d Mon Sep 17 00:00:00 2001 From: Sergey Savelyev Date: Thu, 1 Jan 2026 10:11:53 -0800 Subject: [PATCH] Replace gRPC Backend (#10) **Rationale:** Having two separate servers and communication methods resulted in additional maintenance & the need to convert often between backend & frontend data types. By moving the backend communication off of gRPC and to just use websockets it both gives more control & allows for simplification of the implementation. https://gitea.sergeysav.com/sergeysav/telemetry_visualization/issues/8 **Changes:** - Replaces gRPC backend. - New implementation automatically handles reconnect logic - Implements an api layer - Migrates examples to the api layer - Implements a proc macro to make command handling easier - Implements unit tests for the api layer (90+% coverage) - Implements integration tests for the proc macro (90+% coverage) Reviewed-on: https://gitea.sergeysav.com/sergeysav/telemetry_visualization/pulls/10 Co-authored-by: Sergey Savelyev Co-committed-by: Sergey Savelyev --- Cargo.lock | 1034 ++++++++--------- Cargo.toml | 29 +- api-core/Cargo.toml | 12 + api-core/src/command.rs | 47 + api-core/src/data_type.rs | 26 + api-core/src/data_value.rs | 20 + api-core/src/lib.rs | 3 + api-proc-macro/Cargo.toml | 19 + api-proc-macro/src/into_command_definition.rs | 123 ++ api-proc-macro/src/lib.rs | 11 + .../into_command_definition/enum_fails.rs | 10 + .../into_command_definition/enum_fails.stderr | 5 + .../into_command_definition/union_fails.rs | 12 + .../union_fails.stderr | 5 + .../unnamed_struct_fails.rs | 8 + .../unnamed_struct_fails.stderr | 5 + ...st_derive_macro_into_command_definition.rs | 170 +++ api/Cargo.toml | 24 + api/src/client/command.rs | 454 ++++++++ api/src/client/config.rs | 11 + api/src/client/context.rs | 594 ++++++++++ api/src/client/error.rs | 37 + api/src/client/mod.rs | 598 ++++++++++ api/src/client/telemetry.rs | 464 ++++++++ api/src/lib.rs | 15 + api/src/messages/callback.rs | 7 + api/src/messages/command.rs | 15 + api/src/messages/mod.rs | 40 + api/src/messages/payload.rs | 23 + api/src/messages/telemetry_definition.rs | 19 + api/src/messages/telemetry_entry.rs | 14 + api/src/test/mock_stream_sink.rs | 82 ++ api/src/test/mod.rs | 1 + examples/simple_command/Cargo.toml | 14 +- examples/simple_command/src/main.rs | 154 +-- examples/simple_producer/Cargo.toml | 14 +- examples/simple_producer/src/main.rs | 328 +----- server/Cargo.toml | 39 +- server/build.rs | 1 - server/proto/core.proto | 83 -- server/src/command/command_handle.rs | 20 + server/src/command/definition.rs | 21 +- server/src/command/error.rs | 4 +- server/src/command/mod.rs | 1 + server/src/command/service.rs | 131 ++- server/src/grpc/cmd.rs | 13 +- server/src/grpc/mod.rs | 44 - server/src/grpc/tlm.rs | 141 --- server/src/http/api/panels.rs | 15 +- server/src/http/api/tlm.rs | 9 +- server/src/http/backend/connection.rs | 117 ++ server/src/http/backend/mod.rs | 60 + server/src/http/error.rs | 8 +- server/src/http/mod.rs | 3 + server/src/http/websocket/mod.rs | 17 +- server/src/http/websocket/request.rs | 5 +- server/src/http/websocket/response.rs | 3 +- server/src/lib.rs | 9 - server/src/main.rs | 14 +- server/src/panels/mod.rs | 17 +- server/src/telemetry/data_item.rs | 4 +- server/src/telemetry/data_type.rs | 38 - server/src/telemetry/data_value.rs | 8 - server/src/telemetry/definition.rs | 11 +- server/src/telemetry/history.rs | 62 +- server/src/telemetry/management_service.rs | 73 +- server/src/telemetry/mod.rs | 2 - server/src/uuid.rs | 18 - 68 files changed, 3934 insertions(+), 1504 deletions(-) create mode 100644 api-core/Cargo.toml create mode 100644 api-core/src/command.rs create mode 100644 api-core/src/data_type.rs create mode 100644 api-core/src/data_value.rs create mode 100644 api-core/src/lib.rs create mode 100644 api-proc-macro/Cargo.toml create mode 100644 api-proc-macro/src/into_command_definition.rs create mode 100644 api-proc-macro/src/lib.rs create mode 100644 api-proc-macro/tests/into_command_definition/enum_fails.rs create mode 100644 api-proc-macro/tests/into_command_definition/enum_fails.stderr create mode 100644 api-proc-macro/tests/into_command_definition/union_fails.rs create mode 100644 api-proc-macro/tests/into_command_definition/union_fails.stderr create mode 100644 api-proc-macro/tests/into_command_definition/unnamed_struct_fails.rs create mode 100644 api-proc-macro/tests/into_command_definition/unnamed_struct_fails.stderr create mode 100644 api-proc-macro/tests/test_derive_macro_into_command_definition.rs create mode 100644 api/Cargo.toml create mode 100644 api/src/client/command.rs create mode 100644 api/src/client/config.rs create mode 100644 api/src/client/context.rs create mode 100644 api/src/client/error.rs create mode 100644 api/src/client/mod.rs create mode 100644 api/src/client/telemetry.rs create mode 100644 api/src/lib.rs create mode 100644 api/src/messages/callback.rs create mode 100644 api/src/messages/command.rs create mode 100644 api/src/messages/mod.rs create mode 100644 api/src/messages/payload.rs create mode 100644 api/src/messages/telemetry_definition.rs create mode 100644 api/src/messages/telemetry_entry.rs create mode 100644 api/src/test/mock_stream_sink.rs create mode 100644 api/src/test/mod.rs delete mode 100644 server/proto/core.proto create mode 100644 server/src/command/command_handle.rs delete mode 100644 server/src/grpc/mod.rs delete mode 100644 server/src/grpc/tlm.rs create mode 100644 server/src/http/backend/connection.rs create mode 100644 server/src/http/backend/mod.rs delete mode 100644 server/src/telemetry/data_type.rs delete mode 100644 server/src/telemetry/data_value.rs delete mode 100644 server/src/uuid.rs diff --git a/Cargo.lock b/Cargo.lock index 8f99aab..8c355ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,7 +39,7 @@ dependencies = [ "flate2", "foldhash", "futures-core", - "h2 0.3.27", + "h2", "http 0.2.12", "httparse", "httpdate", @@ -65,7 +65,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn", + "syn 2.0.112", ] [[package]] @@ -183,7 +183,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn", + "syn 2.0.112", ] [[package]] @@ -245,6 +245,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.100" @@ -252,36 +302,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +name = "api" +version = "0.1.0" dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", + "api-core", + "api-proc-macro", + "chrono", + "derive_more", + "env_logger", + "futures-util", + "log", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-tungstenite", + "tokio-util", + "uuid", ] [[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +name = "api-core" +version = "0.1.0" dependencies = [ - "proc-macro2", - "quote", - "syn", + "chrono", + "derive_more", + "serde", + "thiserror", ] [[package]] -name = "async-trait" -version = "0.1.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +name = "api-proc-macro" +version = "0.1.0" dependencies = [ - "proc-macro2", + "api", + "api-core", + "proc-macro-error", "quote", - "syn", + "syn 2.0.112", + "trybuild", ] [[package]] @@ -293,65 +352,12 @@ dependencies = [ "num-traits", ] -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" -[[package]] -name = "axum" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" -dependencies = [ - "async-trait", - "axum-core", - "bytes", - "futures-util", - "http 1.1.0", - "http-body", - "http-body-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "sync_wrapper 1.0.1", - "tower 0.5.1", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 1.1.0", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper 1.0.1", - "tower-layer", - "tower-service", -] - [[package]] name = "base64" version = "0.22.1" @@ -417,9 +423,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.2" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "bytestring" @@ -456,10 +462,27 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -497,9 +520,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.4" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -569,6 +592,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + [[package]] name = "der" version = "0.7.10" @@ -591,24 +620,24 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "convert_case", "proc-macro2", "quote", "rustc_version", - "syn", + "syn 2.0.112", "unicode-xid", ] @@ -632,7 +661,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.112", ] [[package]] @@ -659,22 +688,35 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "errno" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "etcetera" version = "0.8.0" @@ -697,27 +739,16 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "fastrand" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" - [[package]] name = "fern" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" dependencies = [ + "colored", "log", ] -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - [[package]] name = "flate2" version = "1.0.34" @@ -751,21 +782,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -819,6 +835,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.112", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -839,6 +866,7 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -880,6 +908,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "h2" version = "0.3.27" @@ -892,38 +926,13 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.6.0", + "indexmap", "slab", "tokio", "tokio-util", "tracing", ] -[[package]] -name = "h2" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.1.0", - "indexmap 2.6.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.15.0" @@ -935,6 +944,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashlink" version = "0.10.0" @@ -1011,29 +1026,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http 1.1.0", -] - -[[package]] -name = "http-body-util" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" -dependencies = [ - "bytes", - "futures-util", - "http 1.1.0", - "http-body", - "pin-project-lite", -] - [[package]] name = "httparse" version = "1.9.5" @@ -1046,59 +1038,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "hyper" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2 0.4.6", - "http 1.1.0", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-timeout" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" -dependencies = [ - "hyper", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http 1.1.0", - "http-body", - "hyper", - "pin-project-lite", - "socket2 0.5.7", - "tokio", - "tower-service", - "tracing", -] - [[package]] name = "iana-time-zone" version = "0.1.61" @@ -1232,32 +1171,19 @@ checksum = "aae21c3177a27788957044151cc2800043d127acaa460a47ebb9b84dfa2c6aa0" [[package]] name = "indexmap" -version = "1.9.3" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - -[[package]] -name = "indexmap" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown 0.16.1", ] [[package]] -name = "itertools" -version = "0.13.0" +name = "is_terminal_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" @@ -1265,6 +1191,30 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jiff" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a87d9b8105c23642f50cbbae03d1f75d8422c5cb98ce7ee9271f7ff7505be6b8" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b787bebb543f8969132630c51fd0afab173a86c6abae56ff3b9e5e3e3f9f6e58" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.112", +] + [[package]] name = "jobserver" version = "0.1.32" @@ -1333,12 +1283,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" - [[package]] name = "litemap" version = "0.8.1" @@ -1378,12 +1322,6 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - [[package]] name = "md-5" version = "0.10.6" @@ -1428,29 +1366,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "multimap" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" - -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -1510,48 +1425,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "openssl" -version = "0.10.75" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openssl-probe" -version = "0.1.6" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-sys" -version = "0.9.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] +checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" [[package]] name = "papaya" @@ -1613,36 +1496,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "petgraph" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" -dependencies = [ - "fixedbitset", - "indexmap 2.6.0", -] - -[[package]] -name = "pin-project" -version = "1.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf123a161dde1e524adf36f90bc5d8d3462824a9c43553ad07a8183161189ec" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4502d8515ca9f32f1fb543d987f63d95a14934883db45bdb48060b6b69257f8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "pin-project-lite" version = "0.2.14" @@ -1682,6 +1535,21 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1707,82 +1575,43 @@ dependencies = [ ] [[package]] -name = "prettyplease" -version = "0.2.22" +name = "proc-macro-error" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", - "syn", + "quote", + "version_check", ] [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" dependencies = [ "unicode-ident", ] -[[package]] -name = "prost" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-build" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1318b19085f08681016926435853bbf7858f9c082d0999b80550ff5d9abe15" -dependencies = [ - "bytes", - "heck", - "itertools", - "log", - "multimap", - "once_cell", - "petgraph", - "prettyplease", - "prost", - "prost-types", - "regex", - "syn", - "tempfile", -] - -[[package]] -name = "prost-derive" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" -dependencies = [ - "anyhow", - "itertools", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "prost-types" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4759aa0d3a6232fb8dbdb97b61de2c20047c68aca932c7ed76da9d788508d670" -dependencies = [ - "prost", -] - [[package]] name = "quote" -version = "1.0.37" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -1892,6 +1721,20 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "ring" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75ec5e92c4d8aede845126adc388046234541629e76029599ed35a003c7ed24" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rsa" version = "0.9.9" @@ -1922,16 +1765,49 @@ dependencies = [ ] [[package]] -name = "rustix" -version = "0.38.37" +name = "rustls" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.52.0", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -1963,9 +1839,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.11.1" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ "bitflags", "core-foundation", @@ -2027,20 +1903,29 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.112", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", ] [[package]] @@ -2062,22 +1947,19 @@ dependencies = [ "actix-web", "actix-ws", "anyhow", + "api", "chrono", "derive_more", "fern", - "hex", + "futures-util", "log", "papaya", - "prost", - "rand 0.9.0", "serde", "serde_json", "sqlx", "thiserror", "tokio", "tokio-util", - "tonic", - "tonic-build", "uuid", ] @@ -2133,25 +2015,25 @@ name = "simple_command" version = "0.0.0" dependencies = [ "anyhow", - "chrono", + "api", + "env_logger", "log", - "num-traits", - "server", "tokio", "tokio-util", - "tonic", ] [[package]] name = "simple_producer" version = "0.0.0" dependencies = [ + "anyhow", + "api", "chrono", + "env_logger", + "futures-util", "num-traits", - "server", "tokio", "tokio-util", - "tonic", ] [[package]] @@ -2242,12 +2124,13 @@ dependencies = [ "futures-util", "hashbrown 0.15.0", "hashlink", - "indexmap 2.6.0", + "indexmap", "log", "memchr", - "native-tls", "once_cell", "percent-encoding", + "rustls", + "rustls-native-certs", "serde", "serde_json", "sha2", @@ -2269,7 +2152,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn", + "syn 2.0.112", ] [[package]] @@ -2292,7 +2175,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn", + "syn 2.0.112", "tokio", "url", ] @@ -2425,27 +2308,25 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.93" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - -[[package]] -name = "sync_wrapper" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" - [[package]] name = "synstructure" version = "0.13.2" @@ -2454,20 +2335,22 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.112", ] [[package]] -name = "tempfile" -version = "3.13.0" +name = "target-triple" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ - "cfg-if", - "fastrand", - "once_cell", - "rustix", - "windows-sys 0.59.0", + "winapi-util", ] [[package]] @@ -2487,7 +2370,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.112", ] [[package]] @@ -2571,20 +2454,46 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.112", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.17" @@ -2599,94 +2508,43 @@ dependencies = [ ] [[package]] -name = "tonic" -version = "0.12.3" +name = "toml" +version = "0.9.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" dependencies = [ - "async-stream", - "async-trait", - "axum", - "base64", - "bytes", - "h2 0.4.6", - "http 1.1.0", - "http-body", - "http-body-util", - "hyper", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "prost", - "socket2 0.5.7", - "tokio", - "tokio-stream", - "tower 0.4.13", - "tower-layer", - "tower-service", - "tracing", + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", ] [[package]] -name = "tonic-build" -version = "0.12.3" +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ - "prettyplease", - "proc-macro2", - "prost-build", - "prost-types", - "quote", - "syn", + "serde_core", ] [[package]] -name = "tower" -version = "0.4.13" +name = "toml_parser" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ - "futures-core", - "futures-util", - "indexmap 1.9.3", - "pin-project", - "pin-project-lite", - "rand 0.8.5", - "slab", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", + "winnow", ] [[package]] -name = "tower" -version = "0.5.1" +name = "toml_writer" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper 0.1.2", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] name = "tracing" @@ -2708,7 +2566,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.112", ] [[package]] @@ -2721,10 +2579,38 @@ dependencies = [ ] [[package]] -name = "try-lock" -version = "0.2.5" +name = "trybuild" +version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +checksum = "3e17e807bff86d2a06b52bca4276746584a78375055b6e45843925ce2802b335" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand 0.9.0", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror", + "utf-8", +] [[package]] name = "typenum" @@ -2771,6 +2657,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.7" @@ -2783,12 +2675,24 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.19.0" @@ -2797,6 +2701,7 @@ checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.1", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -2812,15 +2717,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2874,7 +2770,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.112", "wasm-bindgen-shared", ] @@ -2897,6 +2793,15 @@ dependencies = [ "wasite", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -2930,15 +2835,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" @@ -3143,6 +3039,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" + [[package]] name = "wit-bindgen-rt" version = "0.33.0" @@ -3177,7 +3079,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.112", "synstructure", ] @@ -3208,7 +3110,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.112", ] [[package]] @@ -3219,7 +3121,7 @@ checksum = "76331675d372f91bf8d17e13afbd5fe639200b73d01f0fc748bb059f9cca2db7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.112", ] [[package]] @@ -3239,7 +3141,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.112", "synstructure", ] @@ -3279,9 +3181,15 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.112", ] +[[package]] +name = "zmij" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77cc0158b0d3103d58e9e82bdbe9cf9289d80dbcf4e686ff16730eb9e5814d1a" + [[package]] name = "zstd" version = "0.13.2" diff --git a/Cargo.toml b/Cargo.toml index 5567a9c..8dfc5ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,33 @@ [workspace] -members = ["server", "examples/simple_producer", "examples/simple_command"] +members = ["api", "api-core", "api-proc-macro", "server", "examples/simple_producer", "examples/simple_command"] resolver = "2" +[workspace.dependencies] +actix-web = "4.12.1" +actix-ws = "0.3.0" +anyhow = "1.0.100" +chrono = { version = "0.4.42" } +derive_more = { version = "2.1.1" } +env_logger = "0.11.8" +fern = "0.7.1" +futures-util = "0.3.31" +log = "0.4.29" +num-traits = "0.2.19" +papaya = "0.2.3" +proc-macro-error = "1.0.4" +quote = "1.0.42" +serde = { version = "1.0.228" } +serde_json = "1.0.148" +sqlx = "0.8.6" +syn = "2.0.112" +thiserror = "2.0.17" +tokio = { version = "1.48.0" } +tokio-test = "0.4.4" +tokio-stream = "0.1.17" +tokio-tungstenite = { version = "0.28.0" } +tokio-util = "0.7.17" +trybuild = "1.0.114" +uuid = { version = "1.19.0", features = ["v4"] } + [profile.dev.package.sqlx-macros] opt-level = 3 diff --git a/api-core/Cargo.toml b/api-core/Cargo.toml new file mode 100644 index 0000000..bedaa3a --- /dev/null +++ b/api-core/Cargo.toml @@ -0,0 +1,12 @@ + +[package] +name = "api-core" +edition = "2021" +version = "0.1.0" +authors = ["Sergey "] + +[dependencies] +chrono = { workspace = true, features = ["serde"] } +derive_more = { workspace = true, features = ["display", "from", "try_into"] } +serde = { workspace = true, features = ["derive"] } +thiserror = { workspace = true } diff --git a/api-core/src/command.rs b/api-core/src/command.rs new file mode 100644 index 0000000..e8a210e --- /dev/null +++ b/api-core/src/command.rs @@ -0,0 +1,47 @@ +use crate::data_type::DataType; +use crate::data_value::DataValue; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use thiserror::Error; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CommandParameterDefinition { + pub name: String, + pub data_type: DataType, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CommandDefinition { + pub name: String, + pub parameters: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CommandHeader { + pub timestamp: DateTime, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Command { + #[serde(flatten)] + pub header: CommandHeader, + pub parameters: HashMap, +} + +#[derive(Debug, PartialEq, Eq, Error)] +pub enum IntoCommandDefinitionError { + #[error("Parameter Missing: {0}")] + ParameterMissing(String), + #[error("Mismatched Type for {parameter}. {expected:?} expected")] + MismatchedType { + parameter: String, + expected: DataType, + }, +} + +pub trait IntoCommandDefinition: Sized { + fn create(name: String) -> CommandDefinition; + + fn parse(command: Command) -> Result; +} diff --git a/api-core/src/data_type.rs b/api-core/src/data_type.rs new file mode 100644 index 0000000..807e084 --- /dev/null +++ b/api-core/src/data_type.rs @@ -0,0 +1,26 @@ +use crate::data_value::DataValue; +use derive_more::Display; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display)] +pub enum DataType { + Float32, + Float64, + Boolean, +} + +pub trait ToDataType: Into { + const DATA_TYPE: DataType; +} + +macro_rules! impl_to_data_type { + ( $ty:ty, $value:expr ) => { + impl ToDataType for $ty { + const DATA_TYPE: DataType = $value; + } + }; +} + +impl_to_data_type!(f32, DataType::Float32); +impl_to_data_type!(f64, DataType::Float64); +impl_to_data_type!(bool, DataType::Boolean); diff --git a/api-core/src/data_value.rs b/api-core/src/data_value.rs new file mode 100644 index 0000000..9e14bd7 --- /dev/null +++ b/api-core/src/data_value.rs @@ -0,0 +1,20 @@ +use crate::data_type::DataType; +use derive_more::{From, TryInto}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, From, TryInto)] +pub enum DataValue { + Float32(f32), + Float64(f64), + Boolean(bool), +} + +impl DataValue { + pub fn to_data_type(self) -> DataType { + match self { + DataValue::Float32(_) => DataType::Float32, + DataValue::Float64(_) => DataType::Float64, + DataValue::Boolean(_) => DataType::Boolean, + } + } +} diff --git a/api-core/src/lib.rs b/api-core/src/lib.rs new file mode 100644 index 0000000..fdd3dc0 --- /dev/null +++ b/api-core/src/lib.rs @@ -0,0 +1,3 @@ +pub mod command; +pub mod data_type; +pub mod data_value; diff --git a/api-proc-macro/Cargo.toml b/api-proc-macro/Cargo.toml new file mode 100644 index 0000000..da77f88 --- /dev/null +++ b/api-proc-macro/Cargo.toml @@ -0,0 +1,19 @@ + +[package] +name = "api-proc-macro" +edition = "2021" +version = "0.1.0" +authors = ["Sergey "] + +[lib] +proc-macro = true + +[dependencies] +api-core = { path = "../api-core" } +proc-macro-error = { workspace = true } +quote = { workspace = true } +syn = { workspace = true } + +[dev-dependencies] +api = { path = "../api" } +trybuild = { workspace = true } diff --git a/api-proc-macro/src/into_command_definition.rs b/api-proc-macro/src/into_command_definition.rs new file mode 100644 index 0000000..18ecf27 --- /dev/null +++ b/api-proc-macro/src/into_command_definition.rs @@ -0,0 +1,123 @@ +use proc_macro_error::abort; +use quote::{quote, quote_spanned}; +use syn::spanned::Spanned; +use syn::{parse_macro_input, parse_quote, Data, DeriveInput, Fields, GenericParam, Generics}; + +pub fn derive_into_command_definition_impl( + item: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + let DeriveInput { + ident, + data, + generics, + .. + }: DeriveInput = parse_macro_input!(item as DeriveInput); + + let data = match data { + Data::Struct(data) => data, + Data::Enum(data) => abort!( + data.enum_token, + "IntoCommandDefinition not supported for enum" + ), + Data::Union(data) => abort!( + data.union_token, + "IntoCommandDefinition not supported for union" + ), + }; + + let generics = add_trait_bounds(generics); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let num_fields = data.fields.len(); + + let create_param_stream = match &data.fields { + Fields::Named(fields) => { + let field_entries = fields.named.iter().map(|field| { + let name = field.ident.clone().map(|id| id.to_string()); + let field_type = &field.ty; + quote_spanned! { field.span() => + parameters.push(api::messages::command::CommandParameterDefinition { + name: #name.to_string(), + data_type: <#field_type as api::data_type::ToDataType>::DATA_TYPE, + }); + } + }); + quote! { #(#field_entries)* } + } + Fields::Unnamed(fields) => abort!( + fields, + "IntoCommandDefinition not supported for unnamed structs" + ), + Fields::Unit => quote! {}, + }; + let parse_param_stream = match &data.fields { + Fields::Named(fields) => { + let field_entries = fields.named.iter().map(|field| { + let name = &field.ident; + let name_string = field.ident.clone().map(|id| id.to_string()); + let field_type = &field.ty; + quote_spanned! { field.span() => + let #name: #field_type = (*command + .parameters + .get(#name_string) + .ok_or_else(|| api::messages::command::IntoCommandDefinitionError::ParameterMissing(#name_string.to_string()))?) + .try_into() + .map_err(|_| api::messages::command::IntoCommandDefinitionError::MismatchedType { + parameter: #name_string.to_string(), + expected: <#field_type as api::data_type::ToDataType>::DATA_TYPE, + })?; + } + }); + quote! { #(#field_entries)* } + } + Fields::Unnamed(_) => unreachable!("Already checked this"), + Fields::Unit => quote! {}, + }; + let param_name_stream = match &data.fields { + Fields::Named(fields) => { + let field_entries = fields.named.iter().map(|field| { + let name = &field.ident; + quote_spanned! { field.span() => #name, } + }); + quote! { #(#field_entries)* } + } + Fields::Unnamed(_) => unreachable!("Already checked this"), + Fields::Unit => quote! {}, + }; + + let result = quote! { + impl #impl_generics api::messages::command::IntoCommandDefinition for #ident #ty_generics #where_clause { + fn create(name: std::string::String) -> api::messages::command::CommandDefinition { + let mut parameters = std::vec::Vec::with_capacity( #num_fields ); + #create_param_stream + api::messages::command::CommandDefinition { + name: name, + parameters: parameters, + } + } + + fn parse(command: api::messages::command::Command) -> core::result::Result { + #parse_param_stream + Ok(Self { + #param_name_stream + }) + } + } + }; + + result.into() +} + +fn add_trait_bounds(mut generics: Generics) -> Generics { + for param in &mut generics.params { + if let GenericParam::Type(ref mut type_param) = *param { + type_param + .bounds + .push(parse_quote!(api::data_type::ToDataType)); + type_param.bounds.push(parse_quote!( + core::convert::TryFrom + )); + } + } + generics +} diff --git a/api-proc-macro/src/lib.rs b/api-proc-macro/src/lib.rs new file mode 100644 index 0000000..2638464 --- /dev/null +++ b/api-proc-macro/src/lib.rs @@ -0,0 +1,11 @@ +extern crate proc_macro; + +use proc_macro_error::proc_macro_error; + +mod into_command_definition; + +#[proc_macro_error] +#[proc_macro_derive(IntoCommandDefinition)] +pub fn derive_into_command_definition(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + into_command_definition::derive_into_command_definition_impl(item) +} diff --git a/api-proc-macro/tests/into_command_definition/enum_fails.rs b/api-proc-macro/tests/into_command_definition/enum_fails.rs new file mode 100644 index 0000000..f04e360 --- /dev/null +++ b/api-proc-macro/tests/into_command_definition/enum_fails.rs @@ -0,0 +1,10 @@ +use api_proc_macro::IntoCommandDefinition; + +#[derive(IntoCommandDefinition)] +enum TestEnum { + Variant +} + +fn main() { + +} diff --git a/api-proc-macro/tests/into_command_definition/enum_fails.stderr b/api-proc-macro/tests/into_command_definition/enum_fails.stderr new file mode 100644 index 0000000..71e487f --- /dev/null +++ b/api-proc-macro/tests/into_command_definition/enum_fails.stderr @@ -0,0 +1,5 @@ +error: IntoCommandDefinition not supported for enum + --> tests/into_command_definition/enum_fails.rs:4:1 + | +4 | enum TestEnum { + | ^^^^ diff --git a/api-proc-macro/tests/into_command_definition/union_fails.rs b/api-proc-macro/tests/into_command_definition/union_fails.rs new file mode 100644 index 0000000..ab785a4 --- /dev/null +++ b/api-proc-macro/tests/into_command_definition/union_fails.rs @@ -0,0 +1,12 @@ +use api_proc_macro::IntoCommandDefinition; + +#[derive(IntoCommandDefinition)] +#[repr(C)] +union TestUnion { + f1: u32, + f2: f32, +} + +fn main() { + +} diff --git a/api-proc-macro/tests/into_command_definition/union_fails.stderr b/api-proc-macro/tests/into_command_definition/union_fails.stderr new file mode 100644 index 0000000..136df2c --- /dev/null +++ b/api-proc-macro/tests/into_command_definition/union_fails.stderr @@ -0,0 +1,5 @@ +error: IntoCommandDefinition not supported for union + --> tests/into_command_definition/union_fails.rs:5:1 + | +5 | union TestUnion { + | ^^^^^ diff --git a/api-proc-macro/tests/into_command_definition/unnamed_struct_fails.rs b/api-proc-macro/tests/into_command_definition/unnamed_struct_fails.rs new file mode 100644 index 0000000..5093ce0 --- /dev/null +++ b/api-proc-macro/tests/into_command_definition/unnamed_struct_fails.rs @@ -0,0 +1,8 @@ +use api_proc_macro::IntoCommandDefinition; + +#[derive(IntoCommandDefinition)] +struct TestUnnamedStruct(f32, f64, bool); + +fn main() { + +} diff --git a/api-proc-macro/tests/into_command_definition/unnamed_struct_fails.stderr b/api-proc-macro/tests/into_command_definition/unnamed_struct_fails.stderr new file mode 100644 index 0000000..6d80049 --- /dev/null +++ b/api-proc-macro/tests/into_command_definition/unnamed_struct_fails.stderr @@ -0,0 +1,5 @@ +error: IntoCommandDefinition not supported for unnamed structs + --> tests/into_command_definition/unnamed_struct_fails.rs:4:25 + | +4 | struct TestUnnamedStruct(f32, f64, bool); + | ^^^^^^^^^^^^^^^^ diff --git a/api-proc-macro/tests/test_derive_macro_into_command_definition.rs b/api-proc-macro/tests/test_derive_macro_into_command_definition.rs new file mode 100644 index 0000000..dda6e9e --- /dev/null +++ b/api-proc-macro/tests/test_derive_macro_into_command_definition.rs @@ -0,0 +1,170 @@ +use api_core::command::{ + Command, CommandHeader, CommandParameterDefinition, IntoCommandDefinition, +}; +use api_core::data_type::DataType; +use api_core::data_value::DataValue; +use api_proc_macro::IntoCommandDefinition; +use std::collections::HashMap; + +#[test] +fn test_enum_fails() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/into_command_definition/enum_fails.rs"); +} + +#[test] +fn test_union_fails() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/into_command_definition/union_fails.rs"); +} + +#[test] +fn test_unnamed_struct_fails() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/into_command_definition/unnamed_struct_fails.rs"); +} + +#[test] +fn test_basic_command() { + #[derive(IntoCommandDefinition)] + struct TestStruct { + #[allow(unused)] + a: f32, + #[allow(unused)] + b: f64, + #[allow(unused)] + c: bool, + } + + let command_definition = TestStruct::create("Test".to_string()); + + assert_eq!(command_definition.name, "Test"); + assert_eq!(command_definition.parameters.capacity(), 3); + assert_eq!( + command_definition.parameters[0], + CommandParameterDefinition { + name: "a".to_string(), + data_type: DataType::Float32, + } + ); + assert_eq!( + command_definition.parameters[1], + CommandParameterDefinition { + name: "b".to_string(), + data_type: DataType::Float64, + } + ); + assert_eq!( + command_definition.parameters[2], + CommandParameterDefinition { + name: "c".to_string(), + data_type: DataType::Boolean, + } + ); + + let mut parameters = HashMap::new(); + parameters.insert("a".to_string(), DataValue::Float32(1.0)); + parameters.insert("b".to_string(), DataValue::Float64(2.0)); + parameters.insert("c".to_string(), DataValue::Boolean(true)); + let result = TestStruct::parse(Command { + header: CommandHeader { + timestamp: Default::default(), + }, + parameters, + }) + .unwrap(); + assert_eq!(result.a, 1.0f32); + assert_eq!(result.b, 2.0f64); + assert_eq!(result.c, true); +} + +#[test] +fn test_generic_command() { + #[derive(IntoCommandDefinition)] + struct TestStruct { + #[allow(unused)] + a: T, + } + + let command_definition = TestStruct::::create("Test".to_string()); + assert_eq!(command_definition.name, "Test"); + assert_eq!(command_definition.parameters.capacity(), 1); + assert_eq!( + command_definition.parameters[0], + CommandParameterDefinition { + name: "a".to_string(), + data_type: DataType::Float32, + } + ); + let mut parameters = HashMap::new(); + parameters.insert("a".to_string(), DataValue::Float32(1.0)); + let result = TestStruct::::parse(Command { + header: CommandHeader { + timestamp: Default::default(), + }, + parameters, + }) + .unwrap(); + assert_eq!(result.a, 1.0f32); + + let command_definition = TestStruct::::create("Test2".to_string()); + assert_eq!(command_definition.name, "Test2"); + assert_eq!(command_definition.parameters.capacity(), 1); + assert_eq!( + command_definition.parameters[0], + CommandParameterDefinition { + name: "a".to_string(), + data_type: DataType::Float64, + } + ); + let mut parameters = HashMap::new(); + parameters.insert("a".to_string(), DataValue::Float64(2.0)); + let result = TestStruct::::parse(Command { + header: CommandHeader { + timestamp: Default::default(), + }, + parameters, + }) + .unwrap(); + assert_eq!(result.a, 2.0f64); + + let command_definition = TestStruct::::create("Test3".to_string()); + assert_eq!(command_definition.name, "Test3"); + assert_eq!(command_definition.parameters.capacity(), 1); + assert_eq!( + command_definition.parameters[0], + CommandParameterDefinition { + name: "a".to_string(), + data_type: DataType::Boolean, + } + ); + let mut parameters = HashMap::new(); + parameters.insert("a".to_string(), DataValue::Boolean(true)); + let result = TestStruct::::parse(Command { + header: CommandHeader { + timestamp: Default::default(), + }, + parameters, + }) + .unwrap(); + assert_eq!(result.a, true); +} + +#[test] +fn test_unit_command() { + #[derive(IntoCommandDefinition)] + struct TestStruct; + + let command_definition = TestStruct::create("Test".to_string()); + + assert_eq!(command_definition.name, "Test"); + assert_eq!(command_definition.parameters.capacity(), 0); + + TestStruct::parse(Command { + header: CommandHeader { + timestamp: Default::default(), + }, + parameters: HashMap::new(), + }) + .unwrap(); +} diff --git a/api/Cargo.toml b/api/Cargo.toml new file mode 100644 index 0000000..4ca8c50 --- /dev/null +++ b/api/Cargo.toml @@ -0,0 +1,24 @@ + +[package] +name = "api" +edition = "2021" +version = "0.1.0" +authors = ["Sergey "] + +[dependencies] +api-core = { path = "../api-core" } +api-proc-macro = { path = "../api-proc-macro" } +chrono = { workspace = true, features = ["serde"] } +derive_more = { workspace = true, features = ["from", "try_into"] } +futures-util = { workspace = true } +log = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["rt", "macros", "time"] } +tokio-tungstenite = { workspace = true, features = ["rustls-tls-native-roots"] } +tokio-util = { workspace = true } +uuid = { workspace = true, features = ["serde"] } + +[dev-dependencies] +env_logger = { workspace = true } diff --git a/api/src/client/command.rs b/api/src/client/command.rs new file mode 100644 index 0000000..63d834e --- /dev/null +++ b/api/src/client/command.rs @@ -0,0 +1,454 @@ +use crate::client::Client; +use crate::messages::command::CommandResponse; +use api_core::command::{CommandHeader, IntoCommandDefinition}; +use std::fmt::Display; +use std::sync::Arc; +use tokio::select; +use tokio_util::sync::CancellationToken; + +pub struct CommandRegistry { + client: Arc, +} + +impl CommandRegistry { + pub fn new(client: Arc) -> Self { + Self { client } + } + + pub fn register_handler( + &self, + command_name: impl Into, + mut callback: F, + ) -> CommandHandle + where + F: FnMut(CommandHeader, C) -> Result + Send + 'static, + { + let cancellation_token = CancellationToken::new(); + let result = CommandHandle { + cancellation_token: cancellation_token.clone(), + }; + let client = self.client.clone(); + + let command_definition = C::create(command_name.into()); + + tokio::spawn(async move { + while !cancellation_token.is_cancelled() { + // This would only fail if the sender closed while trying to insert data + // It would wait until space is made + let Ok(mut rx) = client + .register_callback_channel(command_definition.clone()) + .await + else { + continue; + }; + + loop { + // select used so that this loop gets broken if the token is cancelled + select!( + rx_value = rx.recv() => { + if let Some((cmd, responder)) = rx_value { + let header = cmd.header.clone(); + let response = match C::parse(cmd) { + Ok(cmd) => match callback(header, cmd) { + Ok(response) => CommandResponse { + success: true, + response, + }, + Err(err) => CommandResponse { + success: false, + response: err.to_string(), + }, + }, + Err(err) => CommandResponse { + success: false, + response: err.to_string(), + }, + }; + // This should only err if we had an error elsewhere + let _ = responder.send(response); + } else { + break; + } + }, + _ = cancellation_token.cancelled() => { break; }, + ); + } + } + }); + + result + } +} + +pub struct CommandHandle { + cancellation_token: CancellationToken, +} + +impl Drop for CommandHandle { + fn drop(&mut self) { + self.cancellation_token.cancel(); + } +} + +#[cfg(test)] +mod tests { + use crate::client::command::CommandRegistry; + use crate::client::tests::create_test_client; + use crate::client::Callback; + use crate::messages::callback::GenericCallbackError; + use crate::messages::command::CommandResponse; + use crate::messages::payload::RequestMessagePayload; + use crate::messages::telemetry_definition::TelemetryDefinitionResponse; + use crate::messages::ResponseMessage; + use api_core::command::{ + Command, CommandDefinition, CommandHeader, CommandParameterDefinition, + IntoCommandDefinition, IntoCommandDefinitionError, + }; + use api_core::data_type::DataType; + use std::collections::HashMap; + use std::convert::Infallible; + use std::sync::Arc; + use std::time::Duration; + use tokio::sync::oneshot; + use tokio::time::timeout; + use uuid::Uuid; + + struct CmdType { + #[allow(unused)] + param1: f32, + } + + impl IntoCommandDefinition for CmdType { + fn create(name: String) -> CommandDefinition { + CommandDefinition { + name, + parameters: vec![CommandParameterDefinition { + name: "param1".to_string(), + data_type: DataType::Float32, + }], + } + } + + fn parse(command: Command) -> Result { + Ok(Self { + param1: (*command.parameters.get("param1").ok_or_else(|| { + IntoCommandDefinitionError::ParameterMissing("param1".to_string()) + })?) + .try_into() + .map_err(|_| IntoCommandDefinitionError::MismatchedType { + parameter: "param1".to_string(), + expected: DataType::Float32, + })?, + }) + } + } + + #[tokio::test] + async fn simple_handler() { + // if _c drops then we are disconnected + let (mut rx, _c, client) = create_test_client(); + + let cmd_reg = CommandRegistry::new(Arc::new(client)); + + let _cmd_handle = cmd_reg.register_handler("cmd", |_, _: CmdType| { + Ok("success".to_string()) as Result<_, Infallible> + }); + + let msg = timeout(Duration::from_secs(1), rx.recv()) + .await + .unwrap() + .unwrap(); + let Callback::Registered(callback) = msg.callback else { + panic!("Incorrect Callback Type"); + }; + + let mut params = HashMap::new(); + params.insert("param1".to_string(), 0.0f32.into()); + + let (response_tx, response_rx) = oneshot::channel(); + timeout( + Duration::from_secs(1), + callback.send(( + ResponseMessage { + uuid: Uuid::new_v4(), + response: Some(msg.msg.uuid), + payload: Command { + header: CommandHeader { + timestamp: Default::default(), + }, + parameters: params, + } + .into(), + }, + response_tx, + )), + ) + .await + .unwrap() + .unwrap(); + let response = timeout(Duration::from_secs(1), response_rx) + .await + .unwrap() + .unwrap(); + let RequestMessagePayload::CommandResponse(CommandResponse { success, response }) = + response + else { + panic!("Unexpected Response Type"); + }; + assert!(success); + assert_eq!(response, "success"); + } + + #[tokio::test] + async fn handler_failed() { + // if _c drops then we are disconnected + let (mut rx, _c, client) = create_test_client(); + + let cmd_reg = CommandRegistry::new(Arc::new(client)); + + let _cmd_handle = cmd_reg.register_handler("cmd", |_, _: CmdType| { + Err("failure".into()) as Result<_, Box> + }); + + let msg = timeout(Duration::from_secs(1), rx.recv()) + .await + .unwrap() + .unwrap(); + let Callback::Registered(callback) = msg.callback else { + panic!("Incorrect Callback Type"); + }; + + let mut params = HashMap::new(); + params.insert("param1".to_string(), 1.0f32.into()); + + let (response_tx, response_rx) = oneshot::channel(); + timeout( + Duration::from_secs(1), + callback.send(( + ResponseMessage { + uuid: Uuid::new_v4(), + response: Some(msg.msg.uuid), + payload: Command { + header: CommandHeader { + timestamp: Default::default(), + }, + parameters: params, + } + .into(), + }, + response_tx, + )), + ) + .await + .unwrap() + .unwrap(); + let response = timeout(Duration::from_secs(1), response_rx) + .await + .unwrap() + .unwrap(); + let RequestMessagePayload::CommandResponse(CommandResponse { success, response }) = + response + else { + panic!("Unexpected Response Type"); + }; + assert!(!success); + assert_eq!(response, "failure"); + } + + #[tokio::test] + async fn parse_failed() { + // if _c drops then we are disconnected + let (mut rx, _c, client) = create_test_client(); + + let cmd_reg = CommandRegistry::new(Arc::new(client)); + + let _cmd_handle = cmd_reg.register_handler("cmd", |_, _: CmdType| { + Err("failure".into()) as Result<_, Box> + }); + + let msg = timeout(Duration::from_secs(1), rx.recv()) + .await + .unwrap() + .unwrap(); + let Callback::Registered(callback) = msg.callback else { + panic!("Incorrect Callback Type"); + }; + + let mut params = HashMap::new(); + params.insert("param1".to_string(), 1.0f64.into()); + + let (response_tx, response_rx) = oneshot::channel(); + timeout( + Duration::from_secs(1), + callback.send(( + ResponseMessage { + uuid: Uuid::new_v4(), + response: Some(msg.msg.uuid), + payload: Command { + header: CommandHeader { + timestamp: Default::default(), + }, + parameters: params, + } + .into(), + }, + response_tx, + )), + ) + .await + .unwrap() + .unwrap(); + let response = timeout(Duration::from_secs(1), response_rx) + .await + .unwrap() + .unwrap(); + let RequestMessagePayload::CommandResponse(CommandResponse { + success, + response: _, + }) = response + else { + panic!("Unexpected Response Type"); + }; + assert!(!success); + } + + #[tokio::test] + async fn wrong_message() { + // if _c drops then we are disconnected + let (mut rx, _c, client) = create_test_client(); + + let cmd_reg = CommandRegistry::new(Arc::new(client)); + + let _cmd_handle = + cmd_reg.register_handler("cmd", |_, _: CmdType| -> Result<_, Infallible> { + panic!("This should not happen"); + }); + + let msg = timeout(Duration::from_secs(1), rx.recv()) + .await + .unwrap() + .unwrap(); + let Callback::Registered(callback) = msg.callback else { + panic!("Incorrect Callback Type"); + }; + + let (response_tx, response_rx) = oneshot::channel(); + timeout( + Duration::from_secs(1), + callback.send(( + ResponseMessage { + uuid: Uuid::new_v4(), + response: Some(msg.msg.uuid), + payload: TelemetryDefinitionResponse { + uuid: Uuid::new_v4(), + } + .into(), + }, + response_tx, + )), + ) + .await + .unwrap() + .unwrap(); + let response = timeout(Duration::from_secs(1), response_rx) + .await + .unwrap() + .unwrap(); + let RequestMessagePayload::GenericCallbackError(err) = response else { + panic!("Unexpected Response Type"); + }; + assert_eq!(err, GenericCallbackError::MismatchedType); + } + + #[tokio::test] + async fn callback_closed() { + // if _c drops then we are disconnected + let (mut rx, _c, client) = create_test_client(); + + let cmd_reg = CommandRegistry::new(Arc::new(client)); + + let cmd_handle = + cmd_reg.register_handler("cmd", |_, _: CmdType| -> Result<_, Infallible> { + panic!("This should not happen"); + }); + + let msg = timeout(Duration::from_secs(1), rx.recv()) + .await + .unwrap() + .unwrap(); + let Callback::Registered(callback) = msg.callback else { + panic!("Incorrect Callback Type"); + }; + + // This should shut down the command handler + drop(cmd_handle); + + // Send a command + let mut params = HashMap::new(); + params.insert("param1".to_string(), 0.0f32.into()); + let (response_tx, response_rx) = oneshot::channel(); + timeout( + Duration::from_secs(1), + callback.send(( + ResponseMessage { + uuid: Uuid::new_v4(), + response: Some(msg.msg.uuid), + payload: Command { + header: CommandHeader { + timestamp: Default::default(), + }, + parameters: params, + } + .into(), + }, + response_tx, + )), + ) + .await + .unwrap() + .unwrap(); + + let response = timeout(Duration::from_secs(1), response_rx) + .await + .unwrap() + .unwrap(); + let RequestMessagePayload::GenericCallbackError(err) = response else { + panic!("Unexpected Response Type"); + }; + assert_eq!(err, GenericCallbackError::CallbackClosed); + } + + #[tokio::test] + async fn reconnect() { + // if _c drops then we are disconnected + let (mut rx, _c, client) = create_test_client(); + + let cmd_reg = CommandRegistry::new(Arc::new(client)); + + let _cmd_handle = + cmd_reg.register_handler("cmd", |_, _: CmdType| -> Result<_, Infallible> { + panic!("This should not happen"); + }); + + let msg = timeout(Duration::from_secs(1), rx.recv()) + .await + .unwrap() + .unwrap(); + let Callback::Registered(callback) = msg.callback else { + panic!("Incorrect Callback Type"); + }; + + println!("Dropping"); + drop(callback); + println!("Dropped"); + + // The command re-registers itself + let msg = timeout(Duration::from_secs(1), rx.recv()) + .await + .unwrap() + .unwrap(); + let Callback::Registered(_) = msg.callback else { + panic!("Incorrect Callback Type"); + }; + } +} diff --git a/api/src/client/config.rs b/api/src/client/config.rs new file mode 100644 index 0000000..18569df --- /dev/null +++ b/api/src/client/config.rs @@ -0,0 +1,11 @@ +pub struct ClientConfiguration { + pub send_buffer_size: usize, +} + +impl Default for ClientConfiguration { + fn default() -> Self { + Self { + send_buffer_size: 128, + } + } +} diff --git a/api/src/client/context.rs b/api/src/client/context.rs new file mode 100644 index 0000000..2299509 --- /dev/null +++ b/api/src/client/context.rs @@ -0,0 +1,594 @@ +use crate::client::config::ClientConfiguration; +use crate::client::error::{ConnectError, MessageError}; +use crate::client::{Callback, ClientChannel, OutgoingMessage, RegisteredCallback}; +use crate::messages::callback::GenericCallbackError; +use crate::messages::payload::RequestMessagePayload; +use crate::messages::{RequestMessage, ResponseMessage}; +use futures_util::{Sink, SinkExt, Stream, StreamExt}; +use log::{debug, error, info, trace, warn}; +use std::collections::HashMap; +use std::fmt::Display; +use std::sync::mpsc::sync_channel; +use std::thread; +use std::time::Duration; +use tokio::sync::{mpsc, oneshot, watch, RwLockWriteGuard}; +use tokio::time::sleep; +use tokio::{select, spawn}; +use tokio_tungstenite::connect_async; +use tokio_tungstenite::tungstenite::handshake::client::{Request, Response as TungResponse}; +use tokio_tungstenite::tungstenite::{Error as TungError, Message}; +use tokio_util::sync::CancellationToken; +use uuid::Uuid; + +pub struct ClientContext { + pub cancel: CancellationToken, + pub request: Request, + pub connected_state_tx: watch::Sender, + pub client_configuration: ClientConfiguration, +} + +impl ClientContext { + pub fn start(mut self, channel: ClientChannel) -> Result<(), ConnectError> { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + + let (tx, rx) = sync_channel::<()>(1); + + let _detached = thread::Builder::new() + .name("tlm-client".to_string()) + .spawn(move || { + runtime.block_on(async { + let mut write_lock = channel.write().await; + + // This cannot fail + let _ = tx.send(()); + + while !self.cancel.is_cancelled() { + write_lock = self + .run_connection(write_lock, &channel, connect_async) + .await; + } + drop(write_lock); + }); + })?; + + // This cannot fail + let _ = rx.recv(); + + Ok(()) + } + + async fn run_connection<'a, F, W, E>( + &mut self, + mut write_lock: RwLockWriteGuard<'a, mpsc::Sender>, + channel: &'a ClientChannel, + mut connection_fn: F, + ) -> RwLockWriteGuard<'a, mpsc::Sender> + where + F: AsyncFnMut(Request) -> Result<(W, TungResponse), TungError>, + W: Stream> + Sink + Unpin, + E: Display, + { + debug!("Attempting to Connect to {}", self.request.uri()); + let mut ws = match connection_fn(self.request.clone()).await { + Ok((ws, _)) => ws, + Err(e) => { + info!("Failed to Connect: {e}"); + sleep(Duration::from_secs(1)).await; + return write_lock; + } + }; + info!("Connected to {}", self.request.uri()); + + let (tx, rx) = mpsc::channel(self.client_configuration.send_buffer_size); + *write_lock = tx; + drop(write_lock); + + // Don't care about the previous value + let _ = self.connected_state_tx.send_replace(true); + + let close_connection = self.handle_connection(&mut ws, rx, channel).await; + + let write_lock = channel.write().await; + // Send this after grabbing the lock - to prevent extra contention when others try to grab + // the lock to use that as a signal that we have reconnected + let _ = self.connected_state_tx.send_replace(false); + if close_connection { + // Manually close to allow the impl trait to be used + if let Err(e) = ws.send(Message::Close(None)).await { + error!("Failed to Close the Connection: {e}"); + } + } + write_lock + } + + async fn handle_connection( + &mut self, + ws: &mut W, + mut rx: mpsc::Receiver, + channel: &ClientChannel, + ) -> bool + where + W: Stream> + Sink + Unpin, + >::Error: Display, + { + let mut callbacks = HashMap::::new(); + loop { + select! { + _ = self.cancel.cancelled() => { break; }, + Some(msg) = ws.next() => { + match msg { + Ok(msg) => { + match msg { + Message::Text(msg) => { + trace!("Incoming: {msg}"); + let msg: ResponseMessage = match serde_json::from_str(&msg) { + Ok(m) => m, + Err(e) => { + error!("Failed to deserialize {e}"); + break; + } + }; + self.handle_incoming(msg, &mut callbacks, channel).await; + } + Message::Binary(_) => unimplemented!("Binary Data Not Implemented"), + Message::Ping(data) => { + if let Err(e) = ws.send(Message::Pong(data)).await { + error!("Failed to send Pong {e}"); + break; + } + } + Message::Pong(_) => { + // Intentionally Left Empty + } + Message::Close(_) => { + debug!("Websocket Closed"); + return false; + } + Message::Frame(_) => unreachable!("Not Possible"), + } + } + Err(e) => { + error!("Receive Error {e}"); + break; + } + } + } + Some(msg) = rx.recv() => { + // Insert a callback if it isn't a None callback + if !matches!(msg.callback, Callback::None) { + callbacks.insert(msg.msg.uuid, msg.callback); + } + let msg = match serde_json::to_string(&msg.msg) { + Ok(m) => m, + Err(e) => { + error!("Encode Error {e}"); + break; + } + }; + trace!("Outgoing: {msg}"); + if let Err(e) = ws.send(Message::Text(msg.into())).await { + error!("Send Error {e}"); + break; + } + } + else => { break; }, + } + } + true + } + + async fn handle_incoming( + &mut self, + msg: ResponseMessage, + callbacks: &mut HashMap, + channel: &ClientChannel, + ) { + if let Some(response_uuid) = msg.response { + match callbacks.get(&response_uuid) { + Some(Callback::None) => { + callbacks.remove(&response_uuid); + unreachable!("We skip registering callbacks of None type"); + } + Some(Callback::Once(_)) => { + let Some(Callback::Once(callback)) = callbacks.remove(&response_uuid) else { + return; + }; + let _ = callback.send(msg); + } + Some(Callback::Registered(callback)) => { + let callback = callback.clone(); + spawn(Self::handle_registered_callback( + callback, + msg, + channel.clone(), + )); + } + None => { + warn!("No Callback Registered for {response_uuid}"); + } + } + } + } + + async fn handle_registered_callback( + callback: RegisteredCallback, + msg: ResponseMessage, + channel: ClientChannel, + ) { + let (tx, rx) = oneshot::channel(); + + let uuid = msg.uuid; + + let response = match callback.send((msg, tx)).await { + Err(_) => GenericCallbackError::CallbackClosed.into(), + Ok(()) => rx + .await + .unwrap_or_else(|_| GenericCallbackError::CallbackClosed.into()), + }; + + if let Err(e) = Self::send_response(channel, response, uuid).await { + error!("Failed to send response {e}"); + } + } + + async fn send_response( + channel: ClientChannel, + payload: RequestMessagePayload, + response_uuid: Uuid, + ) -> Result<(), MessageError> { + // If this failed that means we're in the middle of reconnecting, so our callbacks + // are all being cleaned up as-is. No response needed. + let sender = channel.try_read()?; + let data = sender.reserve().await?; + + data.send(OutgoingMessage { + msg: RequestMessage { + uuid: Uuid::new_v4(), + response: Some(response_uuid), + payload, + }, + callback: Callback::None, + }); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::messages::telemetry_definition::{ + TelemetryDefinitionRequest, TelemetryDefinitionResponse, + }; + use crate::test::mock_stream_sink::{create_mock_stream_sink, MockStreamSinkControl}; + use api_core::data_type::DataType; + use log::LevelFilter; + use std::future::Future; + use std::ops::Deref; + use tokio::sync::mpsc::Sender; + use tokio::sync::RwLock; + use tokio::time::timeout; + use tokio::try_join; + use tokio_tungstenite::tungstenite::client::IntoClientRequest; + use tokio_util::bytes::Bytes; + + async fn assert_client_interaction(future: F) + where + F: Send + + FnOnce( + Sender, + MockStreamSinkControl, Message>, + CancellationToken, + ) -> R + + 'static, + R: Future + Send, + { + let (control, stream_sink) = + create_mock_stream_sink::, Message>(); + + let cancel_token = CancellationToken::new(); + let inner_cancel_token = cancel_token.clone(); + let (connected_state_tx, _connected_state_rx) = watch::channel(false); + + let mut context = ClientContext { + cancel: cancel_token, + request: "mock".into_client_request().unwrap(), + connected_state_tx, + client_configuration: Default::default(), + }; + + let (tx, _rx) = mpsc::channel(1); + let channel = ClientChannel::new(RwLock::new(tx)); + let used_channel = channel.clone(); + + let write_lock = used_channel.write().await; + + let handle = spawn(async move { + let channel = channel; + let read = channel.read().await; + let sender = read.deref().clone(); + drop(read); + future(sender, control, inner_cancel_token).await; + }); + + let mut stream_sink = Some(stream_sink); + + let connection_fn = async |_: Request| { + let stream_sink = stream_sink.take().ok_or(TungError::ConnectionClosed)?; + + Ok((stream_sink, TungResponse::default())) as Result<(_, _), TungError> + }; + + let context_result = async { + drop( + context + .run_connection(write_lock, &used_channel, connection_fn) + .await, + ); + Ok(()) + }; + + try_join!(context_result, timeout(Duration::from_secs(1), handle),) + .unwrap() + .1 + .unwrap(); + } + + #[tokio::test] + async fn connection_closes_when_websocket_closes() { + let _ = env_logger::builder() + .is_test(true) + .filter_level(LevelFilter::Trace) + .try_init(); + + assert_client_interaction(|sender, mut control, _| async move { + let msg = Uuid::new_v4(); + sender + .send(OutgoingMessage { + msg: RequestMessage { + uuid: msg, + response: None, + payload: TelemetryDefinitionRequest { + name: "".to_string(), + data_type: DataType::Float32, + } + .into(), + }, + callback: Callback::None, + }) + .await + .unwrap(); + // We expect an outgoing message + assert!(matches!( + control.outgoing.recv().await.unwrap(), + Message::Text(_) + )); + // We receive an incoming close message + control + .incoming + .send(Ok(Message::Close(None))) + .await + .unwrap(); + // Then we expect the outgoing to close with no message + assert!(control.outgoing.recv().await.is_none()); + assert!(control.incoming.is_closed()); + }) + .await; + } + + #[tokio::test] + async fn connection_closes_when_cancelled() { + let _ = env_logger::builder() + .is_test(true) + .filter_level(LevelFilter::Trace) + .try_init(); + + assert_client_interaction(|_, mut control, cancel| async move { + cancel.cancel(); + // We expect an outgoing cancel message + assert!(matches!( + control.outgoing.recv().await.unwrap(), + Message::Close(_) + )); + // Then we expect to close with no message + assert!(control.outgoing.recv().await.is_none()); + assert!(control.incoming.is_closed()); + }) + .await; + } + + #[tokio::test] + async fn callback_request() { + let _ = env_logger::builder() + .is_test(true) + .filter_level(LevelFilter::Trace) + .try_init(); + + assert_client_interaction(|sender, mut control, _| async move { + let (callback_tx, callback_rx) = oneshot::channel(); + let msg = Uuid::new_v4(); + sender + .send(OutgoingMessage { + msg: RequestMessage { + uuid: msg, + response: None, + payload: TelemetryDefinitionRequest { + name: "".to_string(), + data_type: DataType::Float32, + } + .into(), + }, + callback: Callback::Once(callback_tx), + }) + .await + .unwrap(); + + // We expect an outgoing message + assert!(matches!( + control.outgoing.recv().await.unwrap(), + Message::Text(_) + )); + + // Then we get an incoming message for this callback + let response_message = ResponseMessage { + uuid: Uuid::new_v4(), + response: Some(msg), + payload: TelemetryDefinitionResponse { + uuid: Uuid::new_v4(), + } + .into(), + }; + control + .incoming + .send(Ok(Message::Text( + serde_json::to_string(&response_message).unwrap().into(), + ))) + .await + .unwrap(); + + // We expect the callback to run + let message = callback_rx.await.unwrap(); + // And give us the message we provided it + assert_eq!(message, response_message); + + // We receive an incoming close message + control + .incoming + .send(Ok(Message::Close(None))) + .await + .unwrap(); + // Then we expect the outgoing to close with no message + assert!(control.outgoing.recv().await.is_none()); + assert!(control.incoming.is_closed()); + }) + .await; + } + + #[tokio::test] + async fn callback_registered() { + let _ = env_logger::builder() + .is_test(true) + .filter_level(LevelFilter::Trace) + .try_init(); + + assert_client_interaction(|sender, mut control, _| async move { + let (callback_tx, mut callback_rx) = mpsc::channel(1); + let msg = Uuid::new_v4(); + sender + .send(OutgoingMessage { + msg: RequestMessage { + uuid: msg, + response: None, + payload: TelemetryDefinitionRequest { + name: "".to_string(), + data_type: DataType::Float32, + } + .into(), + }, + callback: Callback::Registered(callback_tx), + }) + .await + .unwrap(); + + // We expect an outgoing message + assert!(matches!( + control.outgoing.recv().await.unwrap(), + Message::Text(_) + )); + + // We handle the callback a few times + for _ in 0..5 { + // Then we get an incoming message for this callback + let response_message = ResponseMessage { + uuid: Uuid::new_v4(), + response: Some(msg), + payload: TelemetryDefinitionResponse { + uuid: Uuid::new_v4(), + } + .into(), + }; + control + .incoming + .send(Ok(Message::Text( + serde_json::to_string(&response_message).unwrap().into(), + ))) + .await + .unwrap(); + + // We expect the response + let (rx, responder) = callback_rx.recv().await.unwrap(); + // And give us the message we provided it + assert_eq!(rx, response_message); + // Then the response gets sent out + responder + .send( + TelemetryDefinitionRequest { + name: "".to_string(), + data_type: DataType::Float32, + } + .into(), + ) + .unwrap(); + + // We expect an outgoing message + assert!(matches!( + control.outgoing.recv().await.unwrap(), + Message::Text(_) + )); + } + + // We receive an incoming close message + control + .incoming + .send(Ok(Message::Close(None))) + .await + .unwrap(); + // Then we expect the outgoing to close with no message + assert!(control.outgoing.recv().await.is_none()); + assert!(control.incoming.is_closed()); + }) + .await; + } + + #[tokio::test] + async fn ping_pong() { + let _ = env_logger::builder() + .is_test(true) + .filter_level(LevelFilter::Trace) + .try_init(); + + assert_client_interaction(|_, mut control, _| async move { + // Expect a pong in response to a ping + let bytes = Bytes::from_owner(Uuid::new_v4().into_bytes()); + control + .incoming + .send(Ok(Message::Ping(bytes.clone()))) + .await + .unwrap(); + let Some(Message::Pong(pong_bytes)) = control.outgoing.recv().await else { + panic!("Expected Pong Response"); + }; + assert_eq!(bytes, pong_bytes); + + // Nothing should happen + control + .incoming + .send(Ok(Message::Pong(bytes.clone()))) + .await + .unwrap(); + + // We receive an incoming close message + control + .incoming + .send(Ok(Message::Close(None))) + .await + .unwrap(); + // Then we expect the outgoing to close with no message + assert!(control.outgoing.recv().await.is_none()); + assert!(control.incoming.is_closed()); + }) + .await; + } +} diff --git a/api/src/client/error.rs b/api/src/client/error.rs new file mode 100644 index 0000000..e5d5738 --- /dev/null +++ b/api/src/client/error.rs @@ -0,0 +1,37 @@ +use api_core::data_type::DataType; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ConnectError { + #[error(transparent)] + TungsteniteError(#[from] tokio_tungstenite::tungstenite::Error), + #[error(transparent)] + IoError(#[from] std::io::Error), +} + +#[derive(Error, Debug)] +pub enum MessageError { + #[error(transparent)] + TokioSendError(#[from] tokio::sync::mpsc::error::SendError<()>), + #[error(transparent)] + TokioTrySendError(#[from] tokio::sync::mpsc::error::TrySendError<()>), + #[error(transparent)] + TokioLockError(#[from] tokio::sync::TryLockError), + #[error("Incorrect Data Type. {expected} expected. {actual} actual.")] + IncorrectDataType { + expected: DataType, + actual: DataType, + }, +} + +#[derive(Error, Debug)] +pub enum RequestError { + #[error(transparent)] + TokioSendError(#[from] tokio::sync::mpsc::error::SendError<()>), + #[error(transparent)] + TokioLockError(#[from] tokio::sync::TryLockError), + #[error(transparent)] + RecvError(#[from] tokio::sync::oneshot::error::RecvError), + #[error(transparent)] + Inner(E), +} diff --git a/api/src/client/mod.rs b/api/src/client/mod.rs new file mode 100644 index 0000000..e4ac66b --- /dev/null +++ b/api/src/client/mod.rs @@ -0,0 +1,598 @@ +pub mod command; +mod config; +mod context; +pub mod error; +pub mod telemetry; + +use crate::client::config::ClientConfiguration; +use crate::client::error::{MessageError, RequestError}; +use crate::messages::callback::GenericCallbackError; +use crate::messages::payload::RequestMessagePayload; +use crate::messages::payload::ResponseMessagePayload; +use crate::messages::{ + ClientMessage, RegisterCallback, RequestMessage, RequestResponse, ResponseMessage, +}; +use context::ClientContext; +use error::ConnectError; +use std::sync::Arc; +use tokio::spawn; +use tokio::sync::{mpsc, oneshot, watch, RwLock}; +use tokio_tungstenite::tungstenite::client::IntoClientRequest; +use tokio_util::sync::CancellationToken; +use uuid::Uuid; + +type RegisteredCallback = mpsc::Sender<(ResponseMessage, oneshot::Sender)>; +type ClientChannel = Arc>>; + +#[derive(Debug)] +enum Callback { + None, + Once(oneshot::Sender), + Registered(RegisteredCallback), +} + +#[derive(Debug)] +struct OutgoingMessage { + msg: RequestMessage, + callback: Callback, +} + +pub struct Client { + cancel: CancellationToken, + channel: ClientChannel, + connected_state_rx: watch::Receiver, +} + +impl Client { + pub fn connect(request: R) -> Result + where + R: IntoClientRequest, + { + Self::connect_with_config(request, ClientConfiguration::default()) + } + + pub fn connect_with_config( + request: R, + config: ClientConfiguration, + ) -> Result + where + R: IntoClientRequest, + { + let (tx, _rx) = mpsc::channel(1); + let cancel = CancellationToken::new(); + let channel = Arc::new(RwLock::new(tx)); + let (connected_state_tx, connected_state_rx) = watch::channel(false); + let context = ClientContext { + cancel: cancel.clone(), + request: request.into_client_request()?, + connected_state_tx, + client_configuration: config, + }; + + context.start(channel.clone())?; + + Ok(Self { + cancel, + channel, + connected_state_rx, + }) + } + + pub async fn send_message(&self, msg: M) -> Result<(), MessageError> { + let sender = self.channel.read().await; + let data = sender.reserve().await?; + data.send(OutgoingMessage { + msg: RequestMessage { + uuid: Uuid::new_v4(), + response: None, + payload: msg.into(), + }, + callback: Callback::None, + }); + Ok(()) + } + + pub async fn send_message_if_connected( + &self, + msg: M, + ) -> Result<(), MessageError> { + let sender = self.channel.try_read()?; + let data = sender.reserve().await?; + data.send(OutgoingMessage { + msg: RequestMessage { + uuid: Uuid::new_v4(), + response: None, + payload: msg.into(), + }, + callback: Callback::None, + }); + Ok(()) + } + + pub fn try_send_message(&self, msg: M) -> Result<(), MessageError> { + let sender = self.channel.try_read()?; + let data = sender.try_reserve()?; + data.send(OutgoingMessage { + msg: RequestMessage { + uuid: Uuid::new_v4(), + response: None, + payload: msg.into(), + }, + callback: Callback::None, + }); + Ok(()) + } + + pub async fn send_request( + &self, + msg: M, + ) -> Result>::Error>> + { + let sender = self.channel.read().await; + let data = sender.reserve().await?; + + let (tx, rx) = oneshot::channel(); + data.send(OutgoingMessage { + msg: RequestMessage { + uuid: Uuid::new_v4(), + response: None, + payload: msg.into(), + }, + callback: Callback::Once(tx), + }); + + let response = rx.await?; + let response = M::Response::try_from(response.payload).map_err(RequestError::Inner)?; + + Ok(response) + } + + pub async fn register_callback_channel( + &self, + msg: M, + ) -> Result)>, MessageError> + where + ::Callback: Send + 'static, + ::Response: Send + 'static, + <::Callback as TryFrom>::Error: Send, + { + let sender = self.channel.read().await; + let data = sender.reserve().await?; + + let (inner_tx, mut inner_rx) = mpsc::channel(16); + let (outer_tx, outer_rx) = mpsc::channel(1); + + data.send(OutgoingMessage { + msg: RequestMessage { + uuid: Uuid::new_v4(), + response: None, + payload: msg.into(), + }, + callback: Callback::Registered(inner_tx), + }); + + spawn(async move { + // If the handler was unregistered we can stop + while let Some((msg, responder)) = inner_rx.recv().await { + let response: RequestMessagePayload = match M::Callback::try_from(msg.payload) { + Err(_) => GenericCallbackError::MismatchedType.into(), + Ok(o) => { + let (response_tx, response_rx) = oneshot::channel::(); + match outer_tx.send((o, response_tx)).await { + Err(_) => GenericCallbackError::CallbackClosed.into(), + Ok(()) => response_rx + .await + .map(M::Response::into) + .unwrap_or_else(|_| GenericCallbackError::CallbackClosed.into()), + } + } + }; + + if responder.send(response).is_err() { + // If the callback was unregistered we can stop + break; + } + } + println!("Exited Loop"); + }); + + Ok(outer_rx) + } + + pub async fn register_callback_fn( + &self, + msg: M, + mut f: F, + ) -> Result<(), MessageError> + where + F: FnMut(M::Callback) -> M::Response + Send + 'static, + { + let sender = self.channel.read().await; + let data = sender.reserve().await?; + + let (inner_tx, mut inner_rx) = mpsc::channel(16); + + data.send(OutgoingMessage { + msg: RequestMessage { + uuid: Uuid::new_v4(), + response: None, + payload: msg.into(), + }, + callback: Callback::Registered(inner_tx), + }); + + spawn(async move { + // If the handler was unregistered we can stop + while let Some((msg, responder)) = inner_rx.recv().await { + let response: RequestMessagePayload = match M::Callback::try_from(msg.payload) { + Err(_) => GenericCallbackError::MismatchedType.into(), + Ok(o) => f(o).into(), + }; + + if responder.send(response).is_err() { + // If the callback was unregistered we can stop + break; + } + } + }); + + Ok(()) + } + + pub async fn wait_connected(&self) { + let mut connected_rx = self.connected_state_rx.clone(); + + // If we aren't currently connected + if !*connected_rx.borrow_and_update() { + // Wait for a change notification + // If the channel is closed there is nothing we can do + let _ = connected_rx.changed().await; + } + } + + pub async fn wait_disconnected(&self) { + let mut connected_rx = self.connected_state_rx.clone(); + + // If we are currently connected + if *connected_rx.borrow_and_update() { + // Wait for a change notification + // If the channel is closed there is nothing we can do + let _ = connected_rx.changed().await; + } + } +} + +impl Drop for Client { + fn drop(&mut self) { + self.cancel.cancel(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::messages::command::CommandResponse; + use crate::messages::telemetry_definition::{ + TelemetryDefinitionRequest, TelemetryDefinitionResponse, + }; + use crate::messages::telemetry_entry::TelemetryEntry; + use api_core::command::{Command, CommandDefinition, CommandHeader}; + use api_core::data_type::DataType; + use chrono::Utc; + use futures_util::future::{select, Either}; + use futures_util::FutureExt; + use std::pin::pin; + use std::time::Duration; + use tokio::join; + use tokio::time::{sleep, timeout}; + + pub fn create_test_client() -> (mpsc::Receiver, watch::Sender, Client) { + let cancel = CancellationToken::new(); + let (tx, rx) = mpsc::channel(1); + let channel = Arc::new(RwLock::new(tx)); + let (connected_state_tx, connected_state_rx) = watch::channel(true); + let client = Client { + cancel, + channel, + connected_state_rx, + }; + (rx, connected_state_tx, client) + } + + #[tokio::test] + async fn send_message() { + let (mut rx, _, client) = create_test_client(); + + let msg_to_send = TelemetryEntry { + uuid: Uuid::new_v4(), + value: 0.0f32.into(), + timestamp: Utc::now(), + }; + let msg_send = timeout( + Duration::from_secs(1), + client.send_message(msg_to_send.clone()), + ); + let msg_recv = timeout(Duration::from_secs(1), rx.recv()); + + let (send, recv) = join!(msg_send, msg_recv); + send.unwrap().unwrap(); + let recv = recv.unwrap().unwrap(); + + assert!(matches!(recv.callback, Callback::None)); + assert!(recv.msg.response.is_none()); + // uuid should be random + + let RequestMessagePayload::TelemetryEntry(recv) = recv.msg.payload else { + panic!("Wrong Message Received") + }; + + assert_eq!(recv, msg_to_send); + } + + #[tokio::test] + async fn send_message_if_connected() { + let (mut rx, _, client) = create_test_client(); + + let msg_to_send = TelemetryEntry { + uuid: Uuid::new_v4(), + value: 0.0f32.into(), + timestamp: Utc::now(), + }; + let msg_send = timeout( + Duration::from_secs(1), + client.send_message_if_connected(msg_to_send.clone()), + ); + let msg_recv = timeout(Duration::from_secs(1), rx.recv()); + + let (send, recv) = join!(msg_send, msg_recv); + send.unwrap().unwrap(); + let recv = recv.unwrap().unwrap(); + + assert!(matches!(recv.callback, Callback::None)); + assert!(recv.msg.response.is_none()); + // uuid should be random + + let RequestMessagePayload::TelemetryEntry(recv) = recv.msg.payload else { + panic!("Wrong Message Received") + }; + + assert_eq!(recv, msg_to_send); + } + + #[tokio::test] + async fn send_message_if_connected_not_connected() { + let (_, connected_state_tx, client) = create_test_client(); + + let _lock = client.channel.write().await; + connected_state_tx.send_replace(false); + + let msg_to_send = TelemetryEntry { + uuid: Uuid::new_v4(), + value: 0.0f32.into(), + timestamp: Utc::now(), + }; + let msg_send = timeout( + Duration::from_secs(1), + client.send_message_if_connected(msg_to_send.clone()), + ); + + let Err(MessageError::TokioLockError(_)) = msg_send.await.unwrap() else { + panic!("Expected to Err due to lock being unavailable") + }; + } + + #[tokio::test] + async fn try_send_message() { + let (_tx, _, client) = create_test_client(); + + let msg_to_send = TelemetryEntry { + uuid: Uuid::new_v4(), + value: 0.0f32.into(), + timestamp: Utc::now(), + }; + client.try_send_message(msg_to_send.clone()).unwrap(); + let Err(MessageError::TokioTrySendError(_)) = client.try_send_message(msg_to_send.clone()) + else { + panic!("Expected the buffer to be full"); + }; + } + + #[tokio::test] + async fn send_request() { + let (mut tx, _, client) = create_test_client(); + + let msg_to_send = TelemetryDefinitionRequest { + name: "".to_string(), + data_type: DataType::Float32, + }; + let response = timeout( + Duration::from_secs(1), + client.send_request(msg_to_send.clone()), + ); + + let response_uuid = Uuid::new_v4(); + let outgoing_rx = timeout(Duration::from_secs(1), async { + let msg = tx.recv().await.unwrap(); + let Callback::Once(cb) = msg.callback else { + panic!("Wrong Callback Type") + }; + cb.send(ResponseMessage { + uuid: Uuid::new_v4(), + response: Some(msg.msg.uuid), + payload: TelemetryDefinitionResponse { + uuid: response_uuid, + } + .into(), + }) + .unwrap(); + }); + + let (response, outgoing_rx) = join!(response, outgoing_rx); + let response = response.unwrap().unwrap(); + outgoing_rx.unwrap(); + + assert_eq!(response.uuid, response_uuid); + } + + #[tokio::test] + async fn register_callback_channel() { + let (mut tx, _, client) = create_test_client(); + + let msg_to_send = CommandDefinition { + name: "".to_string(), + parameters: vec![], + }; + let mut response = timeout( + Duration::from_secs(1), + client.register_callback_channel(msg_to_send), + ) + .await + .unwrap() + .unwrap(); + + let outgoing_rx = timeout(Duration::from_secs(1), async { + let msg = tx.recv().await.unwrap(); + let Callback::Registered(cb) = msg.callback else { + panic!("Wrong Callback Type") + }; + + // Check that we get responses to the callback the expected number of times + for i in 0..5 { + let (tx, rx) = oneshot::channel(); + cb.send(( + ResponseMessage { + uuid: Uuid::new_v4(), + response: Some(msg.msg.uuid), + payload: Command { + header: CommandHeader { + timestamp: Utc::now(), + }, + parameters: Default::default(), + } + .into(), + }, + tx, + )) + .await + .unwrap(); + let RequestMessagePayload::CommandResponse(response) = rx.await.unwrap() else { + panic!("Unexpected Response Type"); + }; + assert_eq!(response.response, format!("{i}")); + } + }); + + let responder = timeout(Duration::from_secs(1), async { + for i in 0..5 { + let (_cmd, responder) = response.recv().await.unwrap(); + responder + .send(CommandResponse { + success: false, + response: format!("{i}"), + }) + .unwrap(); + } + }); + + let (response, outgoing_rx) = join!(responder, outgoing_rx); + response.unwrap(); + outgoing_rx.unwrap(); + } + + #[tokio::test] + async fn register_callback_fn() { + let (mut tx, _, client) = create_test_client(); + + let msg_to_send = CommandDefinition { + name: "".to_string(), + parameters: vec![], + }; + let mut index = 0usize; + timeout( + Duration::from_secs(1), + client.register_callback_fn(msg_to_send, move |_| { + index += 1; + CommandResponse { + success: false, + response: format!("{}", index - 1), + } + }), + ) + .await + .unwrap() + .unwrap(); + + timeout(Duration::from_secs(1), async { + let msg = tx.recv().await.unwrap(); + let Callback::Registered(cb) = msg.callback else { + panic!("Wrong Callback Type") + }; + + // Check that we get responses to the callback the expected number of times + for i in 0..3 { + let (tx, rx) = oneshot::channel(); + cb.send(( + ResponseMessage { + uuid: Uuid::new_v4(), + response: Some(msg.msg.uuid), + payload: Command { + header: CommandHeader { + timestamp: Utc::now(), + }, + parameters: Default::default(), + } + .into(), + }, + tx, + )) + .await + .unwrap(); + let RequestMessagePayload::CommandResponse(response) = rx.await.unwrap() else { + panic!("Unexpected Response Type"); + }; + assert_eq!(response.response, format!("{i}")); + } + }) + .await + .unwrap(); + } + + #[tokio::test] + async fn connected_disconnected() { + let (_, connected, client) = create_test_client(); + + // When we're connected we should return immediately + connected.send_replace(true); + client.wait_connected().now_or_never().unwrap(); + + // When we're disconnected we should return immediately + connected.send_replace(false); + client.wait_disconnected().now_or_never().unwrap(); + + let c2 = connected.clone(); + // When we're disconnected, we should not return immediately + let f1 = pin!(client.wait_connected()); + let f2 = pin!(async move { + sleep(Duration::from_millis(1)).await; + c2.send_replace(true); + }); + let r = select(f1, f2).await; + match r { + Either::Left(_) => panic!("Wait Connected Finished Before Connection Changed"), + Either::Right((_, other)) => timeout(Duration::from_secs(1), other).await.unwrap(), + } + + let c2 = connected.clone(); + // When we're disconnected, we should not return immediately + let f1 = pin!(client.wait_disconnected()); + let f2 = pin!(async move { + sleep(Duration::from_millis(1)).await; + c2.send_replace(false); + }); + let r = select(f1, f2).await; + match r { + Either::Left(_) => panic!("Wait Disconnected Finished Before Connection Changed"), + Either::Right((_, other)) => timeout(Duration::from_secs(1), other).await.unwrap(), + } + } +} diff --git a/api/src/client/telemetry.rs b/api/src/client/telemetry.rs new file mode 100644 index 0000000..5ae33b1 --- /dev/null +++ b/api/src/client/telemetry.rs @@ -0,0 +1,464 @@ +use crate::client::error::MessageError; +use crate::client::Client; +use crate::data_value::DataValue; +use crate::messages::telemetry_definition::TelemetryDefinitionRequest; +use crate::messages::telemetry_entry::TelemetryEntry; +use api_core::data_type::{DataType, ToDataType}; +use chrono::{DateTime, Utc}; +use std::marker::PhantomData; +use std::sync::Arc; +use tokio::sync::{oneshot, RwLock}; +use tokio_util::sync::CancellationToken; +use uuid::Uuid; + +pub struct TelemetryRegistry { + client: Arc, +} + +impl TelemetryRegistry { + pub fn new(client: Arc) -> Self { + Self { client } + } + + #[inline] + pub async fn register_generic( + &self, + name: impl Into, + data_type: DataType, + ) -> GenericTelemetryHandle { + // inner for compilation performance + async fn inner( + client: Arc, + name: String, + data_type: DataType, + ) -> GenericTelemetryHandle { + let cancellation_token = CancellationToken::new(); + let cancel_token = cancellation_token.clone(); + let stored_client = client.clone(); + + let response_uuid = Arc::new(RwLock::new(Uuid::nil())); + + let response_uuid_inner = response_uuid.clone(); + let (tx, rx) = oneshot::channel(); + + tokio::spawn(async move { + let mut write_lock = Some(response_uuid_inner.write().await); + let _ = tx.send(()); + while !cancel_token.is_cancelled() { + if let Ok(response) = client + .send_request(TelemetryDefinitionRequest { + name: name.clone(), + data_type, + }) + .await + { + let mut lock = match write_lock { + None => response_uuid_inner.write().await, + Some(lock) => lock, + }; + // Update the value in the lock + *lock = response.uuid; + // Set this value so the loop works + write_lock = None; + } + + client.wait_disconnected().await; + } + }); + + // Wait until the write lock is acquired + let _ = rx.await; + // Wait until the write lock is released for the first time + drop(response_uuid.read().await); + + GenericTelemetryHandle { + cancellation_token, + uuid: response_uuid, + client: stored_client, + data_type, + } + } + inner(self.client.clone(), name.into(), data_type).await + } + + #[inline] + pub async fn register(&self, name: impl Into) -> TelemetryHandle { + self.register_generic(name, T::DATA_TYPE).await.coerce() + } +} + +impl Drop for GenericTelemetryHandle { + fn drop(&mut self) { + self.cancellation_token.cancel(); + } +} + +pub struct GenericTelemetryHandle { + cancellation_token: CancellationToken, + uuid: Arc>, + client: Arc, + data_type: DataType, +} + +impl GenericTelemetryHandle { + pub async fn publish( + &self, + value: DataValue, + timestamp: DateTime, + ) -> Result<(), MessageError> { + if value.to_data_type() != self.data_type { + return Err(MessageError::IncorrectDataType { + expected: self.data_type, + actual: value.to_data_type(), + }); + } + let Ok(lock) = self.uuid.try_read() else { + return Ok(()); + }; + let uuid = *lock; + drop(lock); + + self.client + .send_message_if_connected(TelemetryEntry { + uuid, + value, + timestamp, + }) + .await + .or_else(|e| match e { + MessageError::TokioLockError(_) => Ok(()), + e => Err(e), + })?; + + Ok(()) + } + + #[inline] + pub async fn publish_now(&self, value: DataValue) -> Result<(), MessageError> { + self.publish(value, Utc::now()).await + } + + fn coerce>(self) -> TelemetryHandle { + TelemetryHandle:: { + generic_handle: self, + _phantom: PhantomData, + } + } +} + +pub struct TelemetryHandle { + generic_handle: GenericTelemetryHandle, + _phantom: PhantomData, +} + +impl TelemetryHandle { + pub fn to_generic(self) -> GenericTelemetryHandle { + self.generic_handle + } + pub fn as_generic(&self) -> &GenericTelemetryHandle { + &self.generic_handle + } +} + +impl> TelemetryHandle { + #[inline] + pub async fn publish(&self, value: T, timestamp: DateTime) -> Result<(), MessageError> { + self.as_generic().publish(value.into(), timestamp).await + } + + #[inline] + pub async fn publish_now(&self, value: T) -> Result<(), MessageError> { + self.publish(value, Utc::now()).await + } +} + +#[cfg(test)] +mod tests { + use crate::client::error::MessageError; + use crate::client::telemetry::TelemetryRegistry; + use crate::client::tests::create_test_client; + use crate::client::Callback; + use crate::messages::payload::RequestMessagePayload; + use crate::messages::telemetry_definition::{ + TelemetryDefinitionRequest, TelemetryDefinitionResponse, + }; + use crate::messages::telemetry_entry::TelemetryEntry; + use crate::messages::ResponseMessage; + use api_core::data_type::DataType; + use api_core::data_value::DataValue; + use futures_util::FutureExt; + use std::sync::Arc; + use std::time::Duration; + use tokio::task::yield_now; + use tokio::time::timeout; + use tokio::try_join; + use uuid::Uuid; + + #[tokio::test] + async fn generic() { + // if _c drops then we are disconnected + let (mut rx, _c, client) = create_test_client(); + + let tlm = TelemetryRegistry::new(Arc::new(client)); + let tlm_handle = tlm.register_generic("generic", DataType::Float32); + + let tlm_uuid = Uuid::new_v4(); + + let expected_rx = async { + let msg = rx.recv().await.unwrap(); + let Callback::Once(responder) = msg.callback else { + panic!("Expected Once Callback"); + }; + assert!(msg.msg.response.is_none()); + let RequestMessagePayload::TelemetryDefinitionRequest(TelemetryDefinitionRequest { + name, + data_type, + }) = msg.msg.payload + else { + panic!("Expected Telemetry Definition Request") + }; + assert_eq!(name, "generic".to_string()); + assert_eq!(data_type, DataType::Float32); + responder + .send(ResponseMessage { + uuid: Uuid::new_v4(), + response: Some(msg.msg.uuid), + payload: TelemetryDefinitionResponse { uuid: tlm_uuid }.into(), + }) + .unwrap(); + }; + + let (tlm_handle, _) = try_join!( + timeout(Duration::from_secs(1), tlm_handle), + timeout(Duration::from_secs(1), expected_rx), + ) + .unwrap(); + + assert_eq!(*tlm_handle.uuid.try_read().unwrap(), tlm_uuid); + + // This should NOT block if there is space in the queue + tlm_handle + .publish_now(0.0f32.into()) + .now_or_never() + .unwrap() + .unwrap(); + + let tlm_msg = timeout(Duration::from_secs(1), rx.recv()) + .await + .unwrap() + .unwrap(); + assert!(matches!(tlm_msg.callback, Callback::None)); + match tlm_msg.msg.payload { + RequestMessagePayload::TelemetryEntry(TelemetryEntry { uuid, value, .. }) => { + assert_eq!(uuid, tlm_uuid); + assert_eq!(value, DataValue::Float32(0.0f32)); + } + _ => panic!("Expected Telemetry Entry"), + } + } + + #[tokio::test] + async fn mismatched_type() { + let (mut rx, _, client) = create_test_client(); + + let tlm = TelemetryRegistry::new(Arc::new(client)); + let tlm_handle = tlm.register_generic("generic", DataType::Float32); + + let tlm_uuid = Uuid::new_v4(); + + let expected_rx = async { + let msg = rx.recv().await.unwrap(); + let Callback::Once(responder) = msg.callback else { + panic!("Expected Once Callback"); + }; + assert!(msg.msg.response.is_none()); + let RequestMessagePayload::TelemetryDefinitionRequest(TelemetryDefinitionRequest { + name, + data_type, + }) = msg.msg.payload + else { + panic!("Expected Telemetry Definition Request") + }; + assert_eq!(name, "generic".to_string()); + assert_eq!(data_type, DataType::Float32); + responder + .send(ResponseMessage { + uuid: Uuid::new_v4(), + response: Some(msg.msg.uuid), + payload: TelemetryDefinitionResponse { uuid: tlm_uuid }.into(), + }) + .unwrap(); + }; + + let (tlm_handle, _) = try_join!( + timeout(Duration::from_secs(1), tlm_handle), + timeout(Duration::from_secs(1), expected_rx), + ) + .unwrap(); + + assert_eq!(*tlm_handle.uuid.try_read().unwrap(), tlm_uuid); + + match timeout( + Duration::from_secs(1), + tlm_handle.publish_now(0.0f64.into()), + ) + .await + .unwrap() + { + Err(MessageError::IncorrectDataType { expected, actual }) => { + assert_eq!(expected, DataType::Float32); + assert_eq!(actual, DataType::Float64); + } + _ => panic!("Error Expected"), + } + } + + #[tokio::test] + async fn typed() { + // if _c drops then we are disconnected + let (mut rx, _c, client) = create_test_client(); + + let tlm = TelemetryRegistry::new(Arc::new(client)); + let tlm_handle = tlm.register::("typed"); + + let tlm_uuid = Uuid::new_v4(); + + let expected_rx = async { + let msg = rx.recv().await.unwrap(); + let Callback::Once(responder) = msg.callback else { + panic!("Expected Once Callback"); + }; + assert!(msg.msg.response.is_none()); + let RequestMessagePayload::TelemetryDefinitionRequest(TelemetryDefinitionRequest { + name, + data_type, + }) = msg.msg.payload + else { + panic!("Expected Telemetry Definition Request") + }; + assert_eq!(name, "typed".to_string()); + assert_eq!(data_type, DataType::Boolean); + responder + .send(ResponseMessage { + uuid: Uuid::new_v4(), + response: Some(msg.msg.uuid), + payload: TelemetryDefinitionResponse { uuid: tlm_uuid }.into(), + }) + .unwrap(); + }; + + let (tlm_handle, _) = try_join!( + timeout(Duration::from_secs(1), tlm_handle), + timeout(Duration::from_secs(1), expected_rx), + ) + .unwrap(); + + assert_eq!(*tlm_handle.as_generic().uuid.try_read().unwrap(), tlm_uuid); + + // This should NOT block if there is space in the queue + tlm_handle + .publish_now(true) + .now_or_never() + .unwrap() + .unwrap(); + // This should block as there should not be space in the queue + assert!(tlm_handle + .publish_now(false) + .now_or_never() + .is_none()); + + let tlm_msg = timeout(Duration::from_secs(1), rx.recv()) + .await + .unwrap() + .unwrap(); + assert!(matches!(tlm_msg.callback, Callback::None)); + match tlm_msg.msg.payload { + RequestMessagePayload::TelemetryEntry(TelemetryEntry { uuid, value, .. }) => { + assert_eq!(uuid, tlm_uuid); + assert_eq!(value, DataValue::Boolean(true)); + } + _ => panic!("Expected Telemetry Entry"), + } + + let _make_generic_again = tlm_handle.to_generic(); + } + + #[tokio::test] + async fn reconnect() { + // if _c drops then we are disconnected + let (mut rx, connected, client) = create_test_client(); + + let tlm = TelemetryRegistry::new(Arc::new(client)); + let tlm_handle = tlm.register_generic("generic", DataType::Float32); + + let tlm_uuid = Uuid::new_v4(); + + let expected_rx = async { + let msg = rx.recv().await.unwrap(); + let Callback::Once(responder) = msg.callback else { + panic!("Expected Once Callback"); + }; + assert!(msg.msg.response.is_none()); + let RequestMessagePayload::TelemetryDefinitionRequest(TelemetryDefinitionRequest { + name, + data_type, + }) = msg.msg.payload + else { + panic!("Expected Telemetry Definition Request") + }; + assert_eq!(name, "generic".to_string()); + assert_eq!(data_type, DataType::Float32); + responder + .send(ResponseMessage { + uuid: Uuid::new_v4(), + response: Some(msg.msg.uuid), + payload: TelemetryDefinitionResponse { uuid: tlm_uuid }.into(), + }) + .unwrap(); + }; + + let (tlm_handle, _) = try_join!( + timeout(Duration::from_secs(1), tlm_handle), + timeout(Duration::from_secs(1), expected_rx), + ) + .unwrap(); + + assert_eq!(*tlm_handle.uuid.try_read().unwrap(), tlm_uuid); + + // Notify Disconnect + connected.send_replace(false); + // Notify Reconnect + connected.send_replace(true); + + { + let new_tlm_uuid = Uuid::new_v4(); + + let msg = rx.recv().await.unwrap(); + let Callback::Once(responder) = msg.callback else { + panic!("Expected Once Callback"); + }; + assert!(msg.msg.response.is_none()); + let RequestMessagePayload::TelemetryDefinitionRequest(TelemetryDefinitionRequest { + name, + data_type, + }) = msg.msg.payload + else { + panic!("Expected Telemetry Definition Request") + }; + assert_eq!(name, "generic".to_string()); + assert_eq!(data_type, DataType::Float32); + responder + .send(ResponseMessage { + uuid: Uuid::new_v4(), + response: Some(msg.msg.uuid), + payload: TelemetryDefinitionResponse { uuid: new_tlm_uuid }.into(), + }) + .unwrap(); + + // Yield to the executor so that the UUIDs can be updated + yield_now().await; + + assert_eq!(*tlm_handle.uuid.try_read().unwrap(), new_tlm_uuid); + } + } +} diff --git a/api/src/lib.rs b/api/src/lib.rs new file mode 100644 index 0000000..d98d8ff --- /dev/null +++ b/api/src/lib.rs @@ -0,0 +1,15 @@ +pub mod client; +pub mod data_type { + pub use api_core::data_type::*; +} +pub mod data_value { + pub use api_core::data_value::*; +} +pub mod messages; + +pub mod macros { + pub use api_proc_macro::IntoCommandDefinition; +} + +#[cfg(test)] +pub mod test; diff --git a/api/src/messages/callback.rs b/api/src/messages/callback.rs new file mode 100644 index 0000000..6b63f4f --- /dev/null +++ b/api/src/messages/callback.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum GenericCallbackError { + CallbackClosed, + MismatchedType, +} diff --git a/api/src/messages/command.rs b/api/src/messages/command.rs new file mode 100644 index 0000000..fe56329 --- /dev/null +++ b/api/src/messages/command.rs @@ -0,0 +1,15 @@ +use crate::messages::RegisterCallback; +use serde::{Deserialize, Serialize}; + +pub use api_core::command::*; + +impl RegisterCallback for CommandDefinition { + type Callback = Command; + type Response = CommandResponse; +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CommandResponse { + pub success: bool, + pub response: String, +} diff --git a/api/src/messages/mod.rs b/api/src/messages/mod.rs new file mode 100644 index 0000000..c4abb6b --- /dev/null +++ b/api/src/messages/mod.rs @@ -0,0 +1,40 @@ +pub mod callback; +pub mod command; +pub mod payload; +pub mod telemetry_definition; +pub mod telemetry_entry; + +use payload::{RequestMessagePayload, ResponseMessagePayload}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RequestMessage { + pub uuid: Uuid, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub response: Option, + #[serde(flatten)] + pub payload: RequestMessagePayload, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ResponseMessage { + pub uuid: Uuid, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub response: Option, + #[serde(flatten)] + pub payload: ResponseMessagePayload, +} + +pub trait ClientMessage: Into {} + +pub trait RequestResponse: Into { + type Response: TryFrom; +} + +pub trait RegisterCallback: Into { + type Callback: TryFrom; + type Response: Into; +} diff --git a/api/src/messages/payload.rs b/api/src/messages/payload.rs new file mode 100644 index 0000000..f525f24 --- /dev/null +++ b/api/src/messages/payload.rs @@ -0,0 +1,23 @@ +use crate::messages::callback::GenericCallbackError; +use crate::messages::command::{Command, CommandDefinition, CommandResponse}; +use crate::messages::telemetry_definition::{ + TelemetryDefinitionRequest, TelemetryDefinitionResponse, +}; +use crate::messages::telemetry_entry::TelemetryEntry; +use derive_more::{From, TryInto}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, From)] +pub enum RequestMessagePayload { + TelemetryDefinitionRequest(TelemetryDefinitionRequest), + TelemetryEntry(TelemetryEntry), + GenericCallbackError(GenericCallbackError), + CommandDefinition(CommandDefinition), + CommandResponse(CommandResponse), +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, From, TryInto)] +pub enum ResponseMessagePayload { + TelemetryDefinitionResponse(TelemetryDefinitionResponse), + Command(Command), +} diff --git a/api/src/messages/telemetry_definition.rs b/api/src/messages/telemetry_definition.rs new file mode 100644 index 0000000..d6369fb --- /dev/null +++ b/api/src/messages/telemetry_definition.rs @@ -0,0 +1,19 @@ +use crate::data_type::DataType; +use crate::messages::RequestResponse; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct TelemetryDefinitionRequest { + pub name: String, + pub data_type: DataType, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct TelemetryDefinitionResponse { + pub uuid: Uuid, +} + +impl RequestResponse for TelemetryDefinitionRequest { + type Response = TelemetryDefinitionResponse; +} diff --git a/api/src/messages/telemetry_entry.rs b/api/src/messages/telemetry_entry.rs new file mode 100644 index 0000000..cb82ce9 --- /dev/null +++ b/api/src/messages/telemetry_entry.rs @@ -0,0 +1,14 @@ +use crate::data_value::DataValue; +use crate::messages::ClientMessage; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TelemetryEntry { + pub uuid: Uuid, + pub value: DataValue, + pub timestamp: DateTime, +} + +impl ClientMessage for TelemetryEntry {} diff --git a/api/src/test/mock_stream_sink.rs b/api/src/test/mock_stream_sink.rs new file mode 100644 index 0000000..aa60dee --- /dev/null +++ b/api/src/test/mock_stream_sink.rs @@ -0,0 +1,82 @@ +use futures_util::sink::{unfold, Unfold}; +use futures_util::{Sink, SinkExt, Stream}; +use std::fmt::Display; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; +use tokio::sync::mpsc; +use tokio::sync::mpsc::error::SendError; +use tokio::sync::mpsc::{Receiver, Sender}; + +pub struct MockStreamSinkControl { + pub incoming: Sender, + pub outgoing: Receiver, +} + +pub struct MockStreamSink { + stream_rx: Receiver, + sink_tx: Pin>>, +} + +impl Stream for MockStreamSink +where + Self: Unpin, +{ + type Item = T; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.stream_rx.poll_recv(cx) + } +} + +impl Sink for MockStreamSink +where + U1: FnMut(u32, R) -> U2, + U2: Future>, +{ + type Error = E; + + fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.sink_tx.poll_ready_unpin(cx) + } + + fn start_send(mut self: Pin<&mut Self>, item: R) -> Result<(), Self::Error> { + self.sink_tx.start_send_unpin(item) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.sink_tx.poll_flush_unpin(cx) + } + + fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.sink_tx.poll_close_unpin(cx) + } +} + +pub fn create_mock_stream_sink() -> ( + MockStreamSinkControl, + impl Stream + Sink, +) { + let (stream_tx, stream_rx) = mpsc::channel::(1); + let (sink_tx, sink_rx) = mpsc::channel::(1); + + let sink_tx = Arc::new(sink_tx); + + ( + MockStreamSinkControl { + incoming: stream_tx, + outgoing: sink_rx, + }, + MockStreamSink:: { + stream_rx, + sink_tx: Box::pin(unfold(0u32, move |_, item| { + let sink_tx = sink_tx.clone(); + async move { + sink_tx.send(item).await?; + Ok(0u32) as Result<_, SendError> + } + })), + }, + ) +} diff --git a/api/src/test/mod.rs b/api/src/test/mod.rs new file mode 100644 index 0000000..86ba89b --- /dev/null +++ b/api/src/test/mod.rs @@ -0,0 +1 @@ +pub mod mock_stream_sink; diff --git a/examples/simple_command/Cargo.toml b/examples/simple_command/Cargo.toml index b799df6..fdefbd1 100644 --- a/examples/simple_command/Cargo.toml +++ b/examples/simple_command/Cargo.toml @@ -4,11 +4,9 @@ name = "simple_command" edition = "2021" [dependencies] -server = { path = "../../server" } -tonic = "0.12.3" -tokio = { version = "1.43.0", features = ["rt-multi-thread", "signal"] } -chrono = "0.4.39" -tokio-util = "0.7.13" -num-traits = "0.2.19" -log = "0.4.29" -anyhow = "1.0.100" +anyhow = { workspace = true } +api = { path = "../../api" } +env_logger = { workspace = true } +log = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "signal"] } +tokio-util = { workspace = true } diff --git a/examples/simple_command/src/main.rs b/examples/simple_command/src/main.rs index ee6f512..5a9c832 100644 --- a/examples/simple_command/src/main.rs +++ b/examples/simple_command/src/main.rs @@ -1,82 +1,16 @@ -use chrono::DateTime; -use server::core::client_side_command::Inner; -use server::core::command_service_client::CommandServiceClient; -use server::core::telemetry_value::Value; -use server::core::{ - ClientSideCommand, Command, CommandDefinitionRequest, CommandParameterDefinition, - CommandResponse, TelemetryDataType, -}; +use api::client::command::CommandRegistry; +use api::client::Client; +use api::macros::IntoCommandDefinition; +use api::messages::command::CommandHeader; +use log::info; use std::error::Error; -use tokio::select; -use tokio::sync::mpsc; -use tokio::task::JoinHandle; +use std::sync::Arc; use tokio_util::sync::CancellationToken; -use tonic::codegen::tokio_stream::wrappers::ReceiverStream; -use tonic::codegen::tokio_stream::StreamExt; -use tonic::transport::Channel; - -struct CommandHandler { - handle: JoinHandle<()>, -} - -impl CommandHandler { - pub async fn new CommandResponse + Send + 'static>( - cancellation_token: CancellationToken, - client: &mut CommandServiceClient, - command_definition_request: CommandDefinitionRequest, - handler: F, - ) -> anyhow::Result { - let (tx, rx) = mpsc::channel(4); - - // The buffer size of 4 means this is safe to send immediately - tx.send(ClientSideCommand { - inner: Some(Inner::Request(command_definition_request)), - }) - .await?; - let response = client.new_command(ReceiverStream::new(rx)).await?; - let mut cmd_stream = response.into_inner(); - - let handle = tokio::spawn(async move { - loop { - select! { - _ = cancellation_token.cancelled() => break, - Some(msg) = cmd_stream.next() => { - match msg { - Ok(cmd) => { - let uuid = cmd.uuid.clone(); - let mut response = handler(cmd); - response.uuid = uuid; - match tx.send(ClientSideCommand { - inner: Some(Inner::Response(response)) - }).await { - Ok(()) => {}, - Err(e) => { - println!("SendError: {e}"); - break; - } - } - } - Err(e) => { - println!("Error: {e}"); - break; - } - } - }, - else => break, - } - } - }); - - Ok(Self { handle }) - } - - pub async fn join(self) -> anyhow::Result<()> { - Ok(self.handle.await?) - } -} #[tokio::main] async fn main() -> Result<(), Box> { + env_logger::init(); + let cancellation_token = CancellationToken::new(); { let cancellation_token = cancellation_token.clone(); @@ -86,56 +20,34 @@ async fn main() -> Result<(), Box> { }); } - let mut client = CommandServiceClient::connect("http://[::1]:50051").await?; + let client = Arc::new(Client::connect("ws://localhost:8080/backend")?); + let cmd = CommandRegistry::new(client); - let cmd_handler = CommandHandler::new( - cancellation_token, - &mut client, - CommandDefinitionRequest { - name: "simple_command/a".to_string(), - parameters: vec![ - CommandParameterDefinition { - name: "a".to_string(), - data_type: TelemetryDataType::Float32.into(), - }, - CommandParameterDefinition { - name: "b".to_string(), - data_type: TelemetryDataType::Float64.into(), - }, - CommandParameterDefinition { - name: "c".to_string(), - data_type: TelemetryDataType::Boolean.into(), - }, - ], - }, - |command| { - let timestamp = command.timestamp.expect("Missing Timestamp"); - let timestamp = DateTime::from_timestamp(timestamp.secs, timestamp.nanos as u32) - .expect("Could not construct date time"); - let Value::Float32(a) = command.parameters["a"].value.expect("Missing Value a") else { - panic!("Wrong Type a"); - }; - let Value::Float64(b) = command.parameters["b"].value.expect("Missing Value b") else { - panic!("Wrong Type b"); - }; - let Value::Boolean(c) = command.parameters["c"].value.expect("Missing Value c") else { - panic!("Wrong Type c"); - }; + let handle = cmd.register_handler("simple_command/a", handle_command); - println!("Command Received:\n timestamp: {timestamp}\n a: {a}\n b: {b}\n c: {c}"); + cancellation_token.cancelled().await; - CommandResponse { - uuid: command.uuid.clone(), - success: true, - response: format!( - "Successfully Received Command! timestamp: {timestamp} a: {a} b: {b} c: {c}" - ), - } - }, - ) - .await?; - - cmd_handler.join().await?; + // This will automatically drop when we return + drop(handle); Ok(()) } + +#[derive(IntoCommandDefinition)] +struct SimpleCommandA { + a: f32, + b: f64, + c: bool, +} + +fn handle_command(header: CommandHeader, command: SimpleCommandA) -> anyhow::Result { + let timestamp = header.timestamp; + let SimpleCommandA { a, b, c } = command; + + info!("Command Received:\n timestamp: {timestamp}\n a: {a}\n b: {b}\n c: {c}"); + + // This gets sent back to the source of the command + Ok(format!( + "Successfully Received Command! timestamp: {timestamp} a: {a} b: {b} c: {c}" + )) +} diff --git a/examples/simple_producer/Cargo.toml b/examples/simple_producer/Cargo.toml index e22533c..c105fc6 100644 --- a/examples/simple_producer/Cargo.toml +++ b/examples/simple_producer/Cargo.toml @@ -4,9 +4,11 @@ name = "simple_producer" edition = "2021" [dependencies] -server = { path = "../../server" } -tonic = "0.12.3" -tokio = { version = "1.43.0", features = ["rt-multi-thread", "signal"] } -chrono = "0.4.39" -tokio-util = "0.7.13" -num-traits = "0.2.19" +anyhow = { workspace = true } +api = { path = "../../api" } +chrono = { workspace = true } +env_logger = { workspace = true } +futures-util = { workspace = true } +num-traits = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "signal", "time", "macros"] } +tokio-util = { workspace = true } diff --git a/examples/simple_producer/src/main.rs b/examples/simple_producer/src/main.rs index 8aaecb7..e7ce4c3 100644 --- a/examples/simple_producer/src/main.rs +++ b/examples/simple_producer/src/main.rs @@ -1,213 +1,44 @@ -use chrono::DateTime; -use num_traits::float::FloatConst; -use server::core::telemetry_service_client::TelemetryServiceClient; -use server::core::telemetry_value::Value; -use server::core::{ - TelemetryDataType, TelemetryDefinitionRequest, TelemetryItem, TelemetryValue, Timestamp, Uuid, -}; -use std::error::Error; +use api::client::telemetry::TelemetryRegistry; +use api::client::Client; +use chrono::{TimeDelta, Utc}; +use futures_util::future::join_all; +use num_traits::FloatConst; +use std::f64; +use std::sync::Arc; use std::time::Duration; -use tokio::select; -use tokio::sync::mpsc; -use tokio::sync::mpsc::Sender; -use tokio::time::Instant; +use tokio::time::{sleep_until, Instant}; use tokio_util::sync::CancellationToken; -use tonic::codegen::tokio_stream::wrappers::ReceiverStream; -use tonic::codegen::tokio_stream::StreamExt; -use tonic::codegen::StdError; -use tonic::transport::Channel; -use tonic::Request; - -struct Telemetry { - client: TelemetryServiceClient, - tx: Sender, - cancel: CancellationToken, -} - -struct TelemetryItemHandle { - uuid: String, - tx: Sender, -} - -impl Telemetry { - pub async fn new(dst: D) -> Result> - where - D: TryInto, - D::Error: Into, - { - let mut client = TelemetryServiceClient::connect(dst).await?; - let client_stored = client.clone(); - let cancel = CancellationToken::new(); - let cancel_stored = cancel.clone(); - let (local_tx, mut local_rx) = mpsc::channel(16); - - tokio::spawn(async move { - while !cancel.is_cancelled() { - let (server_tx, server_rx) = mpsc::channel(16); - let response_stream = client - .insert_telemetry(ReceiverStream::new(server_rx)) - .await; - if let Ok(response_stream) = response_stream { - let mut response_stream = response_stream.into_inner(); - loop { - select! { - _ = cancel.cancelled() => { - break; - }, - Some(item) = local_rx.recv() => { - match server_tx.send(item).await { - Ok(_) => {} - Err(_) => break, - } - }, - Some(response) = response_stream.next() => { - match response { - Ok(_) => {} - Err(_) => { - break; - } - } - }, - else => break, - } - } - } else { - tokio::time::sleep(Duration::from_secs(1)).await; - } - } - }); - - Ok(Self { - client: client_stored, - tx: local_tx, - cancel: cancel_stored, - }) - } - - pub async fn register( - &mut self, - name: String, - data_type: TelemetryDataType, - ) -> Result> { - let response = self - .client - .new_telemetry(Request::new(TelemetryDefinitionRequest { - name, - data_type: data_type.into(), - })) - .await? - .into_inner(); - - let Some(uuid) = response.uuid else { - return Err("UUID Missing".into()); - }; - - Ok(TelemetryItemHandle { - uuid: uuid.value, - tx: self.tx.clone(), - }) - } -} - -impl Drop for Telemetry { - fn drop(&mut self) { - self.cancel.cancel(); - } -} - -impl TelemetryItemHandle { - pub async fn publish( - &self, - value: Value, - timestamp: DateTime, - ) -> Result<(), Box> { - let offset_from_unix_epoch = - timestamp - DateTime::from_timestamp(0, 0).expect("Could not get Unix epoch"); - self.tx - .send(TelemetryItem { - uuid: Some(Uuid { - value: self.uuid.clone(), - }), - value: Some(TelemetryValue { value: Some(value) }), - timestamp: Some(Timestamp { - secs: offset_from_unix_epoch.num_seconds(), - nanos: offset_from_unix_epoch.subsec_nanos(), - }), - }) - .await?; - Ok(()) - } -} #[tokio::main] -async fn main() -> Result<(), Box> { - let mut tlm = Telemetry::new("http://[::1]:50051").await?; +async fn main() -> anyhow::Result<()> { + env_logger::init(); - let index_handle = tlm - .register( - "simple_producer/time_offset".into(), - TelemetryDataType::Float64, - ) - .await?; + let client = Arc::new(Client::connect("ws://localhost:8080/backend")?); + let tlm = TelemetryRegistry::new(client); - let publish_offset = tlm - .register( - "simple_producer/publish_offset".into(), - TelemetryDataType::Float64, - ) - .await?; + let time_offset = tlm.register::("simple_producer/time_offset").await; - let await_offset = tlm - .register( - "simple_producer/await_offset".into(), - TelemetryDataType::Float64, - ) - .await?; + let publish_offset = tlm.register::("simple_producer/publish_offset").await; - let sin_tlm_handle = tlm - .register("simple_producer/sin".into(), TelemetryDataType::Float32) - .await?; - let cos_tlm_handle = tlm - .register("simple_producer/cos".into(), TelemetryDataType::Float64) - .await?; - let bool_tlm_handle = tlm - .register("simple_producer/bool".into(), TelemetryDataType::Boolean) - .await?; + let await_offset = tlm.register::("simple_producer/await_offset").await; - let sin2_tlm_handle = tlm - .register("simple_producer/sin2".into(), TelemetryDataType::Float32) - .await?; - let cos2_tlm_handle = tlm - .register("simple_producer/cos2".into(), TelemetryDataType::Float64) - .await?; + let bool_tlm_handle = tlm.register::("simple_producer/bool").await; - let sin3_tlm_handle = tlm - .register("simple_producer/sin3".into(), TelemetryDataType::Float32) - .await?; - let cos3_tlm_handle = tlm - .register("simple_producer/cos3".into(), TelemetryDataType::Float64) - .await?; + let sin_handles = join_all((1..=6).map(|i| { + tlm.register::(format!( + "simple_producer/sin{}", + if i == 1 { "".into() } else { i.to_string() } + )) + })) + .await; - let sin4_tlm_handle = tlm - .register("simple_producer/sin4".into(), TelemetryDataType::Float32) - .await?; - let cos4_tlm_handle = tlm - .register("simple_producer/cos4".into(), TelemetryDataType::Float64) - .await?; - - let sin5_tlm_handle = tlm - .register("simple_producer/sin5".into(), TelemetryDataType::Float32) - .await?; - let cos5_tlm_handle = tlm - .register("simple_producer/cos5".into(), TelemetryDataType::Float64) - .await?; - - let sin6_tlm_handle = tlm - .register("simple_producer/sin6".into(), TelemetryDataType::Float32) - .await?; - let cos6_tlm_handle = tlm - .register("simple_producer/cos6".into(), TelemetryDataType::Float64) - .await?; + let cos_handles = join_all((1..=6).map(|i| { + tlm.register::(format!( + "simple_producer/cos{}", + if i == 1 { "".into() } else { i.to_string() } + )) + })) + .await; let cancellation_token = CancellationToken::new(); { @@ -218,85 +49,48 @@ async fn main() -> Result<(), Box> { }); } - let start_time = chrono::Utc::now(); + let start_time = Utc::now(); let start_instant = Instant::now(); let mut next_time = start_instant; let mut index = 0; - let mut tasks = vec![]; while !cancellation_token.is_cancelled() { next_time += Duration::from_millis(10); index += 1; - tokio::time::sleep_until(next_time).await; - let publish_time = - start_time + chrono::TimeDelta::from_std(next_time - start_instant).unwrap(); + sleep_until(next_time).await; + let publish_time = start_time + TimeDelta::from_std(next_time - start_instant).unwrap(); let actual_time = Instant::now(); - tasks.push(index_handle.publish( - Value::Float64((actual_time - next_time).as_secs_f64()), - chrono::Utc::now(), - )); - tasks.push(sin_tlm_handle.publish( - Value::Float32((f32::TAU() * (index as f32) / (1000.0_f32)).sin()), - publish_time, - )); - tasks.push(cos_tlm_handle.publish( - Value::Float64((f64::TAU() * (index as f64) / (1000.0_f64)).cos()), - publish_time, - )); - tasks.push(bool_tlm_handle.publish(Value::Boolean(index % 1000 > 500), publish_time)); - tasks.push(sin2_tlm_handle.publish( - Value::Float32((f32::TAU() * (index as f32) / (500.0_f32)).sin()), - publish_time, - )); - tasks.push(cos2_tlm_handle.publish( - Value::Float64((f64::TAU() * (index as f64) / (500.0_f64)).cos()), - publish_time, - )); - tasks.push(sin3_tlm_handle.publish( - Value::Float32((f32::TAU() * (index as f32) / (333.0_f32)).sin()), - publish_time, - )); - tasks.push(cos3_tlm_handle.publish( - Value::Float64((f64::TAU() * (index as f64) / (333.0_f64)).cos()), - publish_time, - )); - tasks.push(sin4_tlm_handle.publish( - Value::Float32((f32::TAU() * (index as f32) / (250.0_f32)).sin()), - publish_time, - )); - tasks.push(cos4_tlm_handle.publish( - Value::Float64((f64::TAU() * (index as f64) / (250.0_f64)).cos()), - publish_time, - )); - tasks.push(sin5_tlm_handle.publish( - Value::Float32((f32::TAU() * (index as f32) / (200.0_f32)).sin()), - publish_time, - )); - tasks.push(cos5_tlm_handle.publish( - Value::Float64((f64::TAU() * (index as f64) / (200.0_f64)).cos()), - publish_time, - )); - tasks.push(sin6_tlm_handle.publish( - Value::Float32((f32::TAU() * (index as f32) / (166.0_f32)).sin()), - publish_time, - )); - tasks.push(cos6_tlm_handle.publish( - Value::Float64((f64::TAU() * (index as f64) / (166.0_f64)).cos()), - publish_time, - )); - tasks.push(publish_offset.publish( - Value::Float64((Instant::now() - actual_time).as_secs_f64()), - chrono::Utc::now(), - )); + // Due to how telemetry handles are implemented, unless the send buffer is full awaiting + // these will return immediately + time_offset + .publish_now((actual_time - next_time).as_secs_f64()) + .await?; + bool_tlm_handle + .publish(index % 1000 > 500, publish_time) + .await?; - for task in tasks.drain(..) { - task.await?; + for (i, sin) in sin_handles.iter().enumerate() { + sin.publish( + (f32::TAU() * (index as f32) / (1000.0_f32 / (i + 1) as f32)).sin(), + publish_time, + ) + .await?; + } + for (i, cos) in cos_handles.iter().enumerate() { + cos.publish( + (f64::TAU() * (index as f64) / (1000.0_f64 / (i + 1) as f64)).cos(), + publish_time, + ) + .await?; } - tasks.push(await_offset.publish( - Value::Float64((Instant::now() - actual_time).as_secs_f64()), - chrono::Utc::now(), - )); + publish_offset + .publish((Instant::now() - actual_time).as_secs_f64(), Utc::now()) + .await?; + + await_offset + .publish((Instant::now() - actual_time).as_secs_f64(), Utc::now()) + .await?; } Ok(()) diff --git a/server/Cargo.toml b/server/Cargo.toml index 3151585..0c263c5 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -6,25 +6,20 @@ version = "0.1.0" authors = ["Sergey "] [dependencies] -fern = "0.7.1" -log = "0.4.29" -prost = "0.13.5" -rand = "0.9.0" -tonic = { version = "0.12.3" } -tokio = { version = "1.43.0", features = ["rt-multi-thread", "signal", "fs"] } -chrono = "0.4.42" -actix-web = { version = "4.12.1", features = [ ] } -actix-ws = "0.3.0" -tokio-util = "0.7.17" -serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.145" -hex = "0.4.3" -papaya = "0.2.3" -thiserror = "2.0.17" -derive_more = { version = "2.1.0", features = ["from"] } -anyhow = "1.0.100" -sqlx = { version = "0.8.6", features = [ "runtime-tokio", "tls-native-tls", "sqlite" ] } -uuid = { version = "1.19.0", features = ["v4"] } - -[build-dependencies] -tonic-build = "0.12.3" +actix-web = { workspace = true, features = [ ] } +actix-ws = { workspace = true } +anyhow = { workspace = true } +api = { path = "../api" } +chrono = { workspace = true } +derive_more = { workspace = true, features = ["from"] } +fern = { workspace = true, features = ["colored"] } +futures-util = { workspace = true } +log = { workspace = true } +papaya = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +sqlx = { workspace = true, features = [ "runtime-tokio", "tls-rustls-ring-native-roots", "sqlite" ] } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "signal", "fs"] } +tokio-util = { workspace = true } +uuid = { workspace = true } diff --git a/server/build.rs b/server/build.rs index e763efd..d100728 100644 --- a/server/build.rs +++ b/server/build.rs @@ -1,5 +1,4 @@ fn main() -> Result<(), Box> { println!("cargo:rerun-if-changed=migrations"); - tonic_build::compile_protos("proto/core.proto")?; Ok(()) } diff --git a/server/proto/core.proto b/server/proto/core.proto deleted file mode 100644 index 18d0ea9..0000000 --- a/server/proto/core.proto +++ /dev/null @@ -1,83 +0,0 @@ - -syntax = "proto3"; -package core; - -enum TelemetryDataType { - Float32 = 0; - Float64 = 1; - Boolean = 2; -} - -message TelemetryValue { - oneof value { - float float_32 = 1; - double float_64 = 2; - bool boolean = 3; - } -} - -message UUID { - string value = 1; -} - -// UTC since UNIX -message Timestamp { - sfixed64 secs = 1; - sfixed32 nanos = 2; -} - -message TelemetryDefinitionRequest { - string name = 1; - TelemetryDataType data_type = 2; -} - -message TelemetryDefinitionResponse { - UUID uuid = 1; -} - -message TelemetryItem { - UUID uuid = 1; - TelemetryValue value = 2; - Timestamp timestamp = 3; -} - -message TelemetryInsertResponse { -} - -service TelemetryService { - rpc NewTelemetry (TelemetryDefinitionRequest) returns (TelemetryDefinitionResponse); - rpc InsertTelemetry (stream TelemetryItem) returns (stream TelemetryInsertResponse); -} - -message CommandParameterDefinition { - string name = 1; - TelemetryDataType data_type = 2; -} - -message CommandDefinitionRequest { - string name = 1; - repeated CommandParameterDefinition parameters = 2; -} - -message Command { - UUID uuid = 1; - Timestamp timestamp = 2; - map parameters = 3; -} - -message CommandResponse { - UUID uuid = 1; - bool success = 2; - string response = 3; -} - -message ClientSideCommand { - oneof inner { - CommandDefinitionRequest request = 1; - CommandResponse response = 2; - } -} - -service CommandService { - rpc NewCommand (stream ClientSideCommand) returns (stream Command); -} diff --git a/server/src/command/command_handle.rs b/server/src/command/command_handle.rs new file mode 100644 index 0000000..9c7b68d --- /dev/null +++ b/server/src/command/command_handle.rs @@ -0,0 +1,20 @@ +use uuid::Uuid; + +pub struct CommandHandle { + name: String, + uuid: Uuid, +} + +impl CommandHandle { + pub fn new(name: String, uuid: Uuid) -> Self { + Self { name, uuid } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn uuid(&self) -> &Uuid { + &self.uuid + } +} diff --git a/server/src/command/definition.rs b/server/src/command/definition.rs index d724c8a..70b0ceb 100644 --- a/server/src/command/definition.rs +++ b/server/src/command/definition.rs @@ -1,22 +1,5 @@ use crate::command::service::RegisteredCommand; -use crate::core::TelemetryDataType; -use crate::telemetry::data_type::tlm_data_type_deserializer; -use crate::telemetry::data_type::tlm_data_type_serializer; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct CommandParameterDefinition { - pub name: String, - #[serde(serialize_with = "tlm_data_type_serializer")] - #[serde(deserialize_with = "tlm_data_type_deserializer")] - pub data_type: TelemetryDataType, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct CommandDefinition { - pub name: String, - pub parameters: Vec, -} +use api::messages::command::{CommandDefinition, CommandParameterDefinition}; impl From for CommandDefinition { fn from(value: RegisteredCommand) -> Self { @@ -27,7 +10,7 @@ impl From for CommandDefinition { .parameters .into_iter() .map(|param| CommandParameterDefinition { - data_type: param.data_type(), + data_type: param.data_type, name: param.name, }) .collect(), diff --git a/server/src/command/error.rs b/server/src/command/error.rs index 698de99..c218826 100644 --- a/server/src/command/error.rs +++ b/server/src/command/error.rs @@ -1,6 +1,6 @@ -use crate::core::TelemetryDataType; use actix_web::http::StatusCode; use actix_web::ResponseError; +use api::data_type::DataType; use thiserror::Error; #[derive(Error, Debug)] @@ -14,7 +14,7 @@ pub enum Error { #[error("Incorrect Parameter Type for {name}. {expected_type:?} expected.")] WrongParameterType { name: String, - expected_type: TelemetryDataType, + expected_type: DataType, }, #[error("No Command Receiver")] NoCommandReceiver, diff --git a/server/src/command/mod.rs b/server/src/command/mod.rs index e3c4024..adb99ed 100644 --- a/server/src/command/mod.rs +++ b/server/src/command/mod.rs @@ -1,3 +1,4 @@ +pub mod command_handle; mod definition; pub mod error; pub mod service; diff --git a/server/src/command/service.rs b/server/src/command/service.rs index 784b736..15a284e 100644 --- a/server/src/command/service.rs +++ b/server/src/command/service.rs @@ -1,36 +1,40 @@ -use crate::command::definition::CommandDefinition; +use crate::command::command_handle::CommandHandle; use crate::command::error::Error as CmdError; use crate::command::error::Error::{ CommandFailure, CommandNotFound, FailedToReceiveResponse, FailedToSend, IncorrectParameterCount, MisingParameter, NoCommandReceiver, WrongParameterType, }; -use crate::core::telemetry_value::Value; -use crate::core::{ - Command, CommandDefinitionRequest, CommandResponse, TelemetryDataType, TelemetryValue, - Timestamp, Uuid, -}; -use chrono::{DateTime, Utc}; +use anyhow::bail; +use api::data_type::DataType; +use api::data_value::DataValue; +use api::messages::command::{Command, CommandDefinition, CommandHeader, CommandResponse}; +use api::messages::ResponseMessage; +use chrono::Utc; use log::error; use papaya::HashMap; use std::collections::HashMap as StdHashMap; -use tokio::sync::mpsc; use tokio::sync::oneshot; +use tokio::sync::{mpsc, RwLock}; +use uuid::Uuid; #[derive(Clone)] pub(super) struct RegisteredCommand { pub(super) name: String, - pub(super) definition: CommandDefinitionRequest, - tx: mpsc::Sender)>>, + pub(super) definition: CommandDefinition, + response_uuid: Uuid, + tx: mpsc::Sender, } pub struct CommandManagementService { registered_commands: HashMap, + outstanding_responses: RwLock>>, } impl CommandManagementService { pub fn new() -> Self { Self { registered_commands: HashMap::new(), + outstanding_responses: RwLock::new(StdHashMap::new()), } } @@ -52,26 +56,26 @@ impl CommandManagementService { .map(|registration| registration.clone().into()) } - pub async fn register_command( + pub fn register_command( &self, - command: CommandDefinitionRequest, - ) -> anyhow::Result)>>> { - let (tx, rx) = mpsc::channel(1); - - let registered_commands = self.registered_commands.pin_owned(); - if let Some(previous) = registered_commands.insert( - command.name.clone(), + uuid: Uuid, + command: CommandDefinition, + tx: mpsc::Sender, + ) -> anyhow::Result { + let registered_commands = self.registered_commands.pin(); + // We don't care about the previously registered command + let name = command.name.clone(); + let _ = registered_commands.insert( + name.clone(), RegisteredCommand { - name: command.name.clone(), + response_uuid: uuid, + name: name.clone(), definition: command, tx, }, - ) { - // If the receiver was already closed, we don't care (ignore error) - let _ = previous.tx.send(None).await; - } + ); - Ok(rx) + Ok(CommandHandle::new(name, uuid)) } pub async fn send_command( @@ -80,8 +84,6 @@ impl CommandManagementService { parameters: serde_json::Map, ) -> Result { let timestamp = Utc::now(); - let offset_from_unix_epoch = - timestamp - DateTime::from_timestamp(0, 0).expect("Could not get Unix epoch"); let name = name.into(); let registered_commands = self.registered_commands.pin(); @@ -100,27 +102,21 @@ impl CommandManagementService { let Some(param_value) = parameters.get(¶meter.name) else { return Err(MisingParameter(parameter.name.clone())); }; - let Some(param_value) = (match parameter.data_type() { - TelemetryDataType::Float32 => { - param_value.as_f64().map(|v| Value::Float32(v as f32)) - } - TelemetryDataType::Float64 => param_value.as_f64().map(Value::Float64), - TelemetryDataType::Boolean => param_value.as_bool().map(Value::Boolean), + let Some(param_value) = (match parameter.data_type { + DataType::Float32 => param_value.as_f64().map(|v| DataValue::Float32(v as f32)), + DataType::Float64 => param_value.as_f64().map(DataValue::Float64), + DataType::Boolean => param_value.as_bool().map(DataValue::Boolean), }) else { return Err(WrongParameterType { name: parameter.name.clone(), - expected_type: parameter.data_type(), + expected_type: parameter.data_type, }); }; - result_parameters.insert( - parameter.name.clone(), - TelemetryValue { - value: Some(param_value), - }, - ); + result_parameters.insert(parameter.name.clone(), param_value); } // Clone & Drop lets us use a standard pin instead of an owned pin + let response_uuid = registration.response_uuid; let tx = registration.tx.clone(); drop(registered_commands); @@ -128,23 +124,27 @@ impl CommandManagementService { return Err(NoCommandReceiver); } - let uuid = Uuid::random(); + let uuid = Uuid::new_v4(); let (response_tx, response_rx) = oneshot::channel(); + + { + let mut outstanding_responses = self.outstanding_responses.write().await; + outstanding_responses.insert(uuid, response_tx); + } + if let Err(e) = tx - .send(Some(( - Command { - uuid: Some(uuid), - timestamp: Some(Timestamp { - secs: offset_from_unix_epoch.num_seconds(), - nanos: offset_from_unix_epoch.subsec_nanos(), - }), + .send(ResponseMessage { + uuid, + response: Some(response_uuid), + payload: Command { + header: CommandHeader { timestamp }, parameters: result_parameters, - }, - response_tx, - ))) + } + .into(), + }) .await { - error!("Failed to Send Command: {e}"); + error!("Failed to Send Command {e}"); return Err(FailedToSend); } @@ -162,4 +162,33 @@ impl CommandManagementService { } }) } + + pub async fn handle_command_response( + &self, + uuid: Uuid, + response: CommandResponse, + ) -> anyhow::Result<()> { + let responder = { + let mut outstanding_responses = self.outstanding_responses.write().await; + outstanding_responses.remove(&uuid) + }; + match responder { + None => bail!("Unexpected Command Response for Command {uuid}"), + Some(response_tx) => { + if let Err(e) = response_tx.send(response) { + bail!("Failed to send Command Response {e:?}"); + } + } + }; + + Ok(()) + } + + pub fn unregister(&self, command_handle: CommandHandle) { + let registered_commands = self.registered_commands.pin(); + // We don't care if this succeeded + let _ = registered_commands.remove_if(command_handle.name(), |_, registration| { + registration.response_uuid == *command_handle.uuid() + }); + } } diff --git a/server/src/grpc/cmd.rs b/server/src/grpc/cmd.rs index 900201e..711b3ec 100644 --- a/server/src/grpc/cmd.rs +++ b/server/src/grpc/cmd.rs @@ -135,11 +135,14 @@ impl CommandService for CoreCommandService { } } for (key, sender) in in_progress.drain() { - if sender.send(CommandResponse { - uuid: Some(Uuid::from(key)), - success: false, - response: "Command Handler Shut Down".to_string(), - }).is_err() { + if sender + .send(CommandResponse { + uuid: Some(Uuid::from(key)), + success: false, + response: "Command Handler Shut Down".to_string(), + }) + .is_err() + { error!("Failed to send command response on shutdown"); } } diff --git a/server/src/grpc/mod.rs b/server/src/grpc/mod.rs deleted file mode 100644 index 1e7b01b..0000000 --- a/server/src/grpc/mod.rs +++ /dev/null @@ -1,44 +0,0 @@ -mod cmd; -mod tlm; - -use crate::command::service::CommandManagementService; -use crate::core::command_service_server::CommandServiceServer; -use crate::core::telemetry_service_server::TelemetryServiceServer; -use crate::grpc::cmd::CoreCommandService; -use crate::grpc::tlm::CoreTelemetryService; -use crate::telemetry::management_service::TelemetryManagementService; -use log::{error, info}; -use std::sync::Arc; -use tokio::task::JoinHandle; -use tokio_util::sync::CancellationToken; -use tonic::transport::Server; - -pub fn setup( - token: CancellationToken, - telemetry_management_service: Arc, - command_service: Arc, -) -> anyhow::Result> { - let addr = "[::1]:50051".parse()?; - Ok(tokio::spawn(async move { - let tlm_service = CoreTelemetryService { - tlm_management: telemetry_management_service, - cancellation_token: token.clone(), - }; - - let cmd_service = CoreCommandService { - command_service, - cancellation_token: token.clone(), - }; - - info!("Starting gRPC Server"); - let result = Server::builder() - .add_service(TelemetryServiceServer::new(tlm_service)) - .add_service(CommandServiceServer::new(cmd_service)) - .serve_with_shutdown(addr, token.cancelled_owned()) - .await; - - if let Err(err) = result { - error!("gRPC Server Encountered An Error: {err}"); - } - })) -} diff --git a/server/src/grpc/tlm.rs b/server/src/grpc/tlm.rs deleted file mode 100644 index fff260d..0000000 --- a/server/src/grpc/tlm.rs +++ /dev/null @@ -1,141 +0,0 @@ -use crate::core::telemetry_service_server::TelemetryService; -use crate::core::telemetry_value::Value; -use crate::core::{ - TelemetryDataType, TelemetryDefinitionRequest, TelemetryDefinitionResponse, - TelemetryInsertResponse, TelemetryItem, TelemetryValue, Uuid, -}; -use crate::telemetry::data_item::TelemetryDataItem; -use crate::telemetry::data_value::TelemetryDataValue; -use crate::telemetry::history::TelemetryHistory; -use crate::telemetry::management_service::TelemetryManagementService; -use chrono::{DateTime, SecondsFormat}; -use log::trace; -use std::pin::Pin; -use std::sync::Arc; -use tokio::select; -use tokio::sync::mpsc; -use tokio_util::sync::CancellationToken; -use tonic::codegen::tokio_stream::wrappers::ReceiverStream; -use tonic::codegen::tokio_stream::{Stream, StreamExt}; -use tonic::{Request, Response, Status, Streaming}; - -pub struct CoreTelemetryService { - pub tlm_management: Arc, - pub cancellation_token: CancellationToken, -} - -#[tonic::async_trait] -impl TelemetryService for CoreTelemetryService { - async fn new_telemetry( - &self, - request: Request, - ) -> Result, Status> { - trace!("CoreTelemetryService::new_telemetry"); - self.tlm_management - .register(request.into_inner()) - .map(|uuid| { - Response::new(TelemetryDefinitionResponse { - uuid: Some(Uuid { value: uuid }), - }) - }) - .map_err(|err| Status::already_exists(err.to_string())) - } - - type InsertTelemetryStream = - Pin> + Send>>; - - async fn insert_telemetry( - &self, - request: Request>, - ) -> Result, Status> { - trace!("CoreTelemetryService::insert_telemetry"); - - let cancel_token = self.cancellation_token.clone(); - let tlm_management = self.tlm_management.clone(); - let mut in_stream = request.into_inner(); - let (tx, rx) = mpsc::channel(128); - - tokio::spawn(async move { - loop { - select! { - _ = cancel_token.cancelled() => { - break; - }, - Some(message) = in_stream.next() => { - match message { - Ok(tlm_item) => { - tx - .send(Self::handle_new_tlm_item(&tlm_management, &tlm_item)) - .await - .expect("working rx"); - } - Err(err) => { - let _ = tx.send(Err(err)).await; - } - } - }, - else => break, - } - } - }); - - Ok(Response::new(Box::pin(ReceiverStream::new(rx)))) - } -} - -impl CoreTelemetryService { - #[allow(clippy::result_large_err)] - fn handle_new_tlm_item( - tlm_management: &Arc, - tlm_item: &TelemetryItem, - ) -> Result { - trace!("CoreTelemetryService::handle_new_tlm_item {:?}", tlm_item); - let Some(ref uuid) = tlm_item.uuid else { - return Err(Status::failed_precondition("UUID Missing")); - }; - let tlm_management_pin = tlm_management.pin(); - let Some(tlm_data) = tlm_management_pin.get_by_uuid(&uuid.value) else { - return Err(Status::not_found("Telemetry Item Not Found")); - }; - - let Some(TelemetryValue { value: Some(value) }) = tlm_item.value else { - return Err(Status::failed_precondition("Value Missing")); - }; - - let Some(timestamp) = tlm_item.timestamp else { - return Err(Status::failed_precondition("Timestamp Missing")); - }; - - let expected_type = match value { - Value::Float32(_) => TelemetryDataType::Float32, - Value::Float64(_) => TelemetryDataType::Float64, - Value::Boolean(_) => TelemetryDataType::Boolean, - }; - if expected_type != tlm_data.data.definition.data_type { - return Err(Status::failed_precondition("Data Type Mismatch")); - }; - - let Some(timestamp) = DateTime::from_timestamp(timestamp.secs, timestamp.nanos as u32) - else { - return Err(Status::invalid_argument("Failed to construct UTC DateTime")); - }; - - let value = match value { - Value::Float32(x) => TelemetryDataValue::Float32(x), - Value::Float64(x) => TelemetryDataValue::Float64(x), - Value::Boolean(x) => TelemetryDataValue::Boolean(x), - }; - let _ = tlm_data.data.data.send_replace(Some(TelemetryDataItem { - value: value.clone(), - timestamp: timestamp.to_rfc3339_opts(SecondsFormat::Millis, true), - })); - TelemetryHistory::insert_sync( - tlm_data.clone(), - tlm_management.history_service(), - value, - timestamp, - ); - - Ok(TelemetryInsertResponse {}) - } -} diff --git a/server/src/http/api/panels.rs b/server/src/http/api/panels.rs index 055ec44..2341b60 100644 --- a/server/src/http/api/panels.rs +++ b/server/src/http/api/panels.rs @@ -4,6 +4,7 @@ use crate::panels::PanelService; use actix_web::{delete, get, post, put, web, Responder}; use serde::Deserialize; use std::sync::Arc; +use uuid::Uuid; #[derive(Deserialize)] struct CreateParam { @@ -13,7 +14,7 @@ struct CreateParam { #[derive(Deserialize)] struct IdParam { - id: String, + id: Uuid, } #[post("/panel")] @@ -22,7 +23,7 @@ pub(super) async fn new( data: web::Json, ) -> Result { let uuid = panels.create(&data.name, &data.data).await?; - Ok(web::Json(uuid.value)) + Ok(web::Json(uuid)) } #[get("/panel")] @@ -38,12 +39,10 @@ pub(super) async fn get_one( panels: web::Data>, path: web::Path, ) -> Result { - let result = panels.read(path.id.clone().into()).await?; + let result = panels.read(path.id).await?; match result { Some(result) => Ok(web::Json(result)), - None => Err(HttpServerResultError::PanelUuidNotFound { - uuid: path.id.clone(), - }), + None => Err(HttpServerResultError::PanelUuidNotFound { uuid: path.id }), } } @@ -53,7 +52,7 @@ pub(super) async fn set( path: web::Path, data: web::Json, ) -> Result { - panels.update(path.id.clone().into(), data.0).await?; + panels.update(path.id, data.0).await?; Ok(web::Json(())) } @@ -62,6 +61,6 @@ pub(super) async fn delete( panels: web::Data>, path: web::Path, ) -> Result { - panels.delete(path.id.clone().into()).await?; + panels.delete(path.id).await?; Ok(web::Json(())) } diff --git a/server/src/http/api/tlm.rs b/server/src/http/api/tlm.rs index f400db8..38d7156 100644 --- a/server/src/http/api/tlm.rs +++ b/server/src/http/api/tlm.rs @@ -7,6 +7,7 @@ use serde::Deserialize; use std::sync::Arc; use std::time::Duration; use tokio::time::timeout; +use uuid::Uuid; #[get("/tlm/info/{name:[\\w\\d/_-]+}")] pub(super) async fn get_tlm_definition( @@ -36,13 +37,17 @@ struct HistoryQuery { resolution: i64, } -#[get("/tlm/history/{uuid:[0-9a-f]+}")] +#[get("/tlm/history/{uuid:[0-9a-f-]+}")] pub(super) async fn get_tlm_history( data_arc: web::Data>, uuid: web::Path, info: web::Query, ) -> Result { - let uuid = uuid.to_string(); + let Ok(uuid) = Uuid::parse_str(&uuid) else { + return Err(HttpServerResultError::InvalidUuid { + uuid: uuid.to_string(), + }); + }; trace!( "get_tlm_history {} from {} to {} resolution {}", uuid, diff --git a/server/src/http/backend/connection.rs b/server/src/http/backend/connection.rs new file mode 100644 index 0000000..1aac379 --- /dev/null +++ b/server/src/http/backend/connection.rs @@ -0,0 +1,117 @@ +use crate::command::command_handle::CommandHandle; +use crate::command::service::CommandManagementService; +use crate::telemetry::management_service::TelemetryManagementService; +use actix_ws::{AggregatedMessage, ProtocolError, Session}; +use anyhow::bail; +use api::messages::payload::RequestMessagePayload; +use api::messages::{RequestMessage, ResponseMessage}; +use std::sync::Arc; +use tokio::sync::mpsc::{Receiver, Sender}; +use uuid::Uuid; + +pub(super) struct BackendConnection { + session: Session, + tlm_management: Arc, + cmd_management: Arc, + tx: Sender, + commands: Vec, + pub rx: Receiver, + pub should_close: bool, +} + +impl BackendConnection { + pub fn new( + session: Session, + tlm_management: Arc, + cmd_management: Arc, + ) -> Self { + let (tx, rx) = tokio::sync::mpsc::channel::(128); + Self { + session, + tlm_management, + cmd_management, + tx, + commands: vec![], + rx, + should_close: false, + } + } + + async fn handle_request(&mut self, msg: RequestMessage) -> anyhow::Result<()> { + match msg.payload { + RequestMessagePayload::TelemetryDefinitionRequest(tlm_def) => { + self.tx + .send(ResponseMessage { + uuid: Uuid::new_v4(), + response: Some(msg.uuid), + payload: self.tlm_management.register(tlm_def)?.into(), + }) + .await?; + } + RequestMessagePayload::TelemetryEntry(tlm_entry) => { + self.tlm_management.add_tlm_item(tlm_entry)?; + } + RequestMessagePayload::GenericCallbackError(_) => todo!(), + RequestMessagePayload::CommandDefinition(def) => { + let cmd = self + .cmd_management + .register_command(msg.uuid, def, self.tx.clone())?; + self.commands.push(cmd); + } + RequestMessagePayload::CommandResponse(response) => match msg.response { + None => bail!("Command Response Payload Must Respond to a Command"), + Some(uuid) => { + self.cmd_management + .handle_command_response(uuid, response) + .await?; + } + }, + } + Ok(()) + } + + pub async fn handle_request_message( + &mut self, + msg: Result, + ) -> anyhow::Result<()> { + let msg = msg?; + match msg { + AggregatedMessage::Text(data) => { + self.handle_request(serde_json::from_str(&data)?).await?; + } + AggregatedMessage::Binary(_) => { + bail!("Binary Messages Unsupported"); + } + AggregatedMessage::Ping(bytes) => { + self.session.pong(&bytes).await?; + } + AggregatedMessage::Pong(_) => { + // Intentionally Ignore + } + AggregatedMessage::Close(_) => { + self.should_close = true; + } + } + Ok(()) + } + + pub async fn handle_response(&mut self, msg: ResponseMessage) -> anyhow::Result<()> { + let msg_json = serde_json::to_string(&msg)?; + self.session.text(msg_json).await?; + Ok(()) + } + + pub async fn cleanup(mut self) { + self.rx.close(); + // Clone here to prevent conflict with the Drop trait + let _ = self.session.clone().close(None).await; + } +} + +impl Drop for BackendConnection { + fn drop(&mut self) { + for command in self.commands.drain(..) { + self.cmd_management.unregister(command); + } + } +} diff --git a/server/src/http/backend/mod.rs b/server/src/http/backend/mod.rs new file mode 100644 index 0000000..8e33375 --- /dev/null +++ b/server/src/http/backend/mod.rs @@ -0,0 +1,60 @@ +use futures_util::stream::StreamExt; +mod connection; + +use crate::command::service::CommandManagementService; +use crate::http::backend::connection::BackendConnection; +use crate::telemetry::management_service::TelemetryManagementService; +use actix_web::{rt, web, HttpRequest, HttpResponse}; +use log::{error, trace}; +use std::sync::Arc; +use tokio::select; +use tokio_util::sync::CancellationToken; + +async fn backend_connect( + req: HttpRequest, + stream: web::Payload, + cancel_token: web::Data, + telemetry_management_service: web::Data>, + command_management_service: web::Data>, +) -> Result { + trace!("backend_connect"); + let (res, session, stream) = actix_ws::handle(&req, stream)?; + + let mut stream = stream + .aggregate_continuations() + // up to 1 MiB + .max_continuation_size(2_usize.pow(20)); + + let cancel_token = cancel_token.get_ref().clone(); + let tlm_management = telemetry_management_service.get_ref().clone(); + let cmd_management = command_management_service.get_ref().clone(); + + rt::spawn(async move { + let mut connection = BackendConnection::new(session, tlm_management, cmd_management); + while !connection.should_close { + let result = select! { + _ = cancel_token.cancelled() => { + connection.should_close = true; + Ok(()) + }, + Some(msg) = connection.rx.recv() => connection.handle_response(msg).await, + Some(msg) = stream.next() => connection.handle_request_message(msg).await, + else => { + connection.should_close = true; + Ok(()) + }, + }; + if let Err(e) = result { + error!("backend socket error: {e}"); + connection.should_close = true; + } + } + connection.cleanup().await; + }); + + Ok(res) +} + +pub fn setup_backend(cfg: &mut web::ServiceConfig) { + cfg.route("", web::get().to(backend_connect)); +} diff --git a/server/src/http/error.rs b/server/src/http/error.rs index 36ae486..a37fff9 100644 --- a/server/src/http/error.rs +++ b/server/src/http/error.rs @@ -3,13 +3,16 @@ use actix_web::http::header::ContentType; use actix_web::http::StatusCode; use actix_web::HttpResponse; use thiserror::Error; +use uuid::Uuid; #[derive(Error, Debug)] pub enum HttpServerResultError { #[error("Telemetry Name Not Found: {tlm}")] TlmNameNotFound { tlm: String }, + #[error("Invalid Uuid: {uuid}")] + InvalidUuid { uuid: String }, #[error("Telemetry Uuid Not Found: {uuid}")] - TlmUuidNotFound { uuid: String }, + TlmUuidNotFound { uuid: Uuid }, #[error("DateTime Parsing Error: {date_time}")] InvalidDateTime { date_time: String }, #[error("Timed out")] @@ -17,7 +20,7 @@ pub enum HttpServerResultError { #[error("Internal Error")] InternalError(#[from] anyhow::Error), #[error("Panel Uuid Not Found: {uuid}")] - PanelUuidNotFound { uuid: String }, + PanelUuidNotFound { uuid: Uuid }, #[error(transparent)] Command(#[from] crate::command::error::Error), } @@ -26,6 +29,7 @@ impl ResponseError for HttpServerResultError { fn status_code(&self) -> StatusCode { match self { HttpServerResultError::TlmNameNotFound { .. } => StatusCode::NOT_FOUND, + HttpServerResultError::InvalidUuid { .. } => StatusCode::BAD_REQUEST, HttpServerResultError::TlmUuidNotFound { .. } => StatusCode::NOT_FOUND, HttpServerResultError::InvalidDateTime { .. } => StatusCode::BAD_REQUEST, HttpServerResultError::Timeout => StatusCode::GATEWAY_TIMEOUT, diff --git a/server/src/http/mod.rs b/server/src/http/mod.rs index 250eafb..9b82a48 100644 --- a/server/src/http/mod.rs +++ b/server/src/http/mod.rs @@ -1,9 +1,11 @@ mod api; +mod backend; mod error; mod websocket; use crate::command::service::CommandManagementService; use crate::http::api::setup_api; +use crate::http::backend::setup_backend; use crate::http::websocket::setup_websocket; use crate::panels::PanelService; use crate::telemetry::management_service::TelemetryManagementService; @@ -31,6 +33,7 @@ pub async fn setup( .app_data(cancel_token.clone()) .app_data(panel_service.clone()) .app_data(command_service.clone()) + .service(web::scope("/backend").configure(setup_backend)) .service(web::scope("/ws").configure(setup_websocket)) .service(web::scope("/api").configure(setup_api)) .wrap(Logger::default()) diff --git a/server/src/http/websocket/mod.rs b/server/src/http/websocket/mod.rs index 6e2911c..031f3b4 100644 --- a/server/src/http/websocket/mod.rs +++ b/server/src/http/websocket/mod.rs @@ -6,6 +6,7 @@ use crate::telemetry::management_service::TelemetryManagementService; use actix_web::{rt, web, HttpRequest, HttpResponse}; use actix_ws::{AggregatedMessage, ProtocolError, Session}; use anyhow::anyhow; +use futures_util::StreamExt; use log::{error, trace}; use std::collections::HashMap; use std::sync::Arc; @@ -14,7 +15,7 @@ use tokio::select; use tokio::sync::mpsc::Sender; use tokio::time::{sleep_until, Instant}; use tokio_util::sync::CancellationToken; -use tonic::codegen::tokio_stream::StreamExt; +use uuid::Uuid; pub mod request; pub mod response; @@ -23,11 +24,11 @@ fn handle_register_tlm_listener( data: &Arc, request: RegisterTlmListenerRequest, tx: &Sender, - tlm_listeners: &mut HashMap, + tlm_listeners: &mut HashMap, ) { if let Some(tlm_data) = data.get_by_uuid(&request.uuid) { let token = CancellationToken::new(); - if let Some(token) = tlm_listeners.insert(tlm_data.definition.uuid.clone(), token.clone()) { + if let Some(token) = tlm_listeners.insert(tlm_data.definition.uuid, token.clone()) { token.cancel(); } let minimum_separation = Duration::from_millis(request.minimum_separation_ms as u64); @@ -46,7 +47,7 @@ fn handle_register_tlm_listener( ref_val.clone() }; let _ = tx.send(TlmValueResponse { - uuid: request.uuid.clone(), + uuid: request.uuid, value, }.into()).await; now @@ -65,7 +66,7 @@ fn handle_register_tlm_listener( fn handle_unregister_tlm_listener( request: UnregisterTlmListenerRequest, - tlm_listeners: &mut HashMap, + tlm_listeners: &mut HashMap, ) { if let Some(token) = tlm_listeners.remove(&request.uuid) { token.cancel(); @@ -76,7 +77,7 @@ async fn handle_websocket_message( data: &Arc, request: WebsocketRequest, tx: &Sender, - tlm_listeners: &mut HashMap, + tlm_listeners: &mut HashMap, ) { match request { WebsocketRequest::RegisterTlmListener(request) => { @@ -110,7 +111,7 @@ async fn handle_websocket_incoming( data: &Arc, session: &mut Session, tx: &Sender, - tlm_listeners: &mut HashMap, + tlm_listeners: &mut HashMap, ) -> anyhow::Result { match msg { Ok(AggregatedMessage::Close(_)) => Ok(false), @@ -130,7 +131,7 @@ async fn handle_websocket_incoming( } } -pub async fn websocket_connect( +async fn websocket_connect( req: HttpRequest, stream: web::Payload, data: web::Data>, diff --git a/server/src/http/websocket/request.rs b/server/src/http/websocket/request.rs index 6eaa984..58c3385 100644 --- a/server/src/http/websocket/request.rs +++ b/server/src/http/websocket/request.rs @@ -1,15 +1,16 @@ use derive_more::From; use serde::{Deserialize, Serialize}; +use uuid::Uuid; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RegisterTlmListenerRequest { - pub uuid: String, + pub uuid: Uuid, pub minimum_separation_ms: u32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UnregisterTlmListenerRequest { - pub uuid: String, + pub uuid: Uuid, } #[derive(Debug, Clone, Serialize, Deserialize, From)] diff --git a/server/src/http/websocket/response.rs b/server/src/http/websocket/response.rs index f056572..f2302c3 100644 --- a/server/src/http/websocket/response.rs +++ b/server/src/http/websocket/response.rs @@ -1,10 +1,11 @@ use crate::telemetry::data_item::TelemetryDataItem; use derive_more::From; use serde::{Deserialize, Serialize}; +use uuid::Uuid; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TlmValueResponse { - pub uuid: String, + pub uuid: Uuid, pub value: Option, } diff --git a/server/src/lib.rs b/server/src/lib.rs index 0fdf332..754e77f 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1,14 +1,8 @@ mod command; -mod grpc; mod http; mod panels; mod serialization; mod telemetry; -mod uuid; - -pub mod core { - tonic::include_proto!("core"); -} use crate::command::service::CommandManagementService; use crate::panels::PanelService; @@ -53,14 +47,11 @@ pub async fn setup() -> anyhow::Result<()> { let cmd = Arc::new(CommandManagementService::new()); - let grpc_server = grpc::setup(cancellation_token.clone(), tlm.clone(), cmd.clone())?; - let panel_service = PanelService::new(sqlite.clone()); let result = http::setup(cancellation_token.clone(), tlm.clone(), panel_service, cmd).await; cancellation_token.cancel(); result?; // result is dropped - grpc_server.await?; //grpc server is dropped drop(cancellation_token); // All cancellation tokens are now dropped sqlite.close().await; diff --git a/server/src/main.rs b/server/src/main.rs index 53f8098..70e0dcc 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,3 +1,4 @@ +use fern::colors::{Color, ColoredLevelConfig}; use std::env; use std::str::FromStr; @@ -9,14 +10,18 @@ async fn main() -> anyhow::Result<()> { Err(_) => log::LevelFilter::Info, }; + let colors = ColoredLevelConfig::new() + .info(Color::Green) + .debug(Color::Blue); + let mut log_config = fern::Dispatch::new() - .format(|out, message, record| { + .format(move |out, message, record| { out.finish(format_args!( - "[{}][{}][{}] {}", + "[{} {} {}] {}", chrono::Utc::now().format("%Y-%m-%d %H:%M:%S"), + colors.color(record.level()), record.target(), - record.level(), - message + message, )) }) .level(log::LevelFilter::Warn) @@ -27,5 +32,6 @@ async fn main() -> anyhow::Result<()> { log_config = log_config.chain(fern::log_file(log_file)?) } log_config.apply()?; + server::setup().await } diff --git a/server/src/panels/mod.rs b/server/src/panels/mod.rs index bb92760..1c91e04 100644 --- a/server/src/panels/mod.rs +++ b/server/src/panels/mod.rs @@ -1,9 +1,9 @@ pub mod panel; -use crate::core::Uuid; use crate::panels::panel::{PanelRequired, PanelUpdate}; use panel::Panel; use sqlx::SqlitePool; +use uuid::Uuid; pub struct PanelService { pool: SqlitePool, @@ -15,7 +15,8 @@ impl PanelService { } pub async fn create(&self, name: &str, data: &str) -> anyhow::Result { - let id = Uuid::random(); + let id = Uuid::new_v4(); + let id_string = id.to_string(); let mut transaction = self.pool.begin().await?; @@ -24,7 +25,7 @@ impl PanelService { INSERT INTO PANELS (id, name, data, deleted) VALUES ($1, $2, $3, FALSE); "#, - id.value, + id_string, name, data ) @@ -65,7 +66,7 @@ impl PanelService { WHERE id = $1 AND deleted = FALSE "#, ) - .bind(id.value) + .bind(id.to_string()) .fetch_optional(&mut *transaction) .await?; @@ -75,6 +76,7 @@ impl PanelService { } pub async fn update(&self, id: Uuid, data: PanelUpdate) -> anyhow::Result<()> { + let id = id.to_string(); let mut transaction = self.pool.begin().await?; if let Some(name) = data.name { @@ -84,7 +86,7 @@ impl PanelService { SET name = $2 WHERE id = $1; "#, - id.value, + id, name ) .execute(&mut *transaction) @@ -97,7 +99,7 @@ impl PanelService { SET data = $2 WHERE id = $1; "#, - id.value, + id, data ) .execute(&mut *transaction) @@ -110,6 +112,7 @@ impl PanelService { } pub async fn delete(&self, id: Uuid) -> anyhow::Result<()> { + let id = id.to_string(); let mut transaction = self.pool.begin().await?; let _ = sqlx::query!( @@ -118,7 +121,7 @@ impl PanelService { SET deleted = TRUE WHERE id = $1; "#, - id.value, + id, ) .execute(&mut *transaction) .await?; diff --git a/server/src/telemetry/data_item.rs b/server/src/telemetry/data_item.rs index 8c7e482..c5b2588 100644 --- a/server/src/telemetry/data_item.rs +++ b/server/src/telemetry/data_item.rs @@ -1,8 +1,8 @@ -use crate::telemetry::data_value::TelemetryDataValue; +use api::data_value::DataValue; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TelemetryDataItem { - pub value: TelemetryDataValue, + pub value: DataValue, pub timestamp: String, } diff --git a/server/src/telemetry/data_type.rs b/server/src/telemetry/data_type.rs deleted file mode 100644 index a19c64d..0000000 --- a/server/src/telemetry/data_type.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::core::TelemetryDataType; -use serde::de::Visitor; -use serde::{Deserializer, Serializer}; -use std::fmt::Formatter; - -pub fn tlm_data_type_serializer( - tlm_data_type: &TelemetryDataType, - serializer: S, -) -> Result -where - S: Serializer, -{ - serializer.serialize_str(tlm_data_type.as_str_name()) -} - -struct TlmDataTypeVisitor; - -impl Visitor<'_> for TlmDataTypeVisitor { - type Value = TelemetryDataType; - - fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { - formatter.write_str("A &str") - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - TelemetryDataType::from_str_name(v).ok_or(E::custom("Invalid TelemetryDataType")) - } -} - -pub fn tlm_data_type_deserializer<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - deserializer.deserialize_str(TlmDataTypeVisitor) -} diff --git a/server/src/telemetry/data_value.rs b/server/src/telemetry/data_value.rs deleted file mode 100644 index 328f2f8..0000000 --- a/server/src/telemetry/data_value.rs +++ /dev/null @@ -1,8 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum TelemetryDataValue { - Float32(f32), - Float64(f64), - Boolean(bool), -} diff --git a/server/src/telemetry/definition.rs b/server/src/telemetry/definition.rs index 95deb36..fb17fa0 100644 --- a/server/src/telemetry/definition.rs +++ b/server/src/telemetry/definition.rs @@ -1,13 +1,10 @@ -use crate::core::TelemetryDataType; -use crate::telemetry::data_type::tlm_data_type_deserializer; -use crate::telemetry::data_type::tlm_data_type_serializer; +use api::data_type::DataType; use serde::{Deserialize, Serialize}; +use uuid::Uuid; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TelemetryDefinition { - pub uuid: String, + pub uuid: Uuid, pub name: String, - #[serde(serialize_with = "tlm_data_type_serializer")] - #[serde(deserialize_with = "tlm_data_type_deserializer")] - pub data_type: TelemetryDataType, + pub data_type: DataType, } diff --git a/server/src/telemetry/history.rs b/server/src/telemetry/history.rs index 78b36a2..ab5802f 100644 --- a/server/src/telemetry/history.rs +++ b/server/src/telemetry/history.rs @@ -1,10 +1,10 @@ -use crate::core::TelemetryDataType; use crate::serialization::file_ext::{ReadExt, WriteExt}; use crate::telemetry::data::TelemetryData; use crate::telemetry::data_item::TelemetryDataItem; -use crate::telemetry::data_value::TelemetryDataValue; use crate::telemetry::definition::TelemetryDefinition; use anyhow::{anyhow, ensure, Context}; +use api::data_type::DataType; +use api::data_value::DataValue; use chrono::{DateTime, DurationRound, SecondsFormat, TimeDelta, Utc}; use log::{error, info}; use std::cmp::min; @@ -44,7 +44,7 @@ fn update_next_from( } struct SegmentData { - values: Vec, + values: Vec, timestamps: Vec>, } @@ -66,7 +66,7 @@ impl HistorySegmentRam { } } - fn insert(&self, value: TelemetryDataValue, timestamp: DateTime) { + fn insert(&self, value: DataValue, timestamp: DateTime) { if timestamp < self.start || timestamp >= self.end { return; } @@ -121,7 +121,7 @@ impl HistorySegmentRam { next_from, ); result.push(TelemetryDataItem { - value: data.values[i].clone(), + value: data.values[i], timestamp: t.to_rfc3339_opts(SecondsFormat::Millis, true), }); } @@ -196,9 +196,9 @@ impl HistorySegmentFile { // Write all the values for value in &data.values { match value { - TelemetryDataValue::Float32(value) => file.write_data::(*value)?, - TelemetryDataValue::Float64(value) => file.write_data::(*value)?, - TelemetryDataValue::Boolean(value) => file.write_data::(*value)?, + DataValue::Float32(value) => file.write_data::(*value)?, + DataValue::Float64(value) => file.write_data::(*value)?, + DataValue::Boolean(value) => file.write_data::(*value)?, } } @@ -215,10 +215,7 @@ impl HistorySegmentFile { }) } - fn load_to_ram( - mut self, - telemetry_data_type: TelemetryDataType, - ) -> anyhow::Result { + fn load_to_ram(mut self, telemetry_data_type: DataType) -> anyhow::Result { let mut segment_data = SegmentData { values: Vec::with_capacity(self.length as usize), timestamps: Vec::with_capacity(self.length as usize), @@ -281,7 +278,7 @@ impl HistorySegmentFile { from: DateTime, to: DateTime, maximum_resolution: TimeDelta, - telemetry_data_type: TelemetryDataType, + telemetry_data_type: DataType, ) -> anyhow::Result<(DateTime, Vec)> { self.file_position = 0; self.file.seek(SeekFrom::Start(0))?; @@ -334,22 +331,19 @@ impl HistorySegmentFile { self.read_date_time() } - fn read_telemetry_item( - &mut self, - telemetry_data_type: TelemetryDataType, - ) -> anyhow::Result { + fn read_telemetry_item(&mut self, telemetry_data_type: DataType) -> anyhow::Result { match telemetry_data_type { - TelemetryDataType::Float32 => { + DataType::Float32 => { self.file_position += 4; - Ok(TelemetryDataValue::Float32(self.file.read_data::()?)) + Ok(DataValue::Float32(self.file.read_data::()?)) } - TelemetryDataType::Float64 => { + DataType::Float64 => { self.file_position += 8; - Ok(TelemetryDataValue::Float64(self.file.read_data::()?)) + Ok(DataValue::Float64(self.file.read_data::()?)) } - TelemetryDataType::Boolean => { + DataType::Boolean => { self.file_position += 1; - Ok(TelemetryDataValue::Boolean(self.file.read_data::()?)) + Ok(DataValue::Boolean(self.file.read_data::()?)) } } } @@ -357,12 +351,12 @@ impl HistorySegmentFile { fn get_telemetry_item( &mut self, index: u64, - telemetry_data_type: TelemetryDataType, - ) -> anyhow::Result { + telemetry_data_type: DataType, + ) -> anyhow::Result { let item_length = match telemetry_data_type { - TelemetryDataType::Float32 => 4, - TelemetryDataType::Float64 => 8, - TelemetryDataType::Boolean => 1, + DataType::Float32 => 4, + DataType::Float64 => 8, + DataType::Boolean => 1, }; let desired_position = Self::HEADER_LENGTH + self.length * Self::TIMESTAMP_LENGTH + index * item_length; @@ -429,7 +423,7 @@ impl TelemetryHistory { history_segment_ram: HistorySegmentRam, ) -> JoinHandle<()> { let mut path = service.data_root_folder.clone(); - path.push(&self.data.definition.uuid); + path.push(self.data.definition.uuid.as_hyphenated().to_string()); spawn_blocking(move || { match HistorySegmentFile::save_to_disk(path, history_segment_ram) { // Immediately drop the segment - now that we've saved it to disk we don't need to keep it in memory @@ -450,7 +444,7 @@ impl TelemetryHistory { start: DateTime, ) -> JoinHandle> { let mut path = service.data_root_folder.clone(); - path.push(&self.data.definition.uuid); + path.push(self.data.definition.uuid.as_hyphenated().to_string()); spawn_blocking(move || HistorySegmentFile::open(path, start)) } @@ -458,7 +452,7 @@ impl TelemetryHistory { &self, start: DateTime, service: &TelemetryHistoryService, - telemetry_data_type: TelemetryDataType, + telemetry_data_type: DataType, ) -> HistorySegmentRam { let ram = self .get_disk_segment(service, start) @@ -480,7 +474,7 @@ impl TelemetryHistory { pub async fn insert( &self, service: &TelemetryHistoryService, - value: TelemetryDataValue, + value: DataValue, timestamp: DateTime, ) { let segments = self.segments.read().await; @@ -531,7 +525,7 @@ impl TelemetryHistory { pub fn insert_sync( history: Arc, service: Arc, - value: TelemetryDataValue, + value: DataValue, timestamp: DateTime, ) { tokio::spawn(async move { @@ -579,7 +573,7 @@ impl TelemetryHistory { .unwrap(); let mut path = telemetry_history_service.data_root_folder.clone(); - path.push(&self.data.definition.uuid); + path.push(self.data.definition.uuid.as_hyphenated().to_string()); let mut start = start; while start < end { diff --git a/server/src/telemetry/management_service.rs b/server/src/telemetry/management_service.rs index d819cc7..b4478b7 100644 --- a/server/src/telemetry/management_service.rs +++ b/server/src/telemetry/management_service.rs @@ -1,7 +1,15 @@ -use crate::core::{TelemetryDefinitionRequest, Uuid}; use crate::telemetry::data::TelemetryData; +use crate::telemetry::data_item::TelemetryDataItem; use crate::telemetry::definition::TelemetryDefinition; use crate::telemetry::history::{TelemetryHistory, TelemetryHistoryService}; +use anyhow::bail; +use api::data_type::DataType; +use api::data_value::DataValue; +use api::messages::telemetry_definition::{ + TelemetryDefinitionRequest, TelemetryDefinitionResponse, +}; +use api::messages::telemetry_entry::TelemetryEntry; +use chrono::SecondsFormat; use log::{error, info, warn}; use papaya::{HashMap, HashMapRef, LocalGuard}; use std::fs; @@ -12,12 +20,13 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::Mutex; use tokio::time::sleep; +use uuid::Uuid; const RELEASED_ATTEMPTS: usize = 5; pub struct TelemetryManagementService { - uuid_index: HashMap, - tlm_data: HashMap>, + uuid_index: HashMap, + tlm_data: HashMap>, telemetry_history_service: Arc, metadata_file: Arc>, } @@ -49,8 +58,8 @@ impl TelemetryManagementService { // Skip invalid entries match serde_json::from_str::(line) { Ok(tlm_def) => { - let _ = uuid_index.insert(tlm_def.name.clone(), tlm_def.uuid.clone()); - let _ = tlm_data.insert(tlm_def.uuid.clone(), Arc::new(tlm_def.into())); + let _ = uuid_index.insert(tlm_def.name.clone(), tlm_def.uuid); + let _ = tlm_data.insert(tlm_def.uuid, Arc::new(tlm_def.into())); } Err(err) => { error!("Failed to parse metadata entry {err}"); @@ -79,23 +88,20 @@ impl TelemetryManagementService { pub fn register( &self, telemetry_definition_request: TelemetryDefinitionRequest, - ) -> anyhow::Result { + ) -> anyhow::Result { let uuid_index = self.uuid_index.pin(); let tlm_data = self.tlm_data.pin(); - let uuid = uuid_index - .get_or_insert_with(telemetry_definition_request.name.clone(), || { - Uuid::random().value - }) - .clone(); + let uuid = + *uuid_index.get_or_insert_with(telemetry_definition_request.name.clone(), Uuid::new_v4); let inserted = tlm_data.try_insert( - uuid.clone(), + uuid, Arc::new( TelemetryDefinition { - uuid: uuid.clone(), + uuid, name: telemetry_definition_request.name.clone(), - data_type: telemetry_definition_request.data_type(), + data_type: telemetry_definition_request.data_type, } .into(), ), @@ -129,7 +135,38 @@ impl TelemetryManagementService { }); } - Ok(uuid) + Ok(TelemetryDefinitionResponse { uuid }) + } + + pub fn add_tlm_item(&self, tlm_item: TelemetryEntry) -> anyhow::Result<()> { + let tlm_management_pin = self.pin(); + let Some(tlm_data) = tlm_management_pin.get_by_uuid(&tlm_item.uuid) else { + bail!("Telemetry Item Not Found"); + }; + + let expected_type = match &tlm_item.value { + DataValue::Float32(_) => DataType::Float32, + DataValue::Float64(_) => DataType::Float64, + DataValue::Boolean(_) => DataType::Boolean, + }; + if expected_type != tlm_data.data.definition.data_type { + bail!("Data Type Mismatch"); + }; + + let _ = tlm_data.data.data.send_replace(Some(TelemetryDataItem { + value: tlm_item.value, + timestamp: tlm_item + .timestamp + .to_rfc3339_opts(SecondsFormat::Millis, true), + })); + TelemetryHistory::insert_sync( + tlm_data.clone(), + self.history_service(), + tlm_item.value, + tlm_item.timestamp, + ); + + Ok(()) } pub fn get_by_name(&self, name: &String) -> Option { @@ -138,7 +175,7 @@ impl TelemetryManagementService { self.get_by_uuid(uuid) } - pub fn get_by_uuid(&self, uuid: &String) -> Option { + pub fn get_by_uuid(&self, uuid: &Uuid) -> Option { let tlm_data = self.tlm_data.pin(); tlm_data .get(uuid) @@ -200,11 +237,11 @@ impl TelemetryManagementService { } pub struct TelemetryManagementServicePin<'a> { - tlm_data: HashMapRef<'a, String, Arc, RandomState, LocalGuard<'a>>, + tlm_data: HashMapRef<'a, Uuid, Arc, RandomState, LocalGuard<'a>>, } impl<'a> TelemetryManagementServicePin<'a> { - pub fn get_by_uuid(&'a self, uuid: &String) -> Option<&'a Arc> { + pub fn get_by_uuid(&'a self, uuid: &Uuid) -> Option<&'a Arc> { self.tlm_data.get(uuid) } } diff --git a/server/src/telemetry/mod.rs b/server/src/telemetry/mod.rs index f95ccc7..bfc497c 100644 --- a/server/src/telemetry/mod.rs +++ b/server/src/telemetry/mod.rs @@ -1,7 +1,5 @@ pub mod data; pub mod data_item; -pub mod data_type; -pub mod data_value; pub mod definition; pub mod history; pub mod management_service; diff --git a/server/src/uuid.rs b/server/src/uuid.rs deleted file mode 100644 index 87f5adf..0000000 --- a/server/src/uuid.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::core::Uuid; -use rand::RngCore; - -impl Uuid { - pub fn random() -> Self { - let mut uuid = [0u8; 16]; - rand::rng().fill_bytes(&mut uuid); - Self { - value: hex::encode(uuid), - } - } -} - -impl From for Uuid { - fn from(value: String) -> Self { - Self { value } - } -}