Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,54 @@
## [Unreleased]

## [0.2.0] – 2026-01-31

### Added
- **Device simulator** (`device_sim` binary) supporting three modes:
- Sensor: Publish-only telemetry (temperature, humidity)
- Actuator: Command handling with status publishing (valve, propulsion)
- Hybrid: Both telemetry and command capabilities
- **NATS-based messaging architecture**:
- Request/reply for actuator commands with timeout handling
- Pub/sub for telemetry forwarding
- Subject-based routing (`devices.*`, `backend.*`)
- **Device registry** with state tracking and configurable timeout detection
- **Exponential backoff connection retry** (1s → 2s → 4s → 8s → 16s → 30s cap)
- Uses bit-shift implementation for efficient exponential calculation
- Resilient reconnection for embedded systems without UI
- **Service management scripts**:
- `service-start.sh` - Start NATS broker in Docker
- `service-stop.sh` - Stop and remove NATS broker
- `demo.sh` - Interactive demo with configurable device count
- **CONTRIBUTING.md** with comprehensive guidelines:
- Code formatting conventions (`// ---` separators)
- Documentation standards for messaging/edge systems
- EMBP (Explicit Module Boundary Pattern) architecture reference
- Testing strategy and coverage expectations

### Changed
- **Applied EMBP architecture pattern** throughout codebase:
- Private module declarations with gateway exports
- Sibling imports via `super::`, external via `crate::`
- Messaging module serves as public API gateway
- **CLI argument parsing** via CLAP with environment variable fallbacks:
- `--nats-url` / `NATS_URL` (default: `nats://localhost:4222`)
- `--device-timeout` / `DEVICE_TIMEOUT` (default: 30s)
- `--interval` / `DEVICE_INTERVAL` for device simulator
- **Async runtime** using Tokio for agent and device simulators

### Documentation
- **README.md**: Added Quick Start guide and demo instructions
- **docs/architecture.md**: Describes gateway vs leaf device patterns
- **CONTRIBUTING.md**: Production-grade documentation standards and EMBP patterns

### Architecture Decisions
- **NATS over MQTT**: Chose NATS for request/reply semantics and simpler implementation
- Avoids MQTT correlation ID complexity (deferred to Phase 2 as `mqtt-rpc` library)
- **Forward raw telemetry**: Edge agent forwards telemetry without aggregation
- Backend has compute/storage for aggregation; keeps edge agent simple
- **Gateway pattern**: Demonstrates coordination between devices and backend
- Not a leaf device - maintains state, routes bidirectionally

## [0.1.0] – 2026-01-27

### Added
Expand Down
307 changes: 307 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
# Contributing to rust-edge-agent

Thanks for considering contributing!

## Quick Start

**New to the project?** See README.md for cross-compilation setup and QEMU validation.

## Local Development

### Starting Services

Before running the agent locally:

```bash
# Start NATS broker
./scripts/service-start.sh

# When done
./scripts/service-stop.sh
```

### Running the Demo

```bash
# Start 3 devices (default)
./scripts/demo.sh

# Start 10 devices
NUM_DEVICES=10 ./scripts/demo.sh

# Use custom telemetry interval
DEVICE_INTERVAL=2 ./scripts/demo.sh
```

See `scripts/demo.sh` for monitoring and testing examples.

**Before submitting a pull request:**

- Run local CI scripts (includes fmt, clippy, and builds):
```bash
./scripts/ci-lint.sh
./scripts/ci-build-native.sh
./scripts/ci-build-aarch64.sh
cargo test --release
```
- If your change affects behavior, please update `CHANGELOG.md` under the [Unreleased] section
- Keep commits focused and descriptive

We follow [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and [Semantic Versioning](https://semver.org/).

## Code Formatting

This project uses `rustfmt` for consistent code formatting. All code should be formatted before committing.

### Visual Separators

Since `rustfmt` removes blank lines at the start of impl blocks, function bodies, and module blocks, we use comment separators `// ---` for visual clarity:

```rust
// Module blocks
mod messaging {
// ---
use super::*;

pub fn start_control_handler() {
// ---
// function body
}
}

// Struct definitions
pub struct DeviceState {
// ---
device_id: String,
last_seen: Instant,
}

// Impl blocks
impl ControlHandler for NatsControlHandler {
// ---
async fn handle_command(&self, cmd: Command) -> Result<Response> {
// ---
// implementation
}
}

// Regular functions
pub async fn route_device_command(device_id: &str, cmd: Command) {
// ---
let client = get_device_client(device_id);
// ...
}

// Struct literals (construction) - NO separator
let config = MqttOptions {
client_id,
broker_addr,
port,
};

// Test modules
#[cfg(test)]
mod tests {
// ---
use super::*;

#[tokio::test]
async fn test_command_routing() {
// ---
// test body
}
}
```

**Style Guidelines:**
1) Use `// ---` for visual separation in at a minimum **module blocks**, **impl blocks**, **struct definitions**, and **function bodies**
2) Place separators after the opening brace and before the first meaningful line
3) Between meaningful steps of logic processing (e.g., separating message parsing, routing, and response handling)
4) For modules: place separator after `mod name {` and before imports/content
5) For impl blocks: place separator after `impl ... {` and before the first method
6) For struct definitions: place separator after `struct Name {` and before field declarations
7) For functions: place separator after function signature and before the main logic
8) Do NOT use separators inside struct literals (during construction)
9) Keep separators consistent across the codebase

**Note:** This project uses rustfmt's default configuration. The `// ---` separator pattern is a formatting convention to work around rustfmt's blank line removal in stable Rust.

## Documentation and Doc Comments

