197 lines
5.9 KiB
Markdown
197 lines
5.9 KiB
Markdown
# RMon Agent Specification
|
|
|
|
## Binary
|
|
|
|
- **Build target**: `x86_64-unknown-linux-musl` (primary), `aarch64-unknown-linux-musl` (secondary)
|
|
- **Static**: fully static binary, no dynamic library dependencies whatsoever
|
|
- **Runtime requirements**: Linux kernel ≥ 3.2, no root, no installed packages
|
|
- **Deployment**: single file, placed at `~/.rmon/agent` by the UI or manually by the user
|
|
|
|
Build command:
|
|
```sh
|
|
cargo build --release --target x86_64-unknown-linux-musl -p rmon-agent
|
|
```
|
|
|
|
Cross-compilation for ARM:
|
|
```sh
|
|
cargo build --release --target aarch64-unknown-linux-musl -p rmon-agent
|
|
```
|
|
|
|
The `.cargo/config.toml` in `rmon-agent/` sets `[profile.release]` with `strip = true` and `lto = true` to minimize binary size.
|
|
|
|
## CLI
|
|
|
|
```
|
|
rmon-agent [OPTIONS]
|
|
|
|
Options:
|
|
--config <path> Config file path (default: ~/.rmon/config.toml)
|
|
--port <port> TCP listen port (default: 7891)
|
|
--bind <addr> Bind address (default: 127.0.0.1 — localhost only, relies on SSH tunnel)
|
|
--log <level> Log level: error|warn|info|debug (default: info)
|
|
--log-file <path> Log to file instead of stderr (default: stderr)
|
|
-v, --version Print version and exit
|
|
-h, --help Print help and exit
|
|
```
|
|
|
|
**The agent always binds to localhost only** (`127.0.0.1`). Remote access is exclusively through the SSH tunnel. Never expose the agent port on a public interface.
|
|
|
|
## Configuration File
|
|
|
|
TOML format. The agent watches the config file and reloads source definitions on change (without restarting).
|
|
|
|
```toml
|
|
[storage]
|
|
# Base directory for recorded data
|
|
data_dir = "~/.rmon/data"
|
|
|
|
# Default storage mode for all signals unless overridden per source
|
|
# "continuous" or "circular"
|
|
default_mode = "circular"
|
|
# For circular mode: how much history to keep in memory (and optionally on disk)
|
|
default_window = "2h"
|
|
|
|
[[sources]]
|
|
type = "udp"
|
|
name = "imu_stream"
|
|
bind = "0.0.0.0:9000"
|
|
|
|
# Signals received in each UDP packet (fixed binary layout)
|
|
[[sources.signals]]
|
|
id = "accel_x"
|
|
label = "Accelerometer X"
|
|
unit = "m/s²"
|
|
scale = 0.001
|
|
offset = 0.0
|
|
byte_offset = 0 # position in the UDP payload
|
|
byte_type = "f32_le" # type at that offset: f32_le, f32_be, f64_le, f64_be, i16_le, ...
|
|
path = ["imu", "accelerometer"]
|
|
|
|
[[sources.signals]]
|
|
id = "accel_y"
|
|
label = "Accelerometer Y"
|
|
unit = "m/s²"
|
|
scale = 0.001
|
|
offset = 0.0
|
|
byte_offset = 4
|
|
byte_type = "f32_le"
|
|
path = ["imu", "accelerometer"]
|
|
|
|
# UDP timestamp: how to find the timestamp in the packet
|
|
[sources.timestamp]
|
|
byte_offset = 24
|
|
byte_type = "u64_le"
|
|
format = "microseconds_since_epoch"
|
|
# Other format options:
|
|
# "nanoseconds_since_epoch"
|
|
# "seconds_since_epoch"
|
|
# "milliseconds_relative" (relative to agent start; converted to absolute)
|
|
|
|
[[sources]]
|
|
type = "csv"
|
|
name = "coolant_temps"
|
|
path = "/mnt/shared/data/coolant.csv"
|
|
|
|
# How often to poll the file for new rows (default: 1s)
|
|
poll_interval = "500ms"
|
|
|
|
# Timestamp column
|
|
[sources.timestamp]
|
|
column = "Time"
|
|
format = "%Y.%m.%d %H:%M:%S"
|
|
|
|
[[sources.signals]]
|
|
id = "inlet"
|
|
column = "T_inlet"
|
|
label = "Coolant Inlet Temperature"
|
|
unit = "°C"
|
|
scale = 1.0
|
|
offset = 0.0
|
|
path = ["temperatures", "coolant"]
|
|
|
|
[[sources]]
|
|
type = "epics"
|
|
name = "vacuum"
|
|
# EPICS Channel Access address (if not using broadcast)
|
|
addr = "192.168.1.50:5064"
|
|
|
|
[[sources.signals]]
|
|
id = "chamber_pressure"
|
|
pv = "VAC:CHAMBER:PRESS"
|
|
label = "Chamber Pressure"
|
|
unit = "mbar"
|
|
scale = 1.0
|
|
offset = 0.0
|
|
path = ["vacuum"]
|
|
```
|
|
|
|
## Source Adapter Trait
|
|
|
|
All data sources implement:
|
|
|
|
```rust
|
|
trait DataSource: Send + 'static {
|
|
/// Human-readable name (from config `name` field).
|
|
fn name(&self) -> &str;
|
|
|
|
/// Full list of signals this source provides.
|
|
fn signals(&self) -> Vec<SignalInfo>;
|
|
|
|
/// Start producing samples. Sends to `tx` until the returned future resolves or `tx` is dropped.
|
|
/// This runs in its own tokio task.
|
|
async fn run(self, tx: mpsc::Sender<(SignalId, Sample)>) -> anyhow::Result<()>;
|
|
}
|
|
```
|
|
|
|
New source types are added by implementing this trait and registering in `sources/mod.rs`.
|
|
|
|
## Storage Engine
|
|
|
|
### Circular Buffer (in-memory)
|
|
|
|
Per-signal `VecDeque<Sample>` bounded by `window` duration. New samples evict old ones when the oldest sample is older than `now - window`. Thread-safe via `tokio::sync::RwLock`.
|
|
|
|
Historical queries on circular buffers are served from this in-memory structure.
|
|
|
|
### Continuous Storage (on-disk)
|
|
|
|
Append-only binary log: sequence of fixed-size `(i64, f64)` records (16 bytes each, little-endian). One file per signal per recording session.
|
|
|
|
File path: `{data_dir}/{source_name}/{signal_id}/{session_start_ns}.bin`
|
|
|
|
Historical queries on continuous storage use binary search on the file (records are sorted by timestamp) to find the start offset, then stream records to the client.
|
|
|
|
## Concurrency Model
|
|
|
|
```
|
|
main thread
|
|
└── tokio runtime
|
|
├── listener task (accepts TCP connections)
|
|
├── per-source task (one per DataSource::run)
|
|
│ └── sends to dispatcher channel
|
|
├── dispatcher task (fans out samples to storage + all sessions)
|
|
│ ├── storage writer tasks (one per signal being recorded)
|
|
│ └── per-session sender tasks (one per connected client)
|
|
└── per-session reader task (handles incoming ClientMessage requests)
|
|
```
|
|
|
|
The dispatcher uses a `tokio::sync::broadcast` channel. Each session's sender task has its own `broadcast::Receiver`. Lagged receivers are detected and reported as a warning (UI display gap, not a crash).
|
|
|
|
## Signal ID Namespacing
|
|
|
|
The full `SignalId` used in the protocol is constructed as:
|
|
|
|
```
|
|
{source_type}://{source_name}/{signal_local_id}
|
|
```
|
|
|
|
Example: `udp://imu_stream/accel_x`
|
|
|
|
The `signal_local_id` is the `id` field from the config. Source names must be unique within one agent config.
|
|
|
|
## Logging
|
|
|
|
Uses the `tracing` crate. Output format: structured text to stderr (or log file if `--log-file` is set).
|
|
|
|
No `println!` in production code; use `tracing::{info, warn, error, debug}`.
|