From e79e934baf33c9e48d969b3a04c10bb771ccc37f Mon Sep 17 00:00:00 2001 From: theredfish Date: Sat, 30 May 2026 03:37:35 +0200 Subject: [PATCH] feat: add arksync bus foundation --- .github/workflows/ci.yml | 20 +++ Cargo.lock | 125 ++++++++++++++ Cargo.toml | 13 ++ crates/bus/Cargo.toml | 24 +++ crates/bus/src/bus.rs | 188 +++++++++++++++++++++ crates/bus/src/event.rs | 47 ++++++ crates/bus/src/ids.rs | 8 + crates/bus/src/lib.rs | 141 ++++++++++++++++ crates/bus/src/postcard/mod.rs | 12 ++ crates/bus/src/postcard/postcard_decode.rs | 18 ++ crates/bus/src/postcard/postcard_encode.rs | 24 +++ crates/bus/tests/local_event_bus.rs | 148 ++++++++++++++++ crates/hub/Cargo.toml | 17 ++ crates/hub/src/domain/id.rs | 28 +++ crates/hub/src/domain/mod.rs | 9 + crates/hub/src/domain/source.rs | 12 ++ crates/hub/src/lib.rs | 7 + crates/knot/Cargo.toml | 17 ++ crates/knot/src/domain/id.rs | 34 ++++ crates/knot/src/domain/mod.rs | 9 + crates/knot/src/domain/source.rs | 15 ++ crates/knot/src/lib.rs | 7 + crates/macros/Cargo.toml | 25 +++ crates/macros/src/lib.rs | 12 ++ crates/macros/src/uuid_v4_macro.rs | 114 +++++++++++++ crates/macros/tests/uuid_v4.rs | 25 +++ crates/sensor/Cargo.toml | 1 + crates/sensor/src/infrastructure/events.rs | 20 +++ crates/sensor/src/infrastructure/mod.rs | 5 + crates/sensor/src/lib.rs | 1 + crates/sensor/src/serial_port.rs | 3 +- crates/utils/Cargo.toml | 16 ++ crates/utils/src/lib.rs | 7 + crates/utils/src/uuid.rs | 35 ++++ docker-compose.yml | 41 +---- 35 files changed, 1188 insertions(+), 40 deletions(-) create mode 100644 crates/bus/Cargo.toml create mode 100644 crates/bus/src/bus.rs create mode 100644 crates/bus/src/event.rs create mode 100644 crates/bus/src/ids.rs create mode 100644 crates/bus/src/lib.rs create mode 100644 crates/bus/src/postcard/mod.rs create mode 100644 crates/bus/src/postcard/postcard_decode.rs create mode 100644 crates/bus/src/postcard/postcard_encode.rs create mode 100644 crates/bus/tests/local_event_bus.rs create mode 100644 crates/hub/Cargo.toml create mode 100644 crates/hub/src/domain/id.rs create mode 100644 crates/hub/src/domain/mod.rs create mode 100644 crates/hub/src/domain/source.rs create mode 100644 crates/hub/src/lib.rs create mode 100644 crates/knot/Cargo.toml create mode 100644 crates/knot/src/domain/id.rs create mode 100644 crates/knot/src/domain/mod.rs create mode 100644 crates/knot/src/domain/source.rs create mode 100644 crates/knot/src/lib.rs create mode 100644 crates/macros/Cargo.toml create mode 100644 crates/macros/src/lib.rs create mode 100644 crates/macros/src/uuid_v4_macro.rs create mode 100644 crates/macros/tests/uuid_v4.rs create mode 100644 crates/sensor/src/infrastructure/events.rs create mode 100644 crates/sensor/src/infrastructure/mod.rs create mode 100644 crates/utils/Cargo.toml create mode 100644 crates/utils/src/lib.rs create mode 100644 crates/utils/src/uuid.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e8b68c..71689fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -157,3 +157,23 @@ jobs: tool: nextest - run: cargo nextest run --workspace --all-features --profile ci + + no-std: + name: no_std checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: libwebkit2gtk-4.1-dev libudev-dev + version: 1.0 + + - uses: ./.github/actions/setup-rust + + - uses: swatinem/rust-cache@v2 + with: + cache-on-failure: true + + - name: Check no_std crates without default features + run: cargo check --workspace --all-targets --no-default-features diff --git a/Cargo.lock b/Cargo.lock index a355863..c548845 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,6 +185,20 @@ dependencies = [ "serde", ] +[[package]] +name = "arksync-bus" +version = "0.1.0" +dependencies = [ + "arksync-macros", + "arksync-sensor", + "arksync-utils", + "critical-section", + "embassy-sync", + "postcard", + "serde", + "tokio", +] + [[package]] name = "arksync-cli" version = "0.1.0" @@ -209,12 +223,43 @@ dependencies = [ "tokio", ] +[[package]] +name = "arksync-hub" +version = "0.1.0" +dependencies = [ + "arksync-macros", + "arksync-utils", + "serde", +] + +[[package]] +name = "arksync-knot" +version = "0.1.0" +dependencies = [ + "arksync-macros", + "arksync-utils", + "serde", +] + +[[package]] +name = "arksync-macros" +version = "0.1.0" +dependencies = [ + "arksync-utils", + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "uuid", +] + [[package]] name = "arksync-sensor" version = "0.1.0" dependencies = [ "chrono", "eyre", + "serde", "serialport", "test-case", "tokio", @@ -251,6 +296,13 @@ dependencies = [ "uuid", ] +[[package]] +name = "arksync-utils" +version = "0.1.0" +dependencies = [ + "uuid", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -861,6 +913,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.12", +] + [[package]] name = "codee" version = "0.3.0" @@ -1072,6 +1133,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-channel" version = "0.5.14" @@ -1451,6 +1518,20 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "embassy-sync" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bbd85cf5a5ae56bdf26f618364af642d1d0a4e245cdd75cd9aabda382f65a81" +dependencies = [ + "cfg-if", + "critical-section", + "embedded-io-async", + "futures-core", + "futures-sink", + "heapless", +] + [[package]] name = "embed-resource" version = "3.0.2" @@ -1471,6 +1552,21 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "embedded-io" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" + +[[package]] +name = "embedded-io-async" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564b9f813c544241430e147d8bc454815ef9ac998878d30cc3055449f7fd4c0" +dependencies = [ + "embedded-io", +] + [[package]] name = "endi" version = "1.1.0" @@ -2197,6 +2293,15 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2232,6 +2337,16 @@ dependencies = [ "hashbrown 0.15.2", ] +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.4.1" @@ -4163,6 +4278,16 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "serde", +] + [[package]] name = "powerfmt" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 4c3114c..8d332bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,13 @@ members = [ "crates/cli", "crates/config", "crates/db", + "crates/bus", + "crates/hub", + "crates/knot", + "crates/macros", "crates/sensor", "crates/ui", + "crates/utils", "crates/users", "src-tauri" ] @@ -21,6 +26,11 @@ arksync-actuator = { path = "crates/actuator" } arksync-cli = { path = "crates/cli" } arksync-config = { path = "crates/config" } arksync-db = { path = "crates/db" } +arksync-bus = { path = "crates/bus" } +arksync-hub = { path = "crates/hub" } +arksync-knot = { path = "crates/knot" } +arksync-macros = { path = "crates/macros" } +arksync-utils = { path = "crates/utils" } arksync-users = { path = "crates/users" } charming = { version = "0.6.0", features = ["wasm"] } chrono = "0.4" @@ -51,6 +61,9 @@ test-case = "3.3.1" tokio = "1.49.0" tokio-util = "0.7" uuid = "1" +syn = "2" +quote = "1" +proc-macro2 = "1" wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" web-sys = { version = "0.3", features = ["EventListener"] } diff --git a/crates/bus/Cargo.toml b/crates/bus/Cargo.toml new file mode 100644 index 0000000..da72cbd --- /dev/null +++ b/crates/bus/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "arksync-bus" +version.workspace = true +edition.workspace = true +license.workspace = true + +[features] +default = [] +uuid-v4 = ["arksync-utils/uuid-v4"] + +[dependencies] +arksync-macros.workspace = true +arksync-utils.workspace = true +postcard = { version = "1", default-features = false } +serde = { version = "1", default-features = false, features = ["alloc", "derive"] } + +[dev-dependencies] +arksync-sensor = { path = "../sensor" } +critical-section = { version = "1", features = ["std"] } +embassy-sync = { version = "0.8", default-features = false } +tokio = { workspace = true, features = ["macros", "rt", "sync"] } + +[lints] +workspace = true diff --git a/crates/bus/src/bus.rs b/crates/bus/src/bus.rs new file mode 100644 index 0000000..c687d72 --- /dev/null +++ b/crates/bus/src/bus.rs @@ -0,0 +1,188 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! no_std EventBus core. +//! +//! The bus owns the generic subscription/filter/handler mechanics. Bounded +//! contexts own their event payloads and can attach any local, MQTT, or storage +//! handler later. + +use crate::EventEnvelope; +use alloc::{boxed::Box, string::String, vec::Vec}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum EventBusError { + HandlerRejected, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Delivery { + Local, + Mqtt { topic: String }, + Both { mqtt_topic: String }, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Persistence { + Memory, + Storage, +} + +/// Decides whether a subscription should receive an event. +pub trait EventFilter { + fn matches(&self, event: &EventEnvelope) -> bool; +} + +impl EventFilter for Handler +where + Handler: Fn(&EventEnvelope) -> bool, +{ + fn matches(&self, event: &EventEnvelope) -> bool { + self(event) + } +} + +impl EventFilter for () { + fn matches(&self, _event: &EventEnvelope) -> bool { + true + } +} + +/// Handles an event selected by a subscription. +/// +/// A handler is what a subscription runs after its filter matched. It can +/// forward the event to a channel, write to storage, publish to MQTT, or run +/// local logic. +pub trait EventHandler { + fn handle(&mut self, event: EventEnvelope) -> Result<(), EventBusError>; +} + +impl EventHandler for Handler +where + Handler: FnMut(EventEnvelope) -> Result<(), EventBusError>, +{ + fn handle(&mut self, event: EventEnvelope) -> Result<(), EventBusError> { + self(event) + } +} + +pub struct EventSubscription { + filter: Box>, + handler: Box>, + delivery: Delivery, + persistence: Persistence, +} + +impl EventSubscription { + pub fn local(filter: Filter, handler: Handler) -> Self + where + Filter: EventFilter + 'static, + Handler: EventHandler + 'static, + { + Self { + filter: Box::new(filter), + handler: Box::new(handler), + delivery: Delivery::Local, + persistence: Persistence::Memory, + } + } + + pub fn delivery(&self) -> &Delivery { + &self.delivery + } + + pub fn persistence(&self) -> Persistence { + self.persistence + } + + fn matches(&self, event: &EventEnvelope) -> bool { + self.filter.matches(event) + } + + fn handle(&mut self, event: EventEnvelope) -> Result<(), EventBusError> { + self.handler.handle(event) + } +} + +pub struct EventBus { + subscriptions: Vec>, +} + +impl Default for EventBus { + fn default() -> Self { + Self::new() + } +} + +impl EventBus { + pub fn new() -> Self { + Self { + subscriptions: Vec::new(), + } + } + + pub fn add_subscription(&mut self, subscription: EventSubscription) { + self.subscriptions.push(subscription); + } + + pub fn subscribe(&mut self, handler: Handler) + where + Handler: EventHandler + 'static, + { + self.subscribe_where((), handler); + } + + pub fn subscribe_where(&mut self, filter: Filter, handler: Handler) + where + Filter: EventFilter + 'static, + Handler: EventHandler + 'static, + { + self.add_subscription(EventSubscription::local(filter, handler)); + } + + pub fn producer(&mut self) -> EventProducer<'_, Payload, Source> { + EventProducer { bus: self } + } +} + +impl EventBus +where + Payload: Clone, + Source: Clone, +{ + pub fn publish( + &mut self, + event: EventEnvelope, + ) -> Result { + let mut delivered = 0; + + for subscription in &mut self.subscriptions { + if !subscription.matches(&event) { + continue; + } + + subscription.handle(event.clone())?; + delivered += 1; + } + + Ok(delivered) + } +} + +pub struct EventProducer<'bus, Payload, Source = ()> { + bus: &'bus mut EventBus, +} + +impl EventProducer<'_, Payload, Source> +where + Payload: Clone, + Source: Clone, +{ + pub fn publish( + &mut self, + event: EventEnvelope, + ) -> Result { + self.bus.publish(event) + } +} diff --git a/crates/bus/src/event.rs b/crates/bus/src/event.rs new file mode 100644 index 0000000..c0fb546 --- /dev/null +++ b/crates/bus/src/event.rs @@ -0,0 +1,47 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Generic event envelopes shared by EventBus transports. + +use crate::EventId; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct Timestamp { + pub unix_millis: i64, +} + +impl Timestamp { + pub fn from_unix_millis(unix_millis: i64) -> Self { + Self { unix_millis } + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct EventEnvelope { + pub id: EventId, + pub source: S, + pub occurred_at: Timestamp, + pub payload: E, +} + +impl EventEnvelope { + pub fn new_with_id(id: EventId, source: S, occurred_at: Timestamp, payload: E) -> Self { + Self { + id, + source, + occurred_at, + payload, + } + } +} + +#[cfg(feature = "uuid-v4")] +impl EventEnvelope { + pub fn new(source: S, occurred_at: Timestamp, payload: E) -> Self { + Self::new_with_id(EventId::new(), source, occurred_at, payload) + } +} diff --git a/crates/bus/src/ids.rs b/crates/bus/src/ids.rs new file mode 100644 index 0000000..b8b62ac --- /dev/null +++ b/crates/bus/src/ids.rs @@ -0,0 +1,8 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use arksync_macros::UuidV4; + +#[derive(UuidV4)] +pub struct EventId([u8; 16]); diff --git a/crates/bus/src/lib.rs b/crates/bus/src/lib.rs new file mode 100644 index 0000000..bd30ea3 --- /dev/null +++ b/crates/bus/src/lib.rs @@ -0,0 +1,141 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Shared EventBus infrastructure between ArkSync bounded contexts. +//! +//! Bounded contexts own their event payload definitions. This crate owns the +//! generic envelope, subscription, handler, and codec mechanics used to move +//! those events. + +#![no_std] + +extern crate alloc; + +mod bus; +mod event; +mod ids; +pub mod postcard; + +pub use bus::*; +pub use event::*; +pub use ids::*; +pub use postcard::*; + +#[cfg(test)] +mod tests { + use super::*; + use alloc::string::{String, ToString}; + use serde::{Deserialize, Serialize}; + + fn timestamp(unix_millis: i64) -> Timestamp { + Timestamp::from_unix_millis(unix_millis) + } + + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] + #[serde(rename_all = "snake_case")] + enum TestEvent { + Observed { label: String }, + Ignored, + } + + #[test] + fn postcard_round_trips_event_envelope() { + let event = EventEnvelope::new_with_id( + EventId::new_with_random_bytes([1; 16]), + "knot:test".to_string(), + timestamp(1_779_840_000_000), + TestEvent::Observed { + label: "rtd".to_string(), + }, + ); + let mut buffer = [0; 128]; + + let encoded = event.encode_postcard(&mut buffer).unwrap(); + let decoded: EventEnvelope = + EventEnvelope::decode_postcard(encoded).unwrap(); + + assert_eq!(decoded, event); + } + + #[test] + fn event_bus_delivers_matching_events() { + let mut bus = EventBus::new(); + bus.subscribe_where( + |event: &EventEnvelope| matches!(event.payload, TestEvent::Observed { .. }), + |_event: EventEnvelope| Ok(()), + ); + let event = EventEnvelope::new_with_id( + EventId::new_with_random_bytes([1; 16]), + (), + timestamp(1_779_840_000_000), + TestEvent::Observed { + label: "rtd".to_string(), + }, + ); + + let delivered = bus.publish(event).unwrap(); + + assert_eq!(delivered, 1); + } + + #[test] + fn event_bus_skips_filtered_events() { + let mut bus = EventBus::new(); + bus.subscribe_where( + |event: &EventEnvelope| matches!(event.payload, TestEvent::Observed { .. }), + |_event: EventEnvelope| Ok(()), + ); + let event = EventEnvelope::new_with_id( + EventId::new_with_random_bytes([1; 16]), + (), + timestamp(1_779_840_000_000), + TestEvent::Ignored, + ); + + let delivered = bus.publish(event).unwrap(); + + assert_eq!(delivered, 0); + } + + #[test] + fn event_bus_delivers_all_events_with_unit_filter() { + let mut bus = EventBus::new(); + bus.subscribe(|_event: EventEnvelope| Ok(())); + let event = EventEnvelope::new_with_id( + EventId::new_with_random_bytes([1; 16]), + (), + timestamp(1_779_840_000_000), + TestEvent::Ignored, + ); + + let delivered = bus.publish(event).unwrap(); + + assert_eq!(delivered, 1); + } + + #[test] + fn postcard_reports_buffer_too_small() { + let event = EventEnvelope::new_with_id( + EventId::new_with_random_bytes([1; 16]), + (), + timestamp(1_779_840_000_000), + TestEvent::Observed { + label: "rtd".to_string(), + }, + ); + let mut buffer = [0; 1]; + + let result = event.encode_postcard(&mut buffer); + + assert!(matches!(result, Err(postcard::Error::SerializeBufferFull))); + } + + #[cfg(feature = "uuid-v4")] + #[test] + fn event_envelope_new_generates_event_id() { + let event = EventEnvelope::new((), timestamp(1_779_840_000_000), TestEvent::Ignored); + + assert_eq!(event.id.as_uuid().get_version_num(), 4); + } +} diff --git a/crates/bus/src/postcard/mod.rs b/crates/bus/src/postcard/mod.rs new file mode 100644 index 0000000..0eba021 --- /dev/null +++ b/crates/bus/src/postcard/mod.rs @@ -0,0 +1,12 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Postcard support for EventBus envelopes and bounded-context events. + +mod postcard_decode; +mod postcard_encode; + +pub use postcard::Error; +pub use postcard_decode::*; +pub use postcard_encode::*; diff --git a/crates/bus/src/postcard/postcard_decode.rs b/crates/bus/src/postcard/postcard_decode.rs new file mode 100644 index 0000000..5f0ff8c --- /dev/null +++ b/crates/bus/src/postcard/postcard_decode.rs @@ -0,0 +1,18 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use serde::de::DeserializeOwned; + +pub trait PostcardDecode: Sized { + fn decode_postcard(bytes: &[u8]) -> Result; +} + +impl PostcardDecode for T +where + T: DeserializeOwned, +{ + fn decode_postcard(bytes: &[u8]) -> Result { + postcard::from_bytes(bytes) + } +} diff --git a/crates/bus/src/postcard/postcard_encode.rs b/crates/bus/src/postcard/postcard_encode.rs new file mode 100644 index 0000000..c1a4e2a --- /dev/null +++ b/crates/bus/src/postcard/postcard_encode.rs @@ -0,0 +1,24 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use serde::Serialize; + +pub trait PostcardEncode { + fn encode_postcard<'buffer>( + &self, + buffer: &'buffer mut [u8], + ) -> Result<&'buffer mut [u8], postcard::Error>; +} + +impl PostcardEncode for T +where + T: Serialize, +{ + fn encode_postcard<'buffer>( + &self, + buffer: &'buffer mut [u8], + ) -> Result<&'buffer mut [u8], postcard::Error> { + postcard::to_slice(self, buffer) + } +} diff --git a/crates/bus/tests/local_event_bus.rs b/crates/bus/tests/local_event_bus.rs new file mode 100644 index 0000000..7c4efdf --- /dev/null +++ b/crates/bus/tests/local_event_bus.rs @@ -0,0 +1,148 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use arksync_bus::{ + EventBus, EventBusError, EventEnvelope, EventHandler, EventId, PostcardDecode, PostcardEncode, + Timestamp, +}; +use arksync_sensor::infrastructure::events::{SensorEvent, SerialSensorObserved}; +use arksync_sensor::serial_port::{SerialPortMetadata, DEFAULT_BAUD_RATE}; +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; +use embassy_sync::channel::{Channel, Sender}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +enum TestSource { + Knot { label: String }, +} + +type SensorEventEnvelope = EventEnvelope; +type SensorEventChannel = Channel; + +static SENSOR_EVENTS: SensorEventChannel = Channel::new(); +static KNOT_EVENTS: SensorEventChannel = Channel::new(); + +struct ChannelHandler(Sender<'static, CriticalSectionRawMutex, SensorEventEnvelope, 1>); + +impl EventHandler for ChannelHandler { + fn handle(&mut self, event: SensorEventEnvelope) -> Result<(), EventBusError> { + self.0 + .try_send(event) + .map_err(|_| EventBusError::HandlerRejected) + } +} + +struct TokioHandler(tokio::sync::mpsc::Sender); + +impl EventHandler for TokioHandler { + fn handle(&mut self, event: SensorEventEnvelope) -> Result<(), EventBusError> { + self.0 + .try_send(event) + .map_err(|_| EventBusError::HandlerRejected) + } +} + +fn sensor_event() -> SensorEventEnvelope { + EventEnvelope::new_with_id( + EventId::new_with_random_bytes([1; 16]), + TestSource::Knot { + label: "rtd-knot".to_string(), + }, + Timestamp::from_unix_millis(1_779_840_000_000), + SensorEvent::SerialSensorObserved(SerialSensorObserved { + metadata: SerialPortMetadata { + port_name: "/dev/ttyUSB0".to_string(), + serial_number: "rtd-serial-1".to_string(), + baud_rate: DEFAULT_BAUD_RATE, + }, + }), + ) +} + +#[test] +fn sends_sensor_event_through_embassy_local_event_bus() { + let mut bus = EventBus::new(); + bus.subscribe_where( + |event: &SensorEventEnvelope| { + matches!( + event.payload, + SensorEvent::SerialSensorObserved(SerialSensorObserved { .. }) + ) + }, + ChannelHandler(SENSOR_EVENTS.sender()), + ); + let event = sensor_event(); + + let delivered = bus.producer().publish(event.clone()).unwrap(); + let received = SENSOR_EVENTS.receiver().try_receive().unwrap(); + + assert_eq!(delivered, 1); + assert_eq!(received, event); +} + +#[tokio::test] +async fn sends_sensor_event_through_tokio_local_event_bus() { + let (hub_tx, mut hub_rx) = tokio::sync::mpsc::channel(1); + let mut bus = EventBus::new(); + bus.subscribe_where( + |event: &SensorEventEnvelope| { + matches!( + event.payload, + SensorEvent::SerialSensorObserved(SerialSensorObserved { .. }) + ) + }, + TokioHandler(hub_tx), + ); + let event = sensor_event(); + + let delivered = bus.producer().publish(event.clone()).unwrap(); + let received = hub_rx.recv().await.unwrap(); + + assert_eq!(delivered, 1); + assert_eq!(received, event); +} + +#[test] +fn postcard_round_trips_sensor_event_envelope() { + let event = sensor_event(); + let mut buffer = [0; 256]; + + let encoded = event.encode_postcard(&mut buffer).unwrap(); + let decoded = SensorEventEnvelope::decode_postcard(encoded).unwrap(); + + assert_eq!(decoded, event); +} + +#[tokio::test] +async fn bridges_embassy_knot_channel_to_tokio_hub_channel() { + let (hub_tx, mut hub_rx) = tokio::sync::mpsc::channel(1); + let bridge = tokio::spawn(async move { + let event = KNOT_EVENTS.receiver().receive().await; + hub_tx + .send(event) + .await + .map_err(|_| EventBusError::HandlerRejected) + }); + + let mut knot_bus = EventBus::new(); + knot_bus.subscribe_where( + |event: &SensorEventEnvelope| { + matches!( + event.payload, + SensorEvent::SerialSensorObserved(SerialSensorObserved { .. }) + ) + }, + ChannelHandler(KNOT_EVENTS.sender()), + ); + let event = sensor_event(); + + let delivered = knot_bus.producer().publish(event.clone()).unwrap(); + let received = hub_rx.recv().await.unwrap(); + let bridged = bridge.await.unwrap(); + + assert_eq!(delivered, 1); + assert_eq!(received, event); + assert_eq!(bridged, Ok(())); +} diff --git a/crates/hub/Cargo.toml b/crates/hub/Cargo.toml new file mode 100644 index 0000000..c544d58 --- /dev/null +++ b/crates/hub/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "arksync-hub" +version.workspace = true +edition.workspace = true +license.workspace = true + +[features] +default = [] +uuid-v4 = ["arksync-utils/uuid-v4"] + +[dependencies] +arksync-macros.workspace = true +arksync-utils.workspace = true +serde = { version = "1", default-features = false, features = ["derive"] } + +[lints] +workspace = true diff --git a/crates/hub/src/domain/id.rs b/crates/hub/src/domain/id.rs new file mode 100644 index 0000000..52ea9a4 --- /dev/null +++ b/crates/hub/src/domain/id.rs @@ -0,0 +1,28 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use arksync_macros::UuidV4; + +#[derive(UuidV4)] +pub struct HubId([u8; 16]); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn builds_hub_id_from_random_bytes() { + let id = HubId::new_with_random_bytes([1; 16]); + + assert_eq!(id.as_uuid().get_version_num(), 4); + } + + #[cfg(feature = "uuid-v4")] + #[test] + fn generates_hub_id() { + let id = HubId::new(); + + assert_eq!(id.as_uuid().get_version_num(), 4); + } +} diff --git a/crates/hub/src/domain/mod.rs b/crates/hub/src/domain/mod.rs new file mode 100644 index 0000000..2eccb6a --- /dev/null +++ b/crates/hub/src/domain/mod.rs @@ -0,0 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +pub mod id; +pub mod source; + +pub use id::*; +pub use source::*; diff --git a/crates/hub/src/domain/source.rs b/crates/hub/src/domain/source.rs new file mode 100644 index 0000000..fb3487f --- /dev/null +++ b/crates/hub/src/domain/source.rs @@ -0,0 +1,12 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use crate::domain::HubId; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum HubEventSource { + Hub { hub_id: HubId }, +} diff --git a/crates/hub/src/lib.rs b/crates/hub/src/lib.rs new file mode 100644 index 0000000..ddb09c2 --- /dev/null +++ b/crates/hub/src/lib.rs @@ -0,0 +1,7 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#![no_std] + +pub mod domain; diff --git a/crates/knot/Cargo.toml b/crates/knot/Cargo.toml new file mode 100644 index 0000000..b0de910 --- /dev/null +++ b/crates/knot/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "arksync-knot" +version.workspace = true +edition.workspace = true +license.workspace = true + +[features] +default = [] +uuid-v4 = ["arksync-utils/uuid-v4"] + +[dependencies] +arksync-macros.workspace = true +arksync-utils.workspace = true +serde = { version = "1", default-features = false, features = ["derive"] } + +[lints] +workspace = true diff --git a/crates/knot/src/domain/id.rs b/crates/knot/src/domain/id.rs new file mode 100644 index 0000000..10453ea --- /dev/null +++ b/crates/knot/src/domain/id.rs @@ -0,0 +1,34 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use arksync_macros::UuidV4; + +#[derive(UuidV4)] +pub struct KnotId([u8; 16]); + +#[derive(UuidV4)] +pub struct ParentHubId([u8; 16]); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hub_and_knot_ids_can_share_same_uuid_bytes() { + let bytes = [1; 16]; + let knot_id = KnotId::new_with_random_bytes(bytes); + let parent_hub_id = ParentHubId::new_with_random_bytes(bytes); + + assert_eq!(knot_id.as_bytes(), parent_hub_id.as_bytes()); + assert_eq!(knot_id.as_uuid(), parent_hub_id.as_uuid()); + } + + #[cfg(feature = "uuid-v4")] + #[test] + fn generates_knot_id() { + let id = KnotId::new(); + + assert_eq!(id.as_uuid().get_version_num(), 4); + } +} diff --git a/crates/knot/src/domain/mod.rs b/crates/knot/src/domain/mod.rs new file mode 100644 index 0000000..2eccb6a --- /dev/null +++ b/crates/knot/src/domain/mod.rs @@ -0,0 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +pub mod id; +pub mod source; + +pub use id::*; +pub use source::*; diff --git a/crates/knot/src/domain/source.rs b/crates/knot/src/domain/source.rs new file mode 100644 index 0000000..ac04d53 --- /dev/null +++ b/crates/knot/src/domain/source.rs @@ -0,0 +1,15 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use crate::domain::{KnotId, ParentHubId}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum KnotEventSource { + Knot { + parent_hub_id: ParentHubId, + knot_id: KnotId, + }, +} diff --git a/crates/knot/src/lib.rs b/crates/knot/src/lib.rs new file mode 100644 index 0000000..ddb09c2 --- /dev/null +++ b/crates/knot/src/lib.rs @@ -0,0 +1,7 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#![no_std] + +pub mod domain; diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml new file mode 100644 index 0000000..9b66315 --- /dev/null +++ b/crates/macros/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "arksync-macros" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +proc-macro = true + +[features] +default = [] +uuid-v4 = [] + +[dependencies] +proc-macro2.workspace = true +quote.workspace = true +syn = { workspace = true, features = ["derive"] } + +[dev-dependencies] +arksync-utils.workspace = true +serde.workspace = true +uuid.workspace = true + +[lints] +workspace = true diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs new file mode 100644 index 0000000..9359454 --- /dev/null +++ b/crates/macros/src/lib.rs @@ -0,0 +1,12 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use proc_macro::TokenStream; + +mod uuid_v4_macro; + +#[proc_macro_derive(UuidV4)] +pub fn derive_uuid_v4(input: TokenStream) -> TokenStream { + uuid_v4_macro::derive(input) +} diff --git a/crates/macros/src/uuid_v4_macro.rs b/crates/macros/src/uuid_v4_macro.rs new file mode 100644 index 0000000..1f1a5ba --- /dev/null +++ b/crates/macros/src/uuid_v4_macro.rs @@ -0,0 +1,114 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Data, DeriveInput, Fields}; + +pub fn derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let ident = input.ident; + + let valid_shape = matches!( + input.data, + Data::Struct(ref data) + if matches!( + data.fields, + Fields::Unnamed(ref fields) if fields.unnamed.len() == 1 + ) + ); + + if !valid_shape { + return syn::Error::new_spanned( + ident, + "UuidV4 can only be derived for tuple structs with one [u8; 16] field", + ) + .to_compile_error() + .into(); + } + + quote! { + impl #ident { + #[cfg(feature = "uuid-v4")] + pub fn new() -> Self { + Self::new_with_uuid(::arksync_utils::uuid::new_v4()) + } + + pub fn new_with_uuid(uuid: ::arksync_utils::uuid::Uuid) -> Self { + Self(*uuid.as_bytes()) + } + + pub fn new_with_random_bytes(bytes: [u8; 16]) -> Self { + Self::new_with_uuid(::arksync_utils::uuid::from_random_bytes(bytes)) + } + + pub fn as_uuid(&self) -> ::arksync_utils::uuid::Uuid { + ::arksync_utils::uuid::Uuid::from_bytes(self.0) + } + + pub fn as_bytes(&self) -> &[u8; 16] { + &self.0 + } + } + + impl From<::arksync_utils::uuid::Uuid> for #ident { + fn from(value: ::arksync_utils::uuid::Uuid) -> Self { + Self::new_with_uuid(value) + } + } + + impl Clone for #ident { + fn clone(&self) -> Self { + *self + } + } + + impl Copy for #ident {} + + impl core::fmt::Debug for #ident { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple(stringify!(#ident)).field(&self.as_uuid()).finish() + } + } + + impl core::fmt::Display for #ident { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Display::fmt(&self.as_uuid(), f) + } + } + + impl PartialEq for #ident { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } + } + + impl Eq for #ident {} + + impl core::hash::Hash for #ident { + fn hash(&self, state: &mut H) { + core::hash::Hash::hash(&self.0, state); + } + } + + impl ::serde::Serialize for #ident { + fn serialize(&self, serializer: S) -> Result + where + S: ::serde::Serializer, + { + ::serde::Serialize::serialize(&self.0, serializer) + } + } + + impl<'de> ::serde::Deserialize<'de> for #ident { + fn deserialize(deserializer: D) -> Result + where + D: ::serde::Deserializer<'de>, + { + <[u8; 16] as ::serde::Deserialize>::deserialize(deserializer).map(Self) + } + } + } + .into() +} diff --git a/crates/macros/tests/uuid_v4.rs b/crates/macros/tests/uuid_v4.rs new file mode 100644 index 0000000..3534142 --- /dev/null +++ b/crates/macros/tests/uuid_v4.rs @@ -0,0 +1,25 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use arksync_macros::UuidV4; + +#[derive(UuidV4)] +struct TestId([u8; 16]); + +#[test] +fn derives_uuid_roundtrip_helpers() { + let uuid = arksync_utils::uuid::from_random_bytes([1; 16]); + let id = TestId::new_with_uuid(uuid); + + assert_eq!(id.as_uuid(), uuid); + assert_eq!(id.as_bytes(), uuid.as_bytes()); + assert_eq!(TestId::from(uuid), id); +} + +#[test] +fn derives_random_bytes_constructor() { + let id = TestId::new_with_random_bytes([2; 16]); + + assert_eq!(id.as_uuid().get_version_num(), 4); +} diff --git a/crates/sensor/Cargo.toml b/crates/sensor/Cargo.toml index ffddc78..51b8b5d 100644 --- a/crates/sensor/Cargo.toml +++ b/crates/sensor/Cargo.toml @@ -11,6 +11,7 @@ license.workspace = true [dependencies] chrono.workspace = true eyre.workspace = true +serde.workspace = true serialport.workspace = true tokio = { workspace = true, features = ["full"] } tokio-util.workspace = true diff --git a/crates/sensor/src/infrastructure/events.rs b/crates/sensor/src/infrastructure/events.rs new file mode 100644 index 0000000..fee0249 --- /dev/null +++ b/crates/sensor/src/infrastructure/events.rs @@ -0,0 +1,20 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Published sensor events consumed by runtime adapters. + +use crate::serial_port::SerialPortMetadata; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum SensorEvent { + SerialSensorObserved(SerialSensorObserved), +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct SerialSensorObserved { + pub metadata: SerialPortMetadata, +} diff --git a/crates/sensor/src/infrastructure/mod.rs b/crates/sensor/src/infrastructure/mod.rs new file mode 100644 index 0000000..d58be11 --- /dev/null +++ b/crates/sensor/src/infrastructure/mod.rs @@ -0,0 +1,5 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +pub mod events; diff --git a/crates/sensor/src/lib.rs b/crates/sensor/src/lib.rs index 5c6a396..9c9a952 100644 --- a/crates/sensor/src/lib.rs +++ b/crates/sensor/src/lib.rs @@ -6,6 +6,7 @@ pub mod core; pub mod error; pub mod ezo; pub mod i2c_bus; +pub mod infrastructure; pub mod sensor; pub mod serial_port; pub mod services; diff --git a/crates/sensor/src/serial_port.rs b/crates/sensor/src/serial_port.rs index 7164a7c..96fb754 100644 --- a/crates/sensor/src/serial_port.rs +++ b/crates/sensor/src/serial_port.rs @@ -2,6 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +use serde::{Deserialize, Serialize}; use serialport::{SerialPortInfo, SerialPortType}; use std::io::{Read, Write}; use std::time::Duration; @@ -12,7 +13,7 @@ pub const DEFAULT_BAUD_RATE: u32 = 9600; pub const SERIAL_PORT_CONN_TIMEOUT: u64 = 1000; // Timeout acts as safety net for response-based reading /// Metadata about a serial port (no active connection) -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SerialPortMetadata { pub port_name: String, pub serial_number: String, diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml new file mode 100644 index 0000000..a21d830 --- /dev/null +++ b/crates/utils/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "arksync-utils" +version.workspace = true +edition.workspace = true +license.workspace = true + +[features] +default = [] +std = ["uuid/std"] +uuid-v4 = ["std", "uuid/v4"] + +[dependencies] +uuid = { version = "1", default-features = false } + +[lints] +workspace = true diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs new file mode 100644 index 0000000..c27dccc --- /dev/null +++ b/crates/utils/src/lib.rs @@ -0,0 +1,7 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#![no_std] + +pub mod uuid; diff --git a/crates/utils/src/uuid.rs b/crates/utils/src/uuid.rs new file mode 100644 index 0000000..bb650af --- /dev/null +++ b/crates/utils/src/uuid.rs @@ -0,0 +1,35 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +pub use uuid::Uuid; + +pub fn from_random_bytes(bytes: [u8; 16]) -> Uuid { + uuid::Builder::from_random_bytes(bytes).into_uuid() +} + +#[cfg(feature = "uuid-v4")] +pub fn new_v4() -> Uuid { + Uuid::new_v4() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn builds_uuid_from_random_bytes() { + let uuid = from_random_bytes([1; 16]); + + assert_eq!(uuid.as_bytes().len(), 16); + assert_eq!(uuid.get_version_num(), 4); + } + + #[cfg(feature = "uuid-v4")] + #[test] + fn generates_uuid_v4() { + let uuid = new_v4(); + + assert_eq!(uuid.get_version_num(), 4); + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 5f644e1..5b4d55a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: container_name: arksync-postgres restart: unless-stopped ports: - - "${POSTGRES_PORT:-5433}:5432" + - "127.0.0.1:${POSTGRES_PORT:-5433}:5432" environment: POSTGRES_DB: arksync POSTGRES_USER: admin @@ -28,45 +28,12 @@ services: - postgres-data:/var/lib/postgresql/data - ./docker/postgres/init:/docker-entrypoint-initdb.d:ro - influxdb: - image: influxdb:3.5.0-core - container_name: influxdb - user: root - ports: - - 8181:8181 - volumes: - - influx-data:/var/lib/influxdb3/data - - influx-plugins:/var/lib/influxdb3/plugins - command: - - influxdb3 - - serve - - --node-id=node0 - - --object-store=file - - --data-dir=/var/lib/influxdb3/data - - --plugin-dir=/var/lib/influxdb3/plugins - - influxdb-explorer: - image: influxdata/influxdb3-ui:1.3.0 - container_name: influxdb-explorer - user: root - restart: unless-stopped - ports: - - 8888:80 - - 8889:8888 - environment: - SESSION_SECRET_KEY: ${SESSION_SECRET_KEY:-changeme123456789012345678901234} - DATABASE_URL: /db/sqlite.db - volumes: - - influx-explorer-db:/db:rw - - influx-explorer-config:/app-root/config:rw - command: ["--mode=admin"] - grafana: image: grafana/grafana-enterprise:12.3.0-18209090404-boringcrypto container_name: grafana restart: unless-stopped ports: - - '33000:3000' + - "127.0.0.1:${GRAFANA_PORT:-33000}:3000" environment: - GF_SECURITY_ADMIN_USER=admin - GF_SECURITY_ADMIN_PASSWORD=admin @@ -75,8 +42,4 @@ services: volumes: postgres-data: - influx-data: - influx-plugins: - influx-explorer-db: - influx-explorer-config: grafana-data: