Files
adas-core/adas-core.module.Relays/README.md
T
2026-06-26 10:29:23 +02:00

223 lines
12 KiB
Markdown

# adas-core.module.Relays — Relay Module
> A **functional module** of the ADAS Core platform.
> Encapsulates all logic for controlling and monitoring **relay hardware devices** (PDU units). This module operates as an independent satellite that the Host registers at startup, providing a unified abstraction over heterogeneous relay hardware.
---
## Table of Contents
1. [Overview](#overview)
2. [Responsibilities](#responsibilities)
3. [Project Structure](#project-structure)
4. [Dependencies](#dependencies)
5. [Device Abstraction](#device-abstraction)
6. [Relay Control Flow](#relay-control-flow)
7. [Design Rules](#design-rules)
---
## Overview
`adas-core.module.Relays` is a modular satellite project that encapsulates every concern related to relay (PDU) hardware within the ADAS Core ecosystem. Relays control power to medical/IoT peripherals — turning outlets on/off, rebooting devices, and reporting outlet status per Point of Care.
Key characteristics:
- **Modular Monolith Pattern** — Self-contained module with device drivers, models, utilities, and settings. Other modules do not depend on it.
- **Polymorphic Devices** — Multiple relay hardware families via `RelayDevice` abstract base. Adding a new family requires only a new derived class.
- **Dual Protocol Support** — `KmTronicRelay` speaks raw UDP hex commands; `KmTronicV2Relay` uses HTTP/XML with Basic Auth.
- **Background Polling** — `RelayDevice` optionally spawns a `Timer` that calls `CheckStatus()` at a configurable refresh interval.
- **Outlet-Level Control** — Per-outlet power on/off, bulk on/off, outlet reboot, and full-device reboot with configurable delay.
- **Mode Awareness** — Supports both "OpenedOffClosedOn" (NC) and standard (NO) wiring modes.
- **Fake/Test Device** — `FakeRelay` provides no-op implementations for unit testing and CI pipelines.
---
## Responsibilities
| Concern | What this project does |
|---------|----------------------|
| **Device Abstraction** | Defines `RelayDevice` abstract base with common state, timers, events, and outlet status tracking. |
| **Hardware Families** | Concrete drivers for KMTronic (UDP hex) and KMTronic V2 (HTTP/XML + Basic Auth). |
| **Outlet Control** | Per-outlet power on, power off, toggle, and timed reboot. |
| **Bulk Control** | Power on/off all outlets simultaneously. |
| **Device Reboot** | Full-device reboot with a configurable `RebootDelay`. |
| **Status Polling** | Optional periodic background refresh via `System.Timers.Timer`. |
| **Status Tracking** | `ConcurrentDictionary`-backed outlet status with `RelayStatusChanged` event. |
| **Mode Adaptation** | Inverts on/off semantics when `Relay.Mode == OpenedOffClosedOn`. |
| **Test Double** | `FakeRelay` no-op driver for isolated testing. |
| **Auth Utilities** | `HttpUtils.AddBasicAuth()` for Base64-encoded Basic authentication headers. |
| **Settings Model** | `RelaySettings` (cache, refresh interval, recording/API URL) consumed by drivers. |
---
## Project Structure
```
adas-core.module.Relays/
├── Models/
│ ├── RelaySettings.cs # Driver tuning: cache, refresh time, recording/API URL
│ └── PowerOutletsConfig.cs # Per-outlet UI configuration (name, icon, visibility, etc.)
├── Devices/
│ ├── RelayDevice.cs # Abstract base: state, timer, events, outlet status
│ ├── KMTronicRelay.cs # UDP hex-protocol driver (legacy KMTronic)
│ ├── KMTronicV2Relay.cs # HTTP/XML driver with Basic Auth (modern KMTronic)
│ └── FakeRelay.cs # No-op test double
└── Utils/
└── HttpUtils.cs # Basic-auth header helper for HTTP drivers
```
| File | Role |
|------|------|
| `RelaySettings.cs` | Configuration model: `Cache` flag, `RefreshTime` (seconds), `RecordingOrApiUrl`. |
| `PowerOutletsConfig.cs` | UI-facing model: `Id`, `Name`, `Outlet`, `Type`, `IconType`, `ShowPowerOutlet`, `PowerOffEnabled`, `ResetEnabled`. |
| `RelayDevice.cs` | Abstract base holding `Relay`/`RelaySettings`, `_relaysStatus` (`ConcurrentDictionary`), optional polling `Timer`, and the `RelayStatusChanged` event. |
| `KMTronicRelay.cs` | UDP-based driver. Sends hex datagrams (`FF0x01` open, `FF0x00` close) over `UdpClient` with 5-second receive timeout. |
| `KMTronicV2Relay.cs` | HTTP/XML driver. Polls `status.xml`, sends `relays.cgi?relay=N` commands. Uses `HttpUtils` for Basic Auth. 15-second HTTP timeout. |
| `FakeRelay.cs` | Internal no-op driver. Returns canned status. Useful for tests and demo environments. |
| `HttpUtils.cs` | Static helper: `AddBasicAuth(username, password, headers)` -> injects `Authorization: Basic ...` into `HttpRequestHeaders`. |
---
## Dependencies
### Downstream References
| Project | Role |
|---------|------|
| `adas-core.Domain` | Uses `Relay`, `RelayEnum` (`Status`, `Mode`, `OutletType`), and shared domain exceptions. |
### Upstream References (projects that depend on this)
| Project | Reason |
|---------|--------|
| `adas-core` (Host) | Registers `IRelayService` (implemented in `adas-core.Infrastructure`) which consumes this module's device classes. Controllers trigger outlet commands. |
| `adas-core.Application` | `IRelayService` is defined here; the Host/Application orchestrates relay workflows. |
| `adas-core.Infrastructure` | `RelayService` (in Infrastructure) instantiates and coordinates `RelayDevice` instances from this module. |
### NuGet Packages
| Package | Version | Purpose |
|---------|---------|---------|
| `Microsoft.Extensions.Hosting` | 10.0.8 | Hosting abstractions for timer-scoped background operations. |
| `AuditLogs` | 1.0.59 | Audit trail for relay state changes and outlet commands. |
---
## Device Abstraction
### `RelayDevice` Abstract Base
All drivers extend `RelayDevice` and inherit:
| Member | Purpose |
|--------|---------|
| `Relay` / `RelaySettings` | Bound configuration and settings objects. |
| `_relaysStatus` | `ConcurrentDictionary<int, RelayEnum.Status>` tracking each outlet. |
| `RelayStatusChanged` | Event fired when an outlet transitions to a new status. |
| `CheckStatus()` | Periodic refresh of outlet states (abstract). |
| `GetStatusRelay(outletId)` | Reads cached status (with `lock` on dictionary). |
| `SetStatusRelay(outletId, status)` | Writes status and raises event if changed (with `lock`). |
| `PowerOnOffRelay(outletId, ms)` | Fire-and-forget: power off, delay, power on. |
| `PowerOnRelay(outletId)` / `PowerOffRelay(outletId)` | Abstract outlet-level commands. |
| `PowerOnAll()` / `PowerOffAll()` | Abstract bulk commands. |
| `RebootOutlet(outletId)` / `Reboot()` | Abstract reboot sequences. |
| `Timer` | Optional background polling when `RelaySettings.RefreshTime > 0`. |
### `KMTronicRelay` — UDP Hex Driver
- **Transport** — `UdpClient` to `Relay.Ip:Relay.Port`.
- **Command Format** — Hex strings sent as ASCII bytes (`FF0N01` = open, `FF0N00` = close).
- **Bulk** — `FFE0FF` (open all), `FFE000` (close all).
- **Status Query** — `FF0000` returns an 8-character bitmask; each char maps to one outlet.
- **Mode Inversion** — When `Relay.Mode == OpenedOffClosedOn`, "open" means ON and "closed" means OFF.
- **Receive Timeout** — 5-second bounded wait on `_udpClient.ReceiveAsync()`; empty result treated as offline.
- **Connection Resilience** — Re-creates `UdpClient` on null or fault; catches and logs socket errors.
### `KMTronicV2Relay` — HTTP/XML Driver
- **Transport** — `HttpClient` to `http://{Relay.Ip}:{Relay.Port}/`.
- **Status Endpoint** — GET `status.xml` -> XML document with `<relay1>...<relay8>` nodes.
- **Control Endpoint** — POST `relays.cgi?relay=N` toggles the specified outlet.
- **Authentication** — `HttpUtils.AddBasicAuth()` injects `Authorization: Basic ...` when `Relay.Username`/`Relay.Password` are set.
- **Polling** — Same `CheckStatus()` contract; parses XML into `_relaysStatus` dictionary.
- **Bulk** — Iterates outlets 1..`Relay.Total` and fires per-outlet POSTs, then refreshes status.
- **Timeout** — 15-second HTTP request timeout; faults return `HttpStatusCode.Gone` to the caller.
### `FakeRelay` — Test Double
- No network calls; all commands are no-ops.
- `GetStatusRelay()` returns a hardcoded `"00000000"` bitmask (all off).
- `CheckDevice()` always returns `true`.
- Internal visibility (`internal class`) — intended for test or DI overrides only.
---
## Relay Control Flow
### Outlet Power Cycle
```
[Host Controller] --> [RelayService.PowerOffRelay(relayId, outletId)]
|
v
[Resolve RelayDevice from cache/settings]
|
v
[device.PowerOffRelay(outletId)]
|
+---------+---------+
| |
[KMTronicRelay] [KMTronicV2Relay]
| |
[Send UDP "FF0N01"] [POST relays.cgi?relay=N]
| |
v v
[Receive bitmask] [Parse HTTP response]
| |
v v
[SetStatusRelay(N, Off)]
|
v
[Raise RelayStatusChanged]
|
v
[Return to controller]
```
### Periodic Status Refresh
1. `Timer` fires every `RefreshTime` seconds (if configured).
2. `CheckStatus()` delegates to the concrete driver.
3. Driver queries hardware (UDP bitmask or HTTP XML).
4. Results are written to `_relaysStatus` via `SetStatusRelay()`.
5. Any changed outlet triggers `RelayStatusChanged`, which the Host can subscribe to for real-time dashboards.
### Full Device Reboot
1. `Reboot()` -> `PowerOffAll()`.
2. Thread sleeps for `Relay.RebootDelay` milliseconds (if > 0).
3. `PowerOnAll()` restores power to all outlets.
---
## Design Rules
1. **Module Isolation** — Must not reference `adas-core.Application`, `adas-core.Infrastructure`, `adas-core.Authentication`, or other modules directly. Communicates through `Relay`/`RelaySettings` models from `Domain`.
2. **Polymorphic Devices** — New relay families are added by inheriting `RelayDevice`. No changes to existing drivers or the Host (Open/Closed).
3. **Thread-Safe State**`_relaysStatus` is a `ConcurrentDictionary` but writes are additionally locked to ensure atomic event raising alongside status updates.
4. **Timer Ownership** — Each `RelayDevice` owns its `Timer`. The timer lifecycle is tied to the device instance. Stopping/starting is handled internally.
5. **Fail-Safe Defaults** — Any receive timeout, parse error, or connection failure results in `RelayEnum.Status.Unknown` for affected outlets. The Host decides how to surface this.
6. **No Business Logic** — This module controls hardware. It does not decide *when* to turn an outlet on/off. Those decisions belong to `Application` or the Host.
7. **Lazy UDP Connection**`KMTronicRelay.Connect()` is called only on first `Send()`. The `UdpClient` is recreated on failure to recover from transient network issues.
8. **Auth Separation**`HttpUtils` is a static utility, not tied to any specific driver. Basic auth logic is reusable by future HTTP-based devices.
9. **Fake Driver for Tests**`FakeRelay` must remain `internal` and must not be referenced from production Host code. It exists solely for unit tests and CI.
10. **Audit Every Mutation** — Every outlet state change (on/off/reboot/bulk) and every status poll emits an audit event via `AuditLogs`.
---
<p align="center">
Back to <a href="../README.md">adas-core Root README</a>
</p>