This project follows a **production-grade documentation standard** for Rust code, with special attention to embedded systems and messaging patterns.

### Required Doc Comments

Use Rust doc comments (`///`) for:

- Public structs and enums (especially messaging types like `ControlCommand`, `TelemetryMessage`)
- Public functions (especially handlers and control plane methods)
- Public modules that define architectural boundaries
- Critical system behavior (device lifecycle, message routing, failure handling)
- Macros that encode non-obvious behavior or policy decisions

Doc comments should describe **intent, guarantees, and failure semantics** —
not restate what the code obviously does.

### Messaging/Edge-Specific Documentation

For messaging and control plane code, doc comments should explicitly describe:

- **Failure modes** - What happens when devices disconnect, messages timeout, etc.?
- **Message flow** - Which part of the control/telemetry flow is this?
- **Delivery semantics** - At-most-once, at-least-once, exactly-once?
- **Concurrency** - Can multiple messages be in-flight? How are they handled?

Example:
```rust
/// Routes a control command to the specified device.
///
/// This implements the edge agent's command routing logic, translating
/// backend requests into device-specific commands.
///
/// # Behavior
///
/// - Uses NATS request/reply for synchronous command execution
/// - Waits up to 5 seconds for device acknowledgment
/// - Returns error if device is offline or command times out
///
/// # Errors
///
/// Returns an error if:
/// - The device ID is unknown or offline
/// - The command times out (5s default)
/// - The device returns an error response
pub async fn route_command(device_id: &str, cmd: Command) -> Result<Response> {
// ---
// implementation
}
```

### Optional (Encouraged) Doc Comments

Doc comments or short block comments are encouraged for:

- Internal functions with concurrency or timing implications
- Device state management logic
- Message serialization and validation
- Configuration parsing and validation
- Startup and initialization logic

### Not Required

Doc comments are not required for:

- Trivial helpers
- Simple getters or pass-through functions
- Test code (assert messages should be sufficient)
- Obvious glue code

### General Guidance

- Prefer documenting *why* over *how*
- Be explicit about failure behavior and recovery
- Keep comments accurate and up to date
- Avoid over-documenting trivial code
- For messaging patterns, describe delivery semantics clearly

Well-written doc comments are considered part of the code's correctness, especially for distributed systems and edge infrastructure.

## Architecture Guidelines

This project uses the [Explicit Module Boundary Pattern (EMBP)](https://github.com/JohnBasrai/architecture-patterns/blob/main/rust/embp.md) for module organization. Please review the EMBP documentation before making structural changes.

### Key EMBP Principles

- Each module's public API is defined in its `mod.rs` gateway file
- Sibling modules import from each other using `super::`
- External modules import through `crate::module::`
- Never bypass module gateways with deep imports

### Edge Agent Module Structure

```
src/
├── agent/ # Agent lifecycle and coordination
├── messaging/ # NATS/MQTT messaging abstraction
├── runtime/ # Device registry, state management
└── bin/
└── device_sim.rs # Device simulator for testing
```

## Test Coverage

This project uses a **layered testing approach** optimized for embedded systems:

### Current Test Strategy

**QEMU Smoke Tests (Primary):**
- `scripts/ci-qemu-smoke.sh` - Validates ARM64 binary execution
- Ensures cross-compilation correctness
- Tests basic runtime behavior

**Integration Tests (Planned):**
- Multi-device scenarios with NATS broker
- Command routing and telemetry aggregation
- Failure recovery and reconnect logic

**Unit Tests:**
- Core logic (device state, message parsing)
- Lifecycle transitions
- Error handling

### When to Add Tests

**Add integration tests when:**
- Adding new messaging patterns
- Changing device lifecycle behavior
- Implementing failure recovery logic

**Add unit tests when:**
- Complex business logic needs isolated testing
- Edge cases are difficult to trigger via integration tests
- Testing device state transitions

### Test Organization

```
scripts/
ci-lint.sh # Formatting and clippy
ci-build-native.sh # Native x86_64 build
ci-build-aarch64.sh # ARM64 cross-compilation
ci-qemu-smoke.sh # QEMU validation
```

**Running tests:**
```bash
# Local build validation
./scripts/ci-lint.sh
./scripts/ci-build-native.sh
./scripts/ci-build-aarch64.sh

# QEMU smoke test (requires qemu-user and cross-compilation tools)
./scripts/ci-qemu-smoke.sh

# All workspace tests (use --release to match build configuration)
cargo test --release
```

**Note:** This is a portfolio/demo project showcasing embedded Linux patterns and cross-compilation. Production code would include more comprehensive integration tests and hardware-in-the-loop validation.

## Testing Edge Agent Behavior

When testing edge agent and messaging flows:

- Test both connected and disconnected device scenarios
- Verify message routing and aggregation
- Test timeout and retry behavior
- Include tests for edge cases (duplicate messages, out-of-order delivery)
- Validate device lifecycle transitions

## Cross-Compilation Notes

This project targets `aarch64-unknown-linux-gnu`. When adding dependencies:

- Verify they support cross-compilation (check for C dependencies)
- Test on both native and ARM64 targets
- Document any platform-specific behavior
- Update CI scripts if new system dependencies are needed
13 changes: 12 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
[package]
name = "rust-edge-agent"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
license = "MIT OR Apache-2.0"

[dependencies]
anyhow = "1.0"
async-nats = "0.35"
clap = { version = "4", features = ["derive", "env"] }
futures = "0.3"
rand = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

[[bin]]
name = "rust-edge-agent"
path = "src/main.rs"

[[bin]]
name = "device_sim"
path = "src/bin/device_sim.rs"
Loading