initial commit
This commit is contained in:
@@ -0,0 +1,86 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**RMon** is a real-time data monitor and plotter for distributed control systems accessed over SSH. The UI runs locally; data is acquired by a remote agent. See `usecase.md` for requirements and `docs/` for full specs.
|
||||||
|
|
||||||
|
## Language and Tech Stack
|
||||||
|
|
||||||
|
**Rust throughout** — single language for agent, UI, and shared types.
|
||||||
|
|
||||||
|
| Crate | Role | Key deps |
|
||||||
|
|-------|------|----------|
|
||||||
|
| `rmon-common` | Shared types, protocol message enums | `serde`, `bincode` |
|
||||||
|
| `rmon-agent` | Remote data acquisition server | `tokio`, `tracing`, `anyhow`, `toml`, `serde` |
|
||||||
|
| `rmon-ui` | Operator desktop application | `egui`, `eframe`, `egui_plot`, `tokio`, `tracing` |
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Build everything (debug)
|
||||||
|
cargo build
|
||||||
|
|
||||||
|
# Build the agent as a fully static binary (primary deployment target)
|
||||||
|
cargo build --release --target x86_64-unknown-linux-musl -p rmon-agent
|
||||||
|
|
||||||
|
# Build agent for ARM (e.g. remote Raspberry Pi)
|
||||||
|
cargo build --release --target aarch64-unknown-linux-musl -p rmon-agent
|
||||||
|
|
||||||
|
# Build the UI for the current platform
|
||||||
|
cargo build --release -p rmon-ui
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
cargo test
|
||||||
|
|
||||||
|
# Run tests for a single crate
|
||||||
|
cargo test -p rmon-common
|
||||||
|
cargo test -p rmon-agent
|
||||||
|
|
||||||
|
# Run a single test
|
||||||
|
cargo test -p rmon-agent sources::csv::tests::parse_timestamp
|
||||||
|
|
||||||
|
# Lint
|
||||||
|
cargo clippy --all-targets --all-features
|
||||||
|
|
||||||
|
# Format
|
||||||
|
cargo fmt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture (short version)
|
||||||
|
|
||||||
|
Two executables communicate via a binary TCP protocol over an SSH tunnel:
|
||||||
|
|
||||||
|
- **`rmon-agent`**: runs on the remote machine, acquires data from heterogeneous sources (CSV, UDP, EPICS…), stores it, streams it to connected clients. Built as a fully static musl binary — single file, zero runtime deps, `scp` + run.
|
||||||
|
|
||||||
|
- **`rmon-ui`**: runs on the operator's local desktop. Manages the SSH tunnel automatically (uses the system `ssh` binary + user's `~/.ssh/config`). Renders real-time plots with `egui`/`egui_plot`.
|
||||||
|
|
||||||
|
- **`rmon-common`**: library crate shared by both. Contains all types (`SignalId`, `SignalInfo`, `Sample`, `StorageMode`) and the `ClientMessage`/`AgentMessage` protocol enums serialized with `bincode`.
|
||||||
|
|
||||||
|
Full architecture: `docs/architecture.md`
|
||||||
|
Wire protocol: `docs/protocol.md`
|
||||||
|
Data model: `docs/data-model.md`
|
||||||
|
Agent config and source trait: `docs/agent-spec.md`
|
||||||
|
UI layout and state machine: `docs/ui-spec.md`
|
||||||
|
|
||||||
|
## Key Design Decisions
|
||||||
|
|
||||||
|
- **Protocol framing**: `[u32 LE length][bincode payload]` — no gRPC/HTTP overhead. Both sides are Rust so `bincode` + shared enum types is simplest.
|
||||||
|
- **Agent portability**: `x86_64-unknown-linux-musl` gives a fully static binary requiring only Linux kernel ≥ 3.2. The agent never has a GUI, so no X11/GL needed.
|
||||||
|
- **Remote access UX**: The UI deploys the agent binary via SSH itself (like VSCode Remote). Users only need to enter their SSH host alias; existing `~/.ssh/config` (including `ProxyJump`) handles multi-hop routing.
|
||||||
|
- **UI rendering**: `egui` immediate-mode with `glow` (OpenGL 2.1) backend. For signals with more points than screen pixels, min-max downsampling runs on the CPU before drawing.
|
||||||
|
- **Timestamps**: all source timestamps are normalized to `i64` nanoseconds since Unix epoch inside the source adapter. The rest of the codebase only sees `i64`.
|
||||||
|
- **Concurrency (agent)**: one tokio task per data source → `broadcast` channel → dispatcher fans out to storage writers and per-client session tasks.
|
||||||
|
- **UI/async boundary**: tokio runtime in a background thread; egui on main thread. `std::sync::mpsc` channels carry `UiEvent` (agent→UI) and `Command` (UI→agent).
|
||||||
|
|
||||||
|
## Adding a New Data Source
|
||||||
|
|
||||||
|
1. Create `rmon-agent/src/sources/{name}.rs`
|
||||||
|
2. Implement the `DataSource` trait (`name()`, `signals()`, `async fn run(self, tx)`)
|
||||||
|
3. Add a new variant to `SourceConfig` in `config.rs` and construct it in `sources/mod.rs`
|
||||||
|
4. Add config example to `docs/agent-spec.md`
|
||||||
|
|
||||||
|
## Protocol Changes
|
||||||
|
|
||||||
|
All protocol types live in `rmon-common/src/protocol.rs`. Since `bincode` is not self-describing, any change to `ClientMessage` or `AgentMessage` requires a protocol version bump in the `Handshake` message and both sides must be rebuilt together.
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
# 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}`.
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# RMon Architecture
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
RMon is split into two executables that communicate over a TCP connection tunneled through SSH:
|
||||||
|
|
||||||
|
```
|
||||||
|
Operator Machine Remote Network
|
||||||
|
┌─────────────────────────────┐ ┌──────────────────────────────────────┐
|
||||||
|
│ rmon-ui │ │ rmon-agent │
|
||||||
|
│ ┌──────────┐ ┌───────────┐ │ SSH │ ┌──────────┐ ┌──────────────────┐ │
|
||||||
|
│ │ Plot │ │ Signal │ │ Tunnel │ │ Protocol │ │ Source Adapters │ │
|
||||||
|
│ │ Panels │ │ Tree │◄├──────────┤► │ Server │◄─│ CSV / UDP / EPICS│ │
|
||||||
|
│ └──────────┘ └───────────┘ │ │ └──────────┘ └──────────────────┘ │
|
||||||
|
│ ┌──────────────────────┐ │ │ ┌──────────────────┐ │
|
||||||
|
│ │ SSH Connection Mgr │ │ │ │ Storage Engine │ │
|
||||||
|
│ └──────────────────────┘ │ │ │ Continuous/Circ. │ │
|
||||||
|
└─────────────────────────────┘ │ └──────────────────┘ │
|
||||||
|
└──────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Language and Build Targets
|
||||||
|
|
||||||
|
**Single language: Rust** throughout agent, UI, and shared library.
|
||||||
|
|
||||||
|
| Crate | Purpose | Build Target |
|
||||||
|
|-------|---------|--------------|
|
||||||
|
| `rmon-common` | Shared types and protocol messages | (library, no binary) |
|
||||||
|
| `rmon-agent` | Remote data acquisition server | `x86_64-unknown-linux-musl` (fully static, zero runtime deps) |
|
||||||
|
| `rmon-ui` | Operator desktop application | native platform (dynamically links X11 + libGL) |
|
||||||
|
|
||||||
|
### Why musl for the agent
|
||||||
|
|
||||||
|
The agent must run on remote machines with arbitrary (potentially old) Linux environments without root access. Compiling with `x86_64-unknown-linux-musl` produces a single binary with:
|
||||||
|
- No glibc dependency
|
||||||
|
- No dynamic linker requirement
|
||||||
|
- Works on any Linux kernel ≥ 3.2
|
||||||
|
- Can be copied with `scp` and run immediately
|
||||||
|
|
||||||
|
The UI runs on the operator's own modern desktop, so strict portability is not required there.
|
||||||
|
|
||||||
|
### Why egui for the UI
|
||||||
|
|
||||||
|
- Immediate-mode rendering — natural fit for real-time data that changes every frame
|
||||||
|
- `glow` backend uses OpenGL 2.1+, available on all modern desktop Linux setups
|
||||||
|
- Cross-platform (Linux, Windows, macOS) from a single Rust codebase
|
||||||
|
- Ships as a single binary (dynamically links only X11/Wayland + libGL which are always present on any desktop Linux)
|
||||||
|
- `egui_plot` provides interactive time-series plots with zoom/pan; custom `egui` painter used for high-throughput rendering paths
|
||||||
|
|
||||||
|
## Cargo Workspace Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
rmon/
|
||||||
|
├── Cargo.toml # workspace root
|
||||||
|
├── rmon-common/ # shared types and protocol (lib crate)
|
||||||
|
│ └── src/
|
||||||
|
│ ├── signal.rs # SignalId, SignalInfo, Sample, StorageMode
|
||||||
|
│ └── protocol.rs # ClientMessage, AgentMessage enums
|
||||||
|
├── rmon-agent/ # remote acquisition agent
|
||||||
|
│ ├── .cargo/config.toml # sets default target to x86_64-unknown-linux-musl
|
||||||
|
│ └── src/
|
||||||
|
│ ├── main.rs
|
||||||
|
│ ├── config.rs # TOML config file parsing
|
||||||
|
│ ├── sources/ # one module per data source type
|
||||||
|
│ │ ├── trait.rs # DataSource trait
|
||||||
|
│ │ ├── csv.rs
|
||||||
|
│ │ ├── udp.rs
|
||||||
|
│ │ └── epics.rs
|
||||||
|
│ ├── storage/
|
||||||
|
│ │ ├── continuous.rs
|
||||||
|
│ │ └── circular.rs
|
||||||
|
│ └── server/
|
||||||
|
│ ├── listener.rs # TCP accept loop
|
||||||
|
│ └── session.rs # per-client session handler
|
||||||
|
└── rmon-ui/ # operator desktop UI
|
||||||
|
└── src/
|
||||||
|
├── main.rs
|
||||||
|
├── app.rs # top-level App struct, egui App impl
|
||||||
|
├── connection/
|
||||||
|
│ ├── ssh.rs # SSH tunnel lifecycle
|
||||||
|
│ └── client.rs # async protocol client (wraps TCP stream)
|
||||||
|
├── state/
|
||||||
|
│ └── session.rs # connected session state, signal cache
|
||||||
|
└── ui/
|
||||||
|
├── connect_dialog.rs
|
||||||
|
├── signal_tree.rs
|
||||||
|
├── plot_panel.rs # one per open plot window
|
||||||
|
└── toolbar.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Remote Access: SSH-Based Deployment
|
||||||
|
|
||||||
|
RMon follows the same pattern as VSCode Remote: the UI manages the agent lifecycle transparently, leveraging the user's existing `~/.ssh/config` (including `ProxyJump` for multi-hop networks).
|
||||||
|
|
||||||
|
### Connection flow
|
||||||
|
|
||||||
|
1. User enters an SSH host alias (from their `~/.ssh/config`) and optional agent port
|
||||||
|
2. UI runs: `ssh {host} 'uname -m'` to detect remote architecture
|
||||||
|
3. UI checks if agent binary is current: `ssh {host} 'cat ~/.rmon/agent.sha256'`
|
||||||
|
4. If stale/missing: UI pipes the correct pre-built binary via SSH:
|
||||||
|
```
|
||||||
|
cat rmon-agent-{arch} | ssh {host} 'mkdir -p ~/.rmon && cat > ~/.rmon/agent && chmod +x ~/.rmon/agent'
|
||||||
|
```
|
||||||
|
5. UI starts the agent (if not already running):
|
||||||
|
```
|
||||||
|
ssh {host} 'pgrep -f "rmon-agent --port {port}" || nohup ~/.rmon/agent --port {port} >> ~/.rmon/agent.log 2>&1 &'
|
||||||
|
```
|
||||||
|
6. UI establishes SSH local port forward: `ssh -N -L {local_port}:localhost:{remote_port} {host}`
|
||||||
|
7. UI opens TCP connection to `localhost:{local_port}` and sends `Handshake`
|
||||||
|
|
||||||
|
No special software needs to be installed on the remote machine. All multi-hop SSH configuration is handled transparently by the system `ssh` binary reading the user's `~/.ssh/config`.
|
||||||
|
|
||||||
|
## Data Flow (steady state)
|
||||||
|
|
||||||
|
```
|
||||||
|
Source (e.g. UDP)
|
||||||
|
│ raw packets + source timestamp
|
||||||
|
▼
|
||||||
|
DataSource adapter
|
||||||
|
│ normalizes timestamp → i64 nanoseconds since Unix epoch
|
||||||
|
│ applies scale/offset
|
||||||
|
▼
|
||||||
|
Dispatcher (tokio broadcast channel per signal)
|
||||||
|
│
|
||||||
|
├──► Storage engine (writes to ring buffer or append log)
|
||||||
|
│
|
||||||
|
└──► Session handlers (one per connected UI client)
|
||||||
|
│ batches samples, sends DataBatch over TCP
|
||||||
|
▼
|
||||||
|
rmon-ui client
|
||||||
|
│ updates per-signal ring buffer (last N seconds for display)
|
||||||
|
▼
|
||||||
|
Plot panels (egui repaint)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Protocol Summary
|
||||||
|
|
||||||
|
Binary framing: `[u32 little-endian length][bincode-serialized payload]`
|
||||||
|
|
||||||
|
Messages are typed Rust enums defined in `rmon-common::protocol`. See `docs/protocol.md` for the full message reference.
|
||||||
|
|
||||||
|
## Agent Configuration
|
||||||
|
|
||||||
|
The agent is configured via a TOML file (default: `~/.rmon/config.toml`). It declares:
|
||||||
|
- Which data sources to connect to (type, address, signal definitions)
|
||||||
|
- Default storage policy
|
||||||
|
|
||||||
|
See `docs/agent-spec.md` for the full configuration reference.
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# RMon Data Model
|
||||||
|
|
||||||
|
## Signal
|
||||||
|
|
||||||
|
A **signal** is a named scalar time-series. Every signal has a unique `SignalId` and associated metadata.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Globally unique signal identifier.
|
||||||
|
/// Convention: "{source_type}://{source_name}/{path/to/signal}"
|
||||||
|
/// Examples:
|
||||||
|
/// "udp://imu_stream/accel_x"
|
||||||
|
/// "csv:///mnt/data/temperatures.csv/sensor_3"
|
||||||
|
/// "epics://PV:SYS:TEMP:READBACK"
|
||||||
|
type SignalId = String;
|
||||||
|
|
||||||
|
struct SignalInfo {
|
||||||
|
id: SignalId,
|
||||||
|
label: String, // human-readable display name
|
||||||
|
unit: String, // e.g. "m/s²", "°C", ""
|
||||||
|
scale: f64, // raw_value * scale + offset = engineering_value
|
||||||
|
offset: f64,
|
||||||
|
sampling_period: Option<Duration>,// None if irregular/event-driven
|
||||||
|
source_type: SourceType,
|
||||||
|
path: Vec<String>, // tree path for UI grouping, e.g. ["imu", "accel"]
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SourceType { Csv, Udp, Epics, Custom(String) }
|
||||||
|
```
|
||||||
|
|
||||||
|
Signals are **read-only** from the UI's perspective. The agent owns all signal definitions (from its config file).
|
||||||
|
|
||||||
|
## Sample
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct Sample {
|
||||||
|
/// Nanoseconds since Unix epoch (UTC).
|
||||||
|
/// All source timestamps are normalized to this by the adapter.
|
||||||
|
timestamp: i64,
|
||||||
|
value: f64,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
All timestamps are normalized at ingestion time inside the adapter. The adapter knows the source-specific format (string, u64 seconds, etc.) and converts it. The rest of the system only sees `i64` nanoseconds.
|
||||||
|
|
||||||
|
## Timestamp Normalization Examples
|
||||||
|
|
||||||
|
| Source format | Example | Agent conversion |
|
||||||
|
|--------------|---------|-----------------|
|
||||||
|
| `%Y.%m.%d %H:%M:%S` string | `"2024.01.15 14:32:07"` | parse → UTC `i64` ns |
|
||||||
|
| u64 microseconds since epoch | `1705329127000000` | multiply by 1000 |
|
||||||
|
| u64 seconds since epoch | `1705329127` | multiply by 1_000_000_000 |
|
||||||
|
| Relative milliseconds | `12345` | add agent start epoch |
|
||||||
|
|
||||||
|
Adapters declare their `TimestampFormat` in config; the normalization logic lives in a shared utility in `rmon-agent::sources`.
|
||||||
|
|
||||||
|
## Storage Modes
|
||||||
|
|
||||||
|
The agent can store signal data in two modes per signal group:
|
||||||
|
|
||||||
|
### Continuous Storage
|
||||||
|
|
||||||
|
```rust
|
||||||
|
StorageMode::Continuous
|
||||||
|
```
|
||||||
|
|
||||||
|
- Data is appended to an on-disk log starting when recording is requested
|
||||||
|
- Data is **kept until explicitly deleted** (either via UI command or manual deletion)
|
||||||
|
- File format: binary append log of `(i64 timestamp, f64 value)` pairs, one file per signal
|
||||||
|
- File path: `{storage_dir}/{signal_id_escaped}/{start_timestamp}.bin`
|
||||||
|
|
||||||
|
### Circular (Loop) Storage
|
||||||
|
|
||||||
|
```rust
|
||||||
|
StorageMode::Circular { window: Duration }
|
||||||
|
```
|
||||||
|
|
||||||
|
- Agent maintains an in-memory ring buffer of the last `window` duration
|
||||||
|
- On UI request, can also persist to disk (same format as continuous)
|
||||||
|
- `window` is configured per signal group in the agent config
|
||||||
|
|
||||||
|
### Storage Location
|
||||||
|
|
||||||
|
Configured per agent in `config.toml`. Defaults to `~/.rmon/data/`. The agent never deletes data automatically (except from in-memory circular buffers).
|
||||||
|
|
||||||
|
## Signal Tree
|
||||||
|
|
||||||
|
For UI navigation, signals are organized into a tree using the `path` field of `SignalInfo`. Each element of the `Vec<String>` is a tree level.
|
||||||
|
|
||||||
|
Example paths:
|
||||||
|
```
|
||||||
|
["system", "imu", "accelerometer"] → system > imu > accelerometer > {signals}
|
||||||
|
["temperatures", "coolant"] → temperatures > coolant > {signals}
|
||||||
|
```
|
||||||
|
|
||||||
|
The UI renders this as a collapsible tree panel. Signals can be dragged from the tree onto plot panels.
|
||||||
|
|
||||||
|
## UI-Side Signal Cache
|
||||||
|
|
||||||
|
The UI maintains a per-signal ring buffer for display (configurable, default: last 60 seconds). Incoming `DataBatch` messages are appended to this buffer. Historical query responses fill in older portions.
|
||||||
|
|
||||||
|
The display buffer is separate from the agent's storage — it only holds what the UI needs to render the current view.
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
# RMon Wire Protocol
|
||||||
|
|
||||||
|
## Transport
|
||||||
|
|
||||||
|
- **TCP** over an SSH local port-forward tunnel
|
||||||
|
- **Port**: configurable, default `7891`
|
||||||
|
- One persistent TCP connection per UI client
|
||||||
|
- Multiple clients can connect to the same agent simultaneously
|
||||||
|
|
||||||
|
## Framing
|
||||||
|
|
||||||
|
Every message (in both directions) uses the same length-prefix frame:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────┬────────────────────────────┐
|
||||||
|
│ length : u32 LE │ payload : [u8; length] │
|
||||||
|
└─────────────────────┴────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- `length` is the byte count of the serialized payload (excludes the 4-byte length field itself)
|
||||||
|
- `payload` is a `bincode`-serialized `ClientMessage` (client→agent) or `AgentMessage` (agent→client)
|
||||||
|
- `bincode` configuration: little-endian, fixed-size integers, no trailing bytes
|
||||||
|
|
||||||
|
Maximum frame size: **16 MiB** (`length` values above this are a protocol error; close connection).
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
The `Handshake` / `HandshakeAck` exchange negotiates compatibility. If the agent does not support the client's version it sends `HandshakeAck { accepted: false }` and closes the connection. Both sides must be built from the same `rmon-common` version to guarantee `bincode` schema compatibility.
|
||||||
|
|
||||||
|
## Client → Agent Messages
|
||||||
|
|
||||||
|
```rust
|
||||||
|
enum ClientMessage {
|
||||||
|
/// Must be the first message sent. No other messages are valid before HandshakeAck.
|
||||||
|
Handshake {
|
||||||
|
protocol_version: u16, // current: 1
|
||||||
|
client_version: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Request the full list of signals the agent knows about.
|
||||||
|
/// Agent replies with one SignalList message.
|
||||||
|
ListSignals,
|
||||||
|
|
||||||
|
/// Subscribe to live data for the given signals.
|
||||||
|
/// Agent begins sending DataBatch messages for these signals.
|
||||||
|
/// `from_ns`: if Some, also request buffered/stored data from that timestamp forward
|
||||||
|
/// before live data begins.
|
||||||
|
Subscribe {
|
||||||
|
ids: Vec<SignalId>,
|
||||||
|
from_ns: Option<i64>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Stop live delivery for these signals. Does not affect storage.
|
||||||
|
Unsubscribe {
|
||||||
|
ids: Vec<SignalId>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Request stored historical data for a signal.
|
||||||
|
/// Agent replies with one or more HistoryChunk messages (done=true on last chunk).
|
||||||
|
/// `max_points`: agent may downsample to fit; 0 = no limit.
|
||||||
|
QueryHistory {
|
||||||
|
id: SignalId,
|
||||||
|
from_ns: i64,
|
||||||
|
to_ns: i64,
|
||||||
|
max_points: u32,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Start recording the given signals. Mode overrides any existing mode for these signals.
|
||||||
|
StartRecording {
|
||||||
|
ids: Vec<SignalId>,
|
||||||
|
mode: StorageMode,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Stop recording (data already written is kept).
|
||||||
|
StopRecording {
|
||||||
|
ids: Vec<SignalId>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Request current agent status (version, uptime, source states).
|
||||||
|
GetStatus,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Agent → Client Messages
|
||||||
|
|
||||||
|
```rust
|
||||||
|
enum AgentMessage {
|
||||||
|
/// Response to Handshake.
|
||||||
|
HandshakeAck {
|
||||||
|
protocol_version: u16,
|
||||||
|
agent_version: String,
|
||||||
|
accepted: bool,
|
||||||
|
reject_reason: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Full signal list, sent in response to ListSignals.
|
||||||
|
/// Sent unsolicited again if the signal list changes (source added/removed).
|
||||||
|
SignalList {
|
||||||
|
signals: Vec<SignalInfo>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Live data batch for subscribed signals.
|
||||||
|
/// The agent batches samples collected since the last send (target: ~50ms intervals).
|
||||||
|
/// Multiple signals may be grouped into one message to reduce framing overhead.
|
||||||
|
DataBatch {
|
||||||
|
batches: Vec<SignalBatch>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// One chunk of a historical query response.
|
||||||
|
HistoryChunk {
|
||||||
|
id: SignalId,
|
||||||
|
samples: Vec<Sample>,
|
||||||
|
done: bool, // true on the final chunk for this query
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Acknowledgement of StartRecording / StopRecording.
|
||||||
|
RecordingStatus {
|
||||||
|
id: SignalId,
|
||||||
|
recording: bool,
|
||||||
|
mode: Option<StorageMode>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Response to GetStatus.
|
||||||
|
StatusReport {
|
||||||
|
agent_version: String,
|
||||||
|
hostname: String,
|
||||||
|
uptime_secs: u64,
|
||||||
|
sources: Vec<SourceStatus>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Unrecoverable error for a specific operation. Connection remains open.
|
||||||
|
Error {
|
||||||
|
context: String, // e.g. "QueryHistory" or "Subscribe"
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SignalBatch {
|
||||||
|
id: SignalId,
|
||||||
|
samples: Vec<Sample>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SourceStatus {
|
||||||
|
name: String,
|
||||||
|
source_type: SourceType,
|
||||||
|
connected: bool,
|
||||||
|
last_sample_ns: Option<i64>,
|
||||||
|
error: Option<String>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Session Lifecycle
|
||||||
|
|
||||||
|
```
|
||||||
|
Client Agent
|
||||||
|
│ │
|
||||||
|
│──── Handshake ────────────────────► │
|
||||||
|
│ │
|
||||||
|
│◄─── HandshakeAck (accepted=true) ─── │
|
||||||
|
│ │
|
||||||
|
│──── ListSignals ───────────────────► │
|
||||||
|
│◄─── SignalList ───────────────────── │
|
||||||
|
│ │
|
||||||
|
│──── Subscribe {ids, from_ns} ──────► │ ←── live data starts flowing
|
||||||
|
│◄═══ DataBatch (repeated) ═══════════ │
|
||||||
|
│ │
|
||||||
|
│──── QueryHistory ──────────────────► │
|
||||||
|
│◄─── HistoryChunk (done=false) ────── │
|
||||||
|
│◄─── HistoryChunk (done=false) ────── │
|
||||||
|
│◄─── HistoryChunk (done=true) ─────── │
|
||||||
|
│ │
|
||||||
|
│──── StartRecording ────────────────► │
|
||||||
|
│◄─── RecordingStatus ──────────────── │
|
||||||
|
│ │
|
||||||
|
│ [TCP close / SSH tunnel drops] │
|
||||||
|
│ │ ←── agent continues running,
|
||||||
|
data continues to be stored
|
||||||
|
```
|
||||||
|
|
||||||
|
## Batching Policy
|
||||||
|
|
||||||
|
The agent collects samples from all sources in a shared `tokio::broadcast` channel. A per-session task drains the channel and sends `DataBatch` messages at a target interval of **50 ms** (20 Hz UI update rate). If a batch would exceed 1 MiB, it is split and sent immediately.
|
||||||
|
|
||||||
|
High-frequency signals (e.g., 10 kHz UDP) are automatically decimated to 10× the batch rate (200 Hz) for live display; full-resolution data is still stored.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- `Error` messages are non-fatal; the session continues
|
||||||
|
- If the agent cannot fulfill a `QueryHistory` (signal not found, time range not stored), it sends `Error` then continues
|
||||||
|
- If the TCP connection drops, the agent discards the session's subscriptions; no other sessions are affected
|
||||||
|
- The UI reconnects automatically (with exponential backoff) if the tunnel drops
|
||||||
+164
@@ -0,0 +1,164 @@
|
|||||||
|
# RMon UI Specification
|
||||||
|
|
||||||
|
## Technology
|
||||||
|
|
||||||
|
- **Framework**: `egui` + `eframe` (immediate-mode Rust GUI)
|
||||||
|
- **Render backend**: `glow` (OpenGL 2.1+) — chosen for maximum Linux compatibility
|
||||||
|
- **Plotting**: `egui_plot` for interactive panels; custom `egui` painter (line strip primitives) for high-throughput paths (>100k points/frame)
|
||||||
|
- **Async runtime**: `tokio` (runs in a background thread; communicates with egui via `std::sync::mpsc`)
|
||||||
|
- **SSH tunnel management**: spawns system `ssh` subprocess
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Single binary. On Linux, dynamically links only:
|
||||||
|
- `libGL` (OpenGL driver, always present on any desktop Linux)
|
||||||
|
- `libX11` / `libxcb` (X11, or Wayland via `libwayland-client` with `wayland` feature)
|
||||||
|
- `libc` (glibc, standard on all Linux distributions)
|
||||||
|
|
||||||
|
On Windows: fully static (links Win32 APIs and ANGLE for OpenGL ES via DirectX).
|
||||||
|
|
||||||
|
Pre-built agent binaries for all supported architectures are **embedded in the UI binary** at compile time using `include_bytes!`. This allows the UI to deploy the agent without any extra files.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ [Connect ▼] [host: myserver] [⏺ Record] [⏹ Stop] [Status: OK] │ ← Toolbar
|
||||||
|
├────────────────┬────────────────────────────────────────────────────┤
|
||||||
|
│ Signal Tree │ Plot Panel 1 │ Plot Panel 2 │
|
||||||
|
│ │ │ │
|
||||||
|
│ ▼ imu │ [signal A] [signal B] [✕] │ [signal C] [✕] │
|
||||||
|
│ ▼ accel │ ┌──────────────────────────┐ │ ┌─────────────┐ │
|
||||||
|
│ accel_x ══╪═►│ time-series plot │ │ │ │ │
|
||||||
|
│ accel_y │ │ │ │ │ │ │
|
||||||
|
│ accel_z │ │ ╭─╮ ╭──╮ │ │ │ │ │
|
||||||
|
│ ▼ gyro │ │ ╯ ╰──╯ ╰─ │ │ │ │ │
|
||||||
|
│ gyro_x │ │ │ │ └─────────────┘ │
|
||||||
|
│ gyro_y │ │ ┼────────────────────── │ │ │
|
||||||
|
│ ▼ temps │ └──────────────────────────┘ │ [+ Add Plot] │
|
||||||
|
│ inlet │ [cursor A: t=12.3s val=1.2] │ │
|
||||||
|
│ outlet │ [cursor B: t=15.1s Δt=2.8s] │ │
|
||||||
|
│ │ [A-B: Δval=-0.3 mean=1.05] │ │
|
||||||
|
└───────────────┴────────────────────────────────┴───────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Connection Dialog
|
||||||
|
|
||||||
|
Shown on startup (or via toolbar "Connect" button) when not connected.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- **SSH Host**: free text, matched against `~/.ssh/config` Host entries (autocomplete offered)
|
||||||
|
- **Agent Port**: numeric, default `7891`
|
||||||
|
- **Agent Config**: path on remote machine, default `~/.rmon/config.toml`
|
||||||
|
|
||||||
|
On connect:
|
||||||
|
1. Detect remote arch via `ssh {host} uname -m`
|
||||||
|
2. Check / upload agent binary (see `docs/architecture.md` connection flow)
|
||||||
|
3. Start agent if not running
|
||||||
|
4. Establish port forward
|
||||||
|
5. Perform protocol handshake
|
||||||
|
6. Fetch signal list → populate signal tree
|
||||||
|
|
||||||
|
Connection errors are shown inline with a retry button.
|
||||||
|
|
||||||
|
## Signal Tree Panel
|
||||||
|
|
||||||
|
- Collapsible tree mirroring the `path` field of each `SignalInfo`
|
||||||
|
- Each leaf shows: label, unit, last value (live-updated), colored dot (source status)
|
||||||
|
- **Drag-and-drop**: drag a signal leaf onto a plot panel to add it
|
||||||
|
- **Right-click menu**: "Add to plot 1 / 2 / new plot", "Signal info" (opens info popup), "Start recording", "Stop recording"
|
||||||
|
- **Search/filter bar** at the top of the panel
|
||||||
|
- Recording state shown with red ⏺ icon on the signal leaf
|
||||||
|
|
||||||
|
## Plot Panels
|
||||||
|
|
||||||
|
Any number of plot panels can be open (horizontally tiled by default, resizable/detachable).
|
||||||
|
|
||||||
|
### Axes
|
||||||
|
- X axis: time (absolute UTC or relative to a reference point; toggled in toolbar)
|
||||||
|
- Y axis: auto-range by default; can be locked manually
|
||||||
|
- Zoom: scroll wheel (time axis), Ctrl+scroll (Y axis)
|
||||||
|
- Pan: click-drag on the plot
|
||||||
|
|
||||||
|
### Signals in a Plot
|
||||||
|
- Each signal rendered as a colored line
|
||||||
|
- Legend at top of plot (click to hide/show individual signals)
|
||||||
|
- Color is auto-assigned, can be changed via right-click on the legend entry
|
||||||
|
- Line style (solid/dashed/dotted) and width configurable per signal
|
||||||
|
|
||||||
|
### Cursors
|
||||||
|
- Add cursors via toolbar button or right-click on plot → "Add cursor"
|
||||||
|
- Drag to position; snaps to nearest sample if within threshold
|
||||||
|
- Measurement bar below the plot shows:
|
||||||
|
- Per cursor: timestamp, value for each visible signal
|
||||||
|
- Between two cursors: Δt, Δvalue, mean, min, max over the interval
|
||||||
|
|
||||||
|
### Math Signals
|
||||||
|
- Right-click plot → "Add math signal"
|
||||||
|
- Expression editor (simple formula: `a + b`, `a * 2.0`, `a - b`, etc. where `a`, `b` are signal IDs)
|
||||||
|
- Math signals appear in the legend with an `f(x)` icon and can be added to other plots
|
||||||
|
|
||||||
|
### High-Throughput Rendering
|
||||||
|
When a signal has more points than pixels in the X direction, the renderer decimates using **min-max downsampling** (preserves peaks/troughs visually). This runs on the CPU in `plot_panel.rs` before passing vertices to egui's painter.
|
||||||
|
|
||||||
|
## Toolbar
|
||||||
|
|
||||||
|
- **Connect / Disconnect** dropdown (shows current host or "Not connected")
|
||||||
|
- **Time range** selector: presets (last 10s / 1min / 10min / 1h) or custom range picker
|
||||||
|
- **Time display** toggle: absolute UTC vs relative
|
||||||
|
- **Record** button: opens signal selection dialog → starts recording for selected signals
|
||||||
|
- **Status indicator**: green = connected, yellow = reconnecting, red = disconnected
|
||||||
|
|
||||||
|
## Keyboard Shortcuts
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|-----|--------|
|
||||||
|
| `Space` | Pause / resume live scrolling |
|
||||||
|
| `R` | Reset all plot zoom/pan to live view |
|
||||||
|
| `C` | Add cursor to focused plot |
|
||||||
|
| `Del` | Remove selected cursor |
|
||||||
|
| `Ctrl+N` | Add new plot panel |
|
||||||
|
| `Ctrl+W` | Close focused plot panel |
|
||||||
|
| `Ctrl+F` | Focus signal tree search |
|
||||||
|
| `Ctrl+S` | Save current layout to file |
|
||||||
|
|
||||||
|
## Session Persistence
|
||||||
|
|
||||||
|
On clean exit, the UI saves the current session to `~/.config/rmon/last_session.toml`:
|
||||||
|
- Connected host alias
|
||||||
|
- Open plot panels and their signal assignments
|
||||||
|
- Cursor positions
|
||||||
|
- Time range and zoom state
|
||||||
|
|
||||||
|
On next launch with the same host, it offers to restore the session.
|
||||||
|
|
||||||
|
## State Machine (connection)
|
||||||
|
|
||||||
|
```
|
||||||
|
Disconnected
|
||||||
|
│ user clicks Connect
|
||||||
|
▼
|
||||||
|
Deploying (copying/verifying agent binary)
|
||||||
|
│ agent ready
|
||||||
|
▼
|
||||||
|
Tunneling (SSH port-forward process running)
|
||||||
|
│ port open
|
||||||
|
▼
|
||||||
|
Handshaking (TCP open, sent Handshake, awaiting HandshakeAck)
|
||||||
|
│ accepted
|
||||||
|
▼
|
||||||
|
Connected (normal operation, DataBatch messages flowing)
|
||||||
|
│ tunnel drops / error
|
||||||
|
▼
|
||||||
|
Reconnecting (exponential backoff, max 30s, reuses same tunnel if still up)
|
||||||
|
│ max retries exceeded
|
||||||
|
▼
|
||||||
|
Disconnected
|
||||||
|
```
|
||||||
|
|
||||||
|
## Async / UI Thread Boundary
|
||||||
|
|
||||||
|
- `tokio` runtime runs in a dedicated OS thread
|
||||||
|
- The egui render loop runs on the main thread (required by most platform window systems)
|
||||||
|
- Communication: `std::sync::mpsc::channel` — the tokio tasks push `UiEvent` variants (DataBatch, SignalList, Status, Error) to the UI; the UI pushes `Command` variants (Subscribe, QueryHistory, StartRecording) to tokio
|
||||||
|
- `eframe::App::update()` drains the incoming event channel each frame before rendering
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
# RMon Use Case
|
||||||
|
|
||||||
|
## Main Goal
|
||||||
|
|
||||||
|
The goal is to have a fast data plotter / monitor for a distributed control system accessible only remotely via multiple ssh jumps
|
||||||
|
|
||||||
|
- The UI will run locally on the operator pc on a modern Linux
|
||||||
|
- The Data will be accessed remotely
|
||||||
|
- The operator can choose to record some of the data for history review
|
||||||
|
|
||||||
|
## Use case
|
||||||
|
|
||||||
|
- The data are acquired at different frequency from a distributed system in a private network
|
||||||
|
- The access to the data are different, some examples include:
|
||||||
|
- CSV file
|
||||||
|
- UDP streams
|
||||||
|
- Specific software (e.g. EPICS)
|
||||||
|
- ...
|
||||||
|
- Timestamp could be different for each case, e.g.:
|
||||||
|
- for CSV could be as string %Y.%m.%d %H:%M:%S
|
||||||
|
- for UDP a u64
|
||||||
|
- etc
|
||||||
|
- Some signal are accessed in a push mode (e.g. udp) and some in pull (e.g. CSV) mode
|
||||||
|
- Each signal could have the following info:
|
||||||
|
- Scale, offset
|
||||||
|
- Unit
|
||||||
|
- Label
|
||||||
|
- Sampling period
|
||||||
|
- Data could be stored, two option:
|
||||||
|
- continuos storage (from user start to user stop, data are kept if not explicitly required)
|
||||||
|
- loop storage (only last N:M hours, N:M specified from user)
|
||||||
|
- The user will access the signals from a UI on his local machine (mostly linux, but windows also possible)
|
||||||
|
- The user UI should be a portable executable with limit dynamic dependencies
|
||||||
|
- In the UI the user can:
|
||||||
|
- have multiple plots
|
||||||
|
- have multiple cursors
|
||||||
|
- make measurements and maths on the signals
|
||||||
|
- hide/show signals
|
||||||
|
- explore the list/tree of available signals and get info on them
|
||||||
|
- request to store siganls
|
||||||
|
- request historical data
|
||||||
|
- drag and drop to add signals
|
||||||
|
- signal plot style can be personalised
|
||||||
Reference in New Issue
Block a user