polymech - fw - base 0.9 - its got to go somewhere :)
23
.clangd
Normal file
@ -0,0 +1,23 @@
|
||||
# Basic clangd configuration.
|
||||
# For more options, see: https://clangd.llvm.org/config
|
||||
|
||||
If:
|
||||
PathMatch: [.*\.cpp, .*\.h, .*\.ino]
|
||||
|
||||
CompileFlags:
|
||||
# These flags are added to all builds.
|
||||
# For full project awareness, it's recommended to generate a compile_commands.json by running:
|
||||
# platformio run -t compiledb
|
||||
Add:
|
||||
- -std=gnu++17
|
||||
# From platformio.ini build_flags
|
||||
- -Isrc
|
||||
- -Ilib/polymech-base/src
|
||||
# From platformio.ini board_build.extra_flags for env:waveshare_base
|
||||
- -DBOARD_HAS_PSRAM
|
||||
- -DARDUINO_ESP32S3_DEV
|
||||
- -DARDUINO_USB_MODE=1
|
||||
Remove:
|
||||
- -mlongcalls
|
||||
- -fstrict-volatile-bitfields
|
||||
- -fno-tree-switch-conversion
|
||||
9
.cursorignore
Normal file
@ -0,0 +1,9 @@
|
||||
./.pio
|
||||
./.vscode
|
||||
./data
|
||||
./docs
|
||||
./examples
|
||||
./scripts
|
||||
./tmp
|
||||
./ref
|
||||
./README.md
|
||||
45
.gitignore
vendored
@ -1,6 +1,39 @@
|
||||
/node_modules
|
||||
/coverage
|
||||
*.log
|
||||
.DS_Store
|
||||
CppPotpourri
|
||||
./tmp
|
||||
# PlatformIO
|
||||
.pio
|
||||
.cache/
|
||||
|
||||
# Build artifacts
|
||||
*.elf
|
||||
*.bin
|
||||
*.map
|
||||
|
||||
# Secrets
|
||||
src/config_secrets.h
|
||||
|
||||
# Doxygen Output
|
||||
docs/doxygen/
|
||||
firmware-docs
|
||||
|
||||
# Python build artifacts
|
||||
scripts/__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
tests/reports/
|
||||
src/firmware.ino.cpp
|
||||
data/
|
||||
dist
|
||||
build
|
||||
|
||||
4
CMakeLists.txt
Normal file
@ -0,0 +1,4 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
set(EXTRA_COMPONENT_DIRS src components)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(cassandra-rc2)
|
||||
118
README.md
@ -1,4 +1,114 @@
|
||||
# PolyMech Firmware Library
|
||||
|
||||
|
||||
|
||||
# Polymech Cassandra Firmware
|
||||
|
||||
This repository contains the firmware for the Polymech Cassandra project, running on an ESP32-S3.
|
||||
|
||||
## Overview
|
||||
|
||||
The firmware manages various hardware components and provides multiple interfaces for control and monitoring:
|
||||
|
||||
* **Serial Interface:** For debugging, testing, and direct command execution.
|
||||
* **Modbus TCP Server:** Allows interaction with components using the Modbus protocol over WiFi.
|
||||
* **REST API & Web UI:** Provides a web-based interface (status page, API documentation) and a RESTful API for interacting with the system over WiFi.
|
||||
|
||||
## Architecture
|
||||
|
||||
The firmware is built upon a component-based architecture:
|
||||
|
||||
* **`App` / `PHApp`:** The main application class (`src/PHApp.h`).
|
||||
* **`Component`:** Base class (`src/Component.h`) for all functional units.
|
||||
* **`Bridge`:** (`src/Bridge.h`) Facilitates serial command dispatch.
|
||||
* **`SerialMessage`:** (`src/SerialMessage.h`) Handles serial communication.
|
||||
* **`ModbusManager`:** (`src/ModbusManager.h`) Manages the Modbus TCP server.
|
||||
* **`RESTServer`:** (`src/RestServer.h`) Implements the web server and REST API.
|
||||
* **Details:** See [docs/components.md](./docs/components.md) for how components are structured, configured, and added.
|
||||
|
||||
## Interfaces
|
||||
|
||||
### 1. Serial Communication
|
||||
|
||||
A command-line interface is available over the USB serial port used for programming and monitoring.
|
||||
|
||||
* **Purpose:** Debugging, direct method calls on components.
|
||||
* **Command Format:** `<<component_id;call_type;flags;payload:arg1:arg2...>>`
|
||||
* **Examples & Details:** See [docs/serial.md](./docs/serial.md)
|
||||
* **Sending Commands:** Use `npm run send -- "<command_string>"` or a standard serial terminal.
|
||||
|
||||
### 2. Modbus TCP
|
||||
|
||||
The device runs a Modbus TCP server (slave/responder) on port 502.
|
||||
|
||||
* **Implementation:** Managed by `ModbusManager` using the `eModbus` library.
|
||||
* **Component Interaction:** Components supporting Modbus (like `Relay` or `PHApp` itself) implement `readNetworkValue` and `writeNetworkValue` methods. They are registered with the `ModbusManager` along with their corresponding Modbus addresses.
|
||||
* **Configuration:** Modbus addresses are defined in `src/config-modbus.h`.
|
||||
* **Design Details:** See [docs/design-network.md](./docs/design-network.md)
|
||||
* **Testing:** Use the `python scripts/modbus_*.py` scripts directly (npm script argument passing has issues):
|
||||
|
||||
```bash
|
||||
# Read Coil 51
|
||||
python scripts/modbus_read_coils.py --address 51 --ip-address <DEVICE_IP>
|
||||
# Write Coil 51 ON
|
||||
python scripts/modbus_write_coil.py --address 51 --value 1 --ip-address <DEVICE_IP>
|
||||
# Read Register 20
|
||||
python scripts/modbus_read_registers.py --address 20 --ip-address <DEVICE_IP>
|
||||
# Write Register 20
|
||||
python scripts/modbus_write_register.py --address 20 --value 123 --ip-address <DEVICE_IP>
|
||||
```
|
||||
|
||||
### 3. Web Interface & REST API
|
||||
|
||||
A web server runs on port 80, providing:
|
||||
|
||||
* **Status Page:** `http://<DEVICE_IP>/`
|
||||
* **Swagger UI:** `http://<DEVICE_IP>/api-docs` (Interactive API documentation)
|
||||
* **REST API:** `http://<DEVICE_IP>/api/v1/...`
|
||||
* **Working Endpoints:**
|
||||
* `GET /api/v1/system/info`: Device status.
|
||||
* `GET /api/v1/coils`: List of registered coils.
|
||||
* `GET /api/v1/registers`: List of registered registers.
|
||||
* `GET /api/v1/coils?address=<addr>`: Read a specific registered coil.
|
||||
* `GET /api/v1/registers?address=<addr>`: Read a specific registered register.
|
||||
* `POST /api/v1/relay/test`: Trigger internal relay test.
|
||||
* **Non-Functional Endpoints (Known Issue):**
|
||||
* `POST /coils/{address}`: Returns 404.
|
||||
* `POST /registers/{address}`: Returns 404.
|
||||
* **Details & Examples:** See [docs/web.md](./docs/web.md)
|
||||
* **Testing:** Use `npm run test-api-ip` (note that some tests related to POST endpoints will fail).
|
||||
|
||||
## Building and Development
|
||||
|
||||
See [firmware/.cursor/rules](./.cursor/rules) for a summary of common workflow commands.
|
||||
|
||||
**Key `npm` Scripts:**
|
||||
|
||||
* `npm run build`: Compile the firmware.
|
||||
* `npm run upload`: Upload the firmware to the device.
|
||||
* `npm run clean`: Clean build artifacts.
|
||||
* `npm run monitor`: Open the serial monitor.
|
||||
* `npm run build-web`: Generate web assets (from `swagger.yaml`, `front/`) and build firmware.
|
||||
* `npm run send -- "<command>"`: Send a serial command.
|
||||
* `npm run test-api-ip`: Run the REST API test script against the device IP.
|
||||
* *(Modbus tests need direct python execution, see Modbus section above)*
|
||||
|
||||
## Configuration
|
||||
|
||||
* **Hardware Pins, Features:** `src/config.h`, `src/config_adv.h`
|
||||
* **WiFi Credentials:** `src/config_secrets.h` (ensure this file exists and is populated)
|
||||
* **Modbus Addresses:** `src/config-modbus.h`
|
||||
* **Component IDs:** `src/enums.h` (enum `COMPONENT_KEY`)
|
||||
|
||||
## Todos
|
||||
|
||||
### Web
|
||||
|
||||
* [x] pid stats
|
||||
* [ ] prof enabled
|
||||
* [ ] warmup toggle
|
||||
* [-] press prof - remaining ( parent )
|
||||
* [ ] auto - interlock ( rs 485 connect ) => modes
|
||||
* [ ] press : sp ( ui - mobile)
|
||||
* [ ] partitions : settings not applied
|
||||
|
||||
### FW
|
||||
|
||||
* [x] press - profile ( check abort => vis error )
|
||||
* [x] sp drag / lag
|
||||
|
||||
38
cassandra-uploader.spec
Normal file
@ -0,0 +1,38 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['scripts\\upload_littlefs.py'],
|
||||
pathex=[],
|
||||
binaries=[('scripts\\mklittlefs.exe', '.'), ('scripts\\esptool.exe', '.')],
|
||||
datas=[],
|
||||
hiddenimports=['serial.tools.list_ports', 'serial.tools.list_ports_windows'],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name='cassandra-uploader',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
126
cassandra.code-workspace
Normal file
@ -0,0 +1,126 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "../cli-ts"
|
||||
},
|
||||
{
|
||||
"path": "../web/packages/client"
|
||||
},
|
||||
{
|
||||
"path": "../web/packages/modbus-ui"
|
||||
},
|
||||
{
|
||||
"path": "../../site2"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"files.associations": {
|
||||
"*.embeddedhtml": "html",
|
||||
"**/frontmatter.json": "jsonc",
|
||||
"**/.frontmatter/config/*.json": "jsonc",
|
||||
"*.json.liquid": "json",
|
||||
"*.yaml.liquid": "yaml",
|
||||
"*.md.liquid": "markdown",
|
||||
"*.js.liquid": "liquid-javascript",
|
||||
"*.css.liquid": "liquid-css",
|
||||
"*.scss.liquid": "liquid-scss",
|
||||
"*.cps": "javascript",
|
||||
"*.h": "c",
|
||||
"XASM2INT.C": "cpp",
|
||||
"XASM.H": "cpp",
|
||||
"TNC_f1.H": "cpp",
|
||||
"algorithm": "cpp",
|
||||
"array": "cpp",
|
||||
"atomic": "cpp",
|
||||
"bit": "cpp",
|
||||
"bitset": "cpp",
|
||||
"cctype": "cpp",
|
||||
"charconv": "cpp",
|
||||
"chrono": "cpp",
|
||||
"cinttypes": "cpp",
|
||||
"clocale": "cpp",
|
||||
"cmath": "cpp",
|
||||
"compare": "cpp",
|
||||
"concepts": "cpp",
|
||||
"condition_variable": "cpp",
|
||||
"coroutine": "cpp",
|
||||
"cstdarg": "cpp",
|
||||
"cstddef": "cpp",
|
||||
"cstdint": "cpp",
|
||||
"cstdio": "cpp",
|
||||
"cstdlib": "cpp",
|
||||
"cstring": "cpp",
|
||||
"ctime": "cpp",
|
||||
"cwchar": "cpp",
|
||||
"cwctype": "cpp",
|
||||
"deque": "cpp",
|
||||
"exception": "cpp",
|
||||
"format": "cpp",
|
||||
"forward_list": "cpp",
|
||||
"fstream": "cpp",
|
||||
"functional": "cpp",
|
||||
"future": "cpp",
|
||||
"initializer_list": "cpp",
|
||||
"iomanip": "cpp",
|
||||
"ios": "cpp",
|
||||
"iosfwd": "cpp",
|
||||
"iostream": "cpp",
|
||||
"istream": "cpp",
|
||||
"iterator": "cpp",
|
||||
"limits": "cpp",
|
||||
"list": "cpp",
|
||||
"locale": "cpp",
|
||||
"map": "cpp",
|
||||
"memory": "cpp",
|
||||
"mutex": "cpp",
|
||||
"new": "cpp",
|
||||
"numeric": "cpp",
|
||||
"optional": "cpp",
|
||||
"ostream": "cpp",
|
||||
"queue": "cpp",
|
||||
"random": "cpp",
|
||||
"ratio": "cpp",
|
||||
"regex": "cpp",
|
||||
"semaphore": "cpp",
|
||||
"set": "cpp",
|
||||
"sstream": "cpp",
|
||||
"stack": "cpp",
|
||||
"stdexcept": "cpp",
|
||||
"stop_token": "cpp",
|
||||
"streambuf": "cpp",
|
||||
"string": "cpp",
|
||||
"system_error": "cpp",
|
||||
"thread": "cpp",
|
||||
"tuple": "cpp",
|
||||
"type_traits": "cpp",
|
||||
"typeinfo": "cpp",
|
||||
"unordered_map": "cpp",
|
||||
"unordered_set": "cpp",
|
||||
"utility": "cpp",
|
||||
"variant": "cpp",
|
||||
"vector": "cpp",
|
||||
"xfacet": "cpp",
|
||||
"xhash": "cpp",
|
||||
"xiosbase": "cpp",
|
||||
"xlocale": "cpp",
|
||||
"xlocbuf": "cpp",
|
||||
"xlocinfo": "cpp",
|
||||
"xlocmes": "cpp",
|
||||
"xlocmon": "cpp",
|
||||
"xlocnum": "cpp",
|
||||
"xloctime": "cpp",
|
||||
"xmemory": "cpp",
|
||||
"xstring": "cpp",
|
||||
"xtr1common": "cpp",
|
||||
"xtree": "cpp",
|
||||
"xutility": "cpp",
|
||||
"*.tcc": "cpp",
|
||||
"memory_resource": "cpp",
|
||||
"string_view": "cpp",
|
||||
"complex": "c"
|
||||
}
|
||||
}
|
||||
}
|
||||
1106
compile_commands.json
Normal file
12
config/amperage_budget.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"minHeatingDurationS": 3,
|
||||
"maxHeatingDurationS": 60,
|
||||
"maxHeatingDurationOscillatingS": 15,
|
||||
"maxSimultaneousHeating": 2,
|
||||
"windowOffset": 1,
|
||||
"startIndex": 0,
|
||||
"endIndex": 3,
|
||||
"mode": 3,
|
||||
"postHeatupMode": 0,
|
||||
"opFlags": 0
|
||||
}
|
||||
BIN
config/assets/1.mp3
Normal file
BIN
config/assets/2.mp3
Normal file
BIN
config/assets/3.mp3
Normal file
BIN
config/assets/4.mp3
Normal file
25
config/cassandra-config.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"features": {
|
||||
"ENABLE_FEEDBACK_BUZZER": true,
|
||||
"ENABLE_FEEDBACK_3C": true,
|
||||
"ENABLE_OPERATOR_SWITCH": true,
|
||||
"ENABLE_LOADCELL_0": true,
|
||||
"ENABLE_LOADCELL_1": true,
|
||||
"ENABLE_SOLENOID_0": true,
|
||||
"ENABLE_SOLENOID_1": true,
|
||||
"ENABLE_PRESS_CYLINDER": true,
|
||||
"ENABLE_OMRON_E5": true,
|
||||
"ENABLE_JOYSTICK": true,
|
||||
"ENABLE_AMPERAGE_BUDGET_MANAGER": true,
|
||||
"ENABLE_PROFILE_TEMPERATURE": true,
|
||||
"ENABLE_PROFILE_PRESSURE": true,
|
||||
"ENABLE_PROFILE_SIGNAL_PLOT": true,
|
||||
"ENABLE_INFLUXDB": false,
|
||||
"NUM_OMRON_DEVICES": 8,
|
||||
"PROFILE_PRESSURE_COUNT": 1,
|
||||
"PROFILE_TEMPERATURE_COUNT": 4,
|
||||
"PROFILE_SIGNAL_PLOT_COUNT": 4,
|
||||
"HMI_NAME": "cassandra"
|
||||
},
|
||||
"settings": {}
|
||||
}
|
||||
15
config/elena-config.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"features": {
|
||||
"ENABLE_PLUNGER": true,
|
||||
"ENABLE_DELTA_VFD": true,
|
||||
"ENABLE_POT0": true,
|
||||
"ENABLE_POT1": true,
|
||||
"ENABLE_JOYSTICK": true,
|
||||
"ENABLE_OMRON_E5": true,
|
||||
"NUM_OMRON_DEVICES": 2
|
||||
},
|
||||
"settings":
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
BIN
config/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
291
config/layout.json
Normal file
@ -0,0 +1,291 @@
|
||||
{
|
||||
"pages": {
|
||||
"dashboard-slot-after-connection": {
|
||||
"id": "dashboard-slot-after-connection",
|
||||
"name": "Canvas",
|
||||
"containers": [
|
||||
{
|
||||
"id": "container-1768665967666-gc81al9y2",
|
||||
"type": "container",
|
||||
"columns": 2,
|
||||
"gap": 16,
|
||||
"widgets": [
|
||||
{
|
||||
"id": "widget-1768665986631-pnyqaf1ji",
|
||||
"widgetId": "address-picker",
|
||||
"props": {
|
||||
"slaveId": 0,
|
||||
"selectedAddress": 801,
|
||||
"selectedSource": "coil"
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"id": "widget-1768665997339-vf12a4kzu",
|
||||
"widgetId": "address-picker",
|
||||
"props": {
|
||||
"slaveId": 0,
|
||||
"selectedAddress": 710,
|
||||
"selectedSource": "coil"
|
||||
},
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"id": "widget-1768666032419-8wgg36t7w",
|
||||
"widgetId": "address-picker",
|
||||
"props": {
|
||||
"slaveId": 0,
|
||||
"selectedAddress": 4104,
|
||||
"selectedSource": "register"
|
||||
},
|
||||
"order": 2
|
||||
},
|
||||
{
|
||||
"id": "widget-1768666072997-aii4lpce7",
|
||||
"widgetId": "address-picker",
|
||||
"props": {
|
||||
"slaveId": 0,
|
||||
"selectedAddress": 56,
|
||||
"selectedSource": "coil"
|
||||
},
|
||||
"order": 3
|
||||
},
|
||||
{
|
||||
"id": "widget-1768666088097-hl2q6w5og",
|
||||
"widgetId": "address-picker",
|
||||
"props": {
|
||||
"slaveId": 0,
|
||||
"selectedAddress": 80,
|
||||
"selectedSource": "coil"
|
||||
},
|
||||
"order": 4
|
||||
},
|
||||
{
|
||||
"id": "widget-1768733409630-2ljum5c9d",
|
||||
"widgetId": "address-picker",
|
||||
"props": {
|
||||
"slaveId": 0,
|
||||
"selectedAddress": 706,
|
||||
"selectedSource": "register"
|
||||
},
|
||||
"order": 5
|
||||
}
|
||||
],
|
||||
"children": [],
|
||||
"order": 0,
|
||||
"settings": {
|
||||
"collapsible": true,
|
||||
"collapsed": false,
|
||||
"title": "Main Controls",
|
||||
"showTitle": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "container-1760249904555-8u9wr1616",
|
||||
"type": "container",
|
||||
"columns": 2,
|
||||
"gap": 16,
|
||||
"widgets": [
|
||||
{
|
||||
"id": "widget-1760249914835-0r1hptraq",
|
||||
"widgetId": "profile-playback",
|
||||
"props": {},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"id": "widget-1760249933648-7y0vhqug9",
|
||||
"widgetId": "press-cylinder",
|
||||
"props": {},
|
||||
"order": 1
|
||||
}
|
||||
],
|
||||
"children": [],
|
||||
"order": 1,
|
||||
"settings": {
|
||||
"collapsible": true,
|
||||
"collapsed": false,
|
||||
"title": "Profile Controls",
|
||||
"showTitle": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "container-1768253004402-6kfu3sv6c",
|
||||
"type": "container",
|
||||
"columns": 1,
|
||||
"gap": 16,
|
||||
"widgets": [
|
||||
{
|
||||
"id": "widget-1768253010858-6jyqlriy9",
|
||||
"widgetId": "chart-widget",
|
||||
"props": {
|
||||
"seriesData": "[{\"id\":\"register-1181-1768253019821\",\"address\":1181,\"source\":\"register\",\"color\":\"#00FF00\",\"title\":\"\"},{\"id\":\"register-1199-1768253026183\",\"address\":1199,\"source\":\"register\",\"color\":\"#FFFF00\",\"title\":\"\"},{\"id\":\"register-1217-1768300140574\",\"address\":1217,\"source\":\"register\",\"color\":\"#0000FF\",\"title\":\"\"},{\"id\":\"register-1235-1768300150374\",\"address\":1235,\"source\":\"register\",\"color\":\"#FF0000\",\"title\":\"\"}]"
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"id": "widget-1768816449838-1l8ghai4q",
|
||||
"widgetId": "chart-widget",
|
||||
"props": {
|
||||
"seriesData": "[{\"id\":\"register-361-1768816458019\",\"address\":361,\"source\":\"register\",\"color\":\"#00FF00\",\"title\":\"\"},{\"id\":\"register-367-1768816468818\",\"address\":367,\"source\":\"register\",\"color\":\"#FFFF00\",\"title\":\"\"}]",
|
||||
"height": 300,
|
||||
"duration": 300,
|
||||
"yMax": 2500,
|
||||
"yMin": 0,
|
||||
"refreshRateMs": 1000
|
||||
},
|
||||
"order": 1
|
||||
}
|
||||
],
|
||||
"children": [],
|
||||
"order": 2,
|
||||
"settings": {
|
||||
"collapsible": true,
|
||||
"collapsed": false,
|
||||
"title": "Monitors",
|
||||
"showTitle": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"createdAt": 1760176922064,
|
||||
"updatedAt": 1768816484372
|
||||
},
|
||||
"dashboard-slot-page-bottom": {
|
||||
"id": "dashboard-slot-page-bottom",
|
||||
"name": "Canvas",
|
||||
"containers": [
|
||||
{
|
||||
"id": "container-1768299954893-oicrx3gpf",
|
||||
"type": "container",
|
||||
"columns": 1,
|
||||
"gap": 16,
|
||||
"widgets": [],
|
||||
"children": [],
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"createdAt": 1768299954893,
|
||||
"updatedAt": 1768299954893
|
||||
},
|
||||
"playground-layout": {
|
||||
"id": "playground-layout",
|
||||
"name": "Page playground-layout",
|
||||
"containers": [
|
||||
{
|
||||
"id": "container-1760252883259-0mlxqj84v",
|
||||
"type": "container",
|
||||
"columns": 1,
|
||||
"gap": 16,
|
||||
"widgets": [],
|
||||
"children": [],
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"createdAt": 1760252883259,
|
||||
"updatedAt": 1760252883259
|
||||
},
|
||||
"profile-layout": {
|
||||
"id": "profile-layout",
|
||||
"name": "Page profile-layout",
|
||||
"containers": [
|
||||
{
|
||||
"id": "container-1760252867711-e0um6x5uu",
|
||||
"type": "container",
|
||||
"columns": 1,
|
||||
"gap": 16,
|
||||
"widgets": [],
|
||||
"children": [],
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"createdAt": 1760252867711,
|
||||
"updatedAt": 1760252867711
|
||||
},
|
||||
"dashboard-layout": {
|
||||
"id": "dashboard-layout",
|
||||
"name": "Page dashboard-layout",
|
||||
"containers": [
|
||||
{
|
||||
"id": "container-1760252873294-ljnit8v9u",
|
||||
"type": "container",
|
||||
"columns": 1,
|
||||
"gap": 16,
|
||||
"widgets": [],
|
||||
"children": [],
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"createdAt": 1760252873294,
|
||||
"updatedAt": 1760252873294
|
||||
},
|
||||
"signals-slot-after-connection": {
|
||||
"id": "signals-slot-after-connection",
|
||||
"name": "Canvas",
|
||||
"containers": [
|
||||
{
|
||||
"id": "container-1768665695177-338cub7e5",
|
||||
"type": "container",
|
||||
"columns": 1,
|
||||
"gap": 16,
|
||||
"widgets": [],
|
||||
"children": [],
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"createdAt": 1768665695177,
|
||||
"updatedAt": 1768665695177
|
||||
},
|
||||
"profile-slot-after-connection": {
|
||||
"id": "profile-slot-after-connection",
|
||||
"name": "Canvas",
|
||||
"containers": [
|
||||
{
|
||||
"id": "container-1768665795342-tylwjuy0o",
|
||||
"type": "container",
|
||||
"columns": 1,
|
||||
"gap": 16,
|
||||
"widgets": [],
|
||||
"children": [],
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"createdAt": 1768665795342,
|
||||
"updatedAt": 1768665795342
|
||||
},
|
||||
"profile-slot-page-bottom": {
|
||||
"id": "profile-slot-page-bottom",
|
||||
"name": "Canvas",
|
||||
"containers": [
|
||||
{
|
||||
"id": "container-1768665798019-6oi1ux1zn",
|
||||
"type": "container",
|
||||
"columns": 1,
|
||||
"gap": 16,
|
||||
"widgets": [],
|
||||
"children": [],
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"createdAt": 1768665798019,
|
||||
"updatedAt": 1768665798019
|
||||
},
|
||||
"signals-slot-page-bottom": {
|
||||
"id": "signals-slot-page-bottom",
|
||||
"name": "Canvas",
|
||||
"containers": [
|
||||
{
|
||||
"id": "container-1768672552114-t8ay2arzh",
|
||||
"type": "container",
|
||||
"columns": 1,
|
||||
"gap": 16,
|
||||
"widgets": [],
|
||||
"children": [],
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"createdAt": 1768672552114,
|
||||
"updatedAt": 1768672552114
|
||||
}
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"lastUpdated": 1768816492699
|
||||
}
|
||||
326
config/layout_blank.json
Normal file
@ -0,0 +1,326 @@
|
||||
{
|
||||
"pages": {
|
||||
"dashboard-slot-after-connection": {
|
||||
"id": "dashboard-slot-after-connection",
|
||||
"name": "Canvas",
|
||||
"containers": [
|
||||
{
|
||||
"id": "container-1760249904555-8u9wr1616",
|
||||
"type": "container",
|
||||
"columns": 2,
|
||||
"gap": 16,
|
||||
"widgets": [
|
||||
{
|
||||
"id": "widget-1760249914835-0r1hptraq",
|
||||
"widgetId": "profile-playback",
|
||||
"props": {},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"id": "widget-1760249933648-7y0vhqug9",
|
||||
"widgetId": "press-cylinder",
|
||||
"props": {},
|
||||
"order": 1
|
||||
}
|
||||
],
|
||||
"children": [],
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"id": "container-1763930328457-f7sq4nsnt",
|
||||
"type": "container",
|
||||
"columns": 2,
|
||||
"gap": 16,
|
||||
"widgets": [
|
||||
{
|
||||
"id": "widget-1763930362044-avhdcazlh",
|
||||
"widgetId": "address-picker",
|
||||
"props": {
|
||||
"slaveId": 0,
|
||||
"selectedAddress": 1199,
|
||||
"selectedSource": "coil"
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"id": "widget-1763930441639-1tqgblf1k",
|
||||
"widgetId": "address-picker",
|
||||
"props": {
|
||||
"slaveId": 0,
|
||||
"selectedAddress": 1217,
|
||||
"selectedSource": "coil"
|
||||
},
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"id": "widget-1763977733979-2fj178n3k",
|
||||
"widgetId": "address-picker",
|
||||
"props": {
|
||||
"slaveId": 0,
|
||||
"selectedAddress": 361,
|
||||
"selectedSource": "register"
|
||||
},
|
||||
"order": 2
|
||||
},
|
||||
{
|
||||
"id": "widget-1763930511250-krer72bfp",
|
||||
"widgetId": "address-picker",
|
||||
"props": {
|
||||
"slaveId": 0,
|
||||
"selectedAddress": 1253,
|
||||
"selectedSource": "coil"
|
||||
},
|
||||
"order": 3
|
||||
},
|
||||
{
|
||||
"id": "widget-1763930479450-xxl5yuhwp",
|
||||
"widgetId": "address-picker",
|
||||
"props": {
|
||||
"slaveId": 0,
|
||||
"selectedAddress": 1235,
|
||||
"selectedSource": "coil"
|
||||
},
|
||||
"order": 4
|
||||
},
|
||||
{
|
||||
"id": "widget-1764670216544-rll38wxvw",
|
||||
"widgetId": "address-picker",
|
||||
"props": {
|
||||
"slaveId": 0,
|
||||
"selectedAddress": 707,
|
||||
"selectedSource": "register"
|
||||
},
|
||||
"order": 5
|
||||
},
|
||||
{
|
||||
"id": "widget-1764670230848-tckw49kq2",
|
||||
"widgetId": "address-picker",
|
||||
"props": {
|
||||
"slaveId": 0,
|
||||
"selectedAddress": 365,
|
||||
"selectedSource": "register"
|
||||
},
|
||||
"order": 6
|
||||
}
|
||||
],
|
||||
"children": [],
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"id": "container-1763931184911-6sz4ajf1t",
|
||||
"type": "container",
|
||||
"columns": 2,
|
||||
"gap": 16,
|
||||
"widgets": [
|
||||
{
|
||||
"id": "widget-1763931204625-c10j6uj9g",
|
||||
"widgetId": "address-picker",
|
||||
"props": {
|
||||
"slaveId": 0,
|
||||
"selectedAddress": 1192,
|
||||
"selectedSource": "coil"
|
||||
},
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"id": "widget-1763931234639-z6h1yr7c9",
|
||||
"widgetId": "address-picker",
|
||||
"props": {
|
||||
"slaveId": 0,
|
||||
"selectedAddress": 1210,
|
||||
"selectedSource": "coil"
|
||||
},
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"id": "widget-1763931273999-x638adq2y",
|
||||
"widgetId": "address-picker",
|
||||
"props": {
|
||||
"slaveId": 0,
|
||||
"selectedAddress": 1228,
|
||||
"selectedSource": "coil"
|
||||
},
|
||||
"order": 2
|
||||
},
|
||||
{
|
||||
"id": "widget-1763931296400-bfhtce7qn",
|
||||
"widgetId": "address-picker",
|
||||
"props": {
|
||||
"slaveId": 0,
|
||||
"selectedAddress": 1246,
|
||||
"selectedSource": "coil"
|
||||
},
|
||||
"order": 3
|
||||
}
|
||||
],
|
||||
"children": [],
|
||||
"order": 2
|
||||
}
|
||||
],
|
||||
"createdAt": 1760176922064,
|
||||
"updatedAt": 1764670270714
|
||||
},
|
||||
"dashboard-slot-page-bottom": {
|
||||
"id": "dashboard-slot-page-bottom",
|
||||
"name": "Canvas",
|
||||
"containers": [],
|
||||
"createdAt": 1760252298033,
|
||||
"updatedAt": 1760252529390
|
||||
},
|
||||
"profile-layout": {
|
||||
"id": "profile-layout",
|
||||
"name": "Page profile-layout",
|
||||
"containers": [
|
||||
{
|
||||
"id": "container-1760252867711-e0um6x5uu",
|
||||
"type": "container",
|
||||
"columns": 1,
|
||||
"gap": 16,
|
||||
"widgets": [],
|
||||
"children": [],
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"createdAt": 1760252867711,
|
||||
"updatedAt": 1760252867711
|
||||
},
|
||||
"dashboard-layout": {
|
||||
"id": "dashboard-layout",
|
||||
"name": "Page dashboard-layout",
|
||||
"containers": [
|
||||
{
|
||||
"id": "container-1760252873294-ljnit8v9u",
|
||||
"type": "container",
|
||||
"columns": 1,
|
||||
"gap": 16,
|
||||
"widgets": [],
|
||||
"children": [],
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"createdAt": 1760252873294,
|
||||
"updatedAt": 1760252873294
|
||||
},
|
||||
"playground-layout": {
|
||||
"id": "playground-layout",
|
||||
"name": "Page playground-layout",
|
||||
"containers": [
|
||||
{
|
||||
"id": "container-1760252883259-0mlxqj84v",
|
||||
"type": "container",
|
||||
"columns": 1,
|
||||
"gap": 16,
|
||||
"widgets": [],
|
||||
"children": [],
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"createdAt": 1760252883259,
|
||||
"updatedAt": 1760252883259
|
||||
},
|
||||
"playground-slot-page-bottom": {
|
||||
"id": "playground-slot-page-bottom",
|
||||
"name": "Canvas",
|
||||
"containers": [
|
||||
{
|
||||
"id": "container-1760253124857-5xqo95me5",
|
||||
"type": "container",
|
||||
"columns": 1,
|
||||
"gap": 16,
|
||||
"widgets": [],
|
||||
"children": [],
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"createdAt": 1760253124857,
|
||||
"updatedAt": 1760253124857
|
||||
},
|
||||
"playground-slot-after-connection": {
|
||||
"id": "playground-slot-after-connection",
|
||||
"name": "Canvas",
|
||||
"containers": [
|
||||
{
|
||||
"id": "container-1760253124773-4p3gvtibi",
|
||||
"type": "container",
|
||||
"columns": 1,
|
||||
"gap": 16,
|
||||
"widgets": [],
|
||||
"children": [],
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"createdAt": 1760253124773,
|
||||
"updatedAt": 1760253124773
|
||||
},
|
||||
"profile-slot-page-bottom": {
|
||||
"id": "profile-slot-page-bottom",
|
||||
"name": "Canvas",
|
||||
"containers": [
|
||||
{
|
||||
"id": "container-1764663700718-rubctfgo7",
|
||||
"type": "container",
|
||||
"columns": 1,
|
||||
"gap": 16,
|
||||
"widgets": [],
|
||||
"children": [],
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"createdAt": 1764663700718,
|
||||
"updatedAt": 1764663700718
|
||||
},
|
||||
"profile-slot-after-connection": {
|
||||
"id": "profile-slot-after-connection",
|
||||
"name": "Canvas",
|
||||
"containers": [
|
||||
{
|
||||
"id": "container-1764663700469-lhrm85l2o",
|
||||
"type": "container",
|
||||
"columns": 1,
|
||||
"gap": 16,
|
||||
"widgets": [],
|
||||
"children": [],
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"createdAt": 1764663700469,
|
||||
"updatedAt": 1764663700469
|
||||
},
|
||||
"signals-slot-page-bottom": {
|
||||
"id": "signals-slot-page-bottom",
|
||||
"name": "Canvas",
|
||||
"containers": [
|
||||
{
|
||||
"id": "container-1763980325281-uliyter0c",
|
||||
"type": "container",
|
||||
"columns": 1,
|
||||
"gap": 16,
|
||||
"widgets": [],
|
||||
"children": [],
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"createdAt": 1763980325281,
|
||||
"updatedAt": 1763980325281
|
||||
},
|
||||
"signals-slot-after-connection": {
|
||||
"id": "signals-slot-after-connection",
|
||||
"name": "Canvas",
|
||||
"containers": [
|
||||
{
|
||||
"id": "container-1764672014515-n0sd8jgvk",
|
||||
"type": "container",
|
||||
"columns": 1,
|
||||
"gap": 16,
|
||||
"widgets": [],
|
||||
"children": [],
|
||||
"order": 0
|
||||
}
|
||||
],
|
||||
"createdAt": 1764672014515,
|
||||
"updatedAt": 1764672014515
|
||||
}
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"lastUpdated": 1764672014515
|
||||
}
|
||||
15
config/network.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"sta_ssid": "Livebox6-EBCD",
|
||||
"sta_password": "c4RK35h4PZNS",
|
||||
"sta_local_ip": "192.168.1.250",
|
||||
"sta_gateway": "192.168.1.1",
|
||||
"sta_subnet": "255.255.255.0",
|
||||
"sta_primary_dns": "192.168.1.1",
|
||||
"sta_secondary_dns": "8.8.8.8",
|
||||
"ap_ssid": "PolyMechAP",
|
||||
"ap_password": "poly1234",
|
||||
"ap_config_ip": "192.168.4.1",
|
||||
"ap_config_gateway": "192.168.4.1",
|
||||
"ap_config_subnet": "255.255.255.240",
|
||||
"hostname": "cassandra-dev"
|
||||
}
|
||||
25
config/plunger.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"speedSlowHz": 16,
|
||||
"speedMediumHz": 20,
|
||||
"speedFastHz": 35,
|
||||
"speedFillPlungeHz": 25,
|
||||
"speedFillHomeHz": 20,
|
||||
"currentJamThresholdMa": 1000,
|
||||
"jammedDurationHomingMs": 100,
|
||||
"jammedDurationMs": 1400,
|
||||
"autoModeHoldDurationMs": 1000,
|
||||
"maxUniversalJamTimeMs": 7000,
|
||||
"fillJoystickHoldDurationMs": 1000,
|
||||
"fillPlungedWaitDurationMs": 1000,
|
||||
"fillHomedWaitDurationMs": 50,
|
||||
"recordHoldDurationMs": 2000,
|
||||
"maxRecordDurationMs": 30000,
|
||||
"replayDurationMs": 4500,
|
||||
"enablePostFlow": false,
|
||||
"postFlowDurationMs": 3000,
|
||||
"postFlowSpeedHz": 15,
|
||||
"currentPostFlowMa": 1300,
|
||||
"postFlowStoppingWaitMs": 500,
|
||||
"postFlowCompleteWaitMs": 500,
|
||||
"defaultMaxOperationDurationMs": 18000
|
||||
}
|
||||
25
config/plunger_default.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"speedSlowHz": 16,
|
||||
"speedMediumHz": 20,
|
||||
"speedFastHz": 35,
|
||||
"speedFillPlungeHz": 25,
|
||||
"speedFillHomeHz": 20,
|
||||
"currentJamThresholdMa": 1000,
|
||||
"jammedDurationHomingMs": 100,
|
||||
"jammedDurationMs": 1400,
|
||||
"autoModeHoldDurationMs": 1000,
|
||||
"maxUniversalJamTimeMs": 7000,
|
||||
"fillJoystickHoldDurationMs": 1000,
|
||||
"fillPlungedWaitDurationMs": 1000,
|
||||
"fillHomedWaitDurationMs": 50,
|
||||
"recordHoldDurationMs": 2000,
|
||||
"maxRecordDurationMs": 30000,
|
||||
"replayDurationMs": 4500,
|
||||
"enablePostFlow": false,
|
||||
"postFlowDurationMs": 3000,
|
||||
"postFlowSpeedHz": 15,
|
||||
"currentPostFlowMa": 1300,
|
||||
"postFlowStoppingWaitMs": 500,
|
||||
"postFlowCompleteWaitMs": 500,
|
||||
"defaultMaxOperationDurationMs": 18000
|
||||
}
|
||||
14
config/press-cylinder.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"homing_threshold": 5,
|
||||
"pressing_threshold": 20,
|
||||
"highload_threshold": 3000,
|
||||
"maxload_threshold": 3200,
|
||||
"sp_deadband_percent": 3,
|
||||
"min_load": 10,
|
||||
"balance_interval": 50,
|
||||
"balance_min_pv_diff": 100,
|
||||
"balance_max_pv_diff": 250,
|
||||
"max_stall_activations": 4,
|
||||
"interlocked": true,
|
||||
"mode": 7
|
||||
}
|
||||
38
config/pressure_profiles.json
Normal file
@ -0,0 +1,38 @@
|
||||
[
|
||||
{
|
||||
"id": 920,
|
||||
"type": 2,
|
||||
"name": "Default Pressure Profile",
|
||||
"description": "",
|
||||
"duration": 1740000,
|
||||
"max": 100,
|
||||
"enabled": true,
|
||||
"controlPoints": [
|
||||
{
|
||||
"x": 0,
|
||||
"y": 700
|
||||
},
|
||||
{
|
||||
"x": 590,
|
||||
"y": 1000
|
||||
},
|
||||
{
|
||||
"x": 1000,
|
||||
"y": 1000
|
||||
},
|
||||
{
|
||||
"x": 1000,
|
||||
"y": 0
|
||||
}
|
||||
],
|
||||
"targetRegisters": [
|
||||
703
|
||||
],
|
||||
"children": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
166
config/profile_defaults.json
Normal file
@ -0,0 +1,166 @@
|
||||
[
|
||||
{
|
||||
"id": 910,
|
||||
"type": 1,
|
||||
"name": "Cas-Master-Lower-Cell-HDPE",
|
||||
"description": "",
|
||||
"duration": 1800000,
|
||||
"max": 165,
|
||||
"slot": 0,
|
||||
"enabled": true,
|
||||
"signalPlot": 0,
|
||||
"pressureProfile": 0,
|
||||
"controlPoints": [
|
||||
{
|
||||
"x": 0,
|
||||
"y": 895
|
||||
},
|
||||
{
|
||||
"x": 400,
|
||||
"y": 1000
|
||||
},
|
||||
{
|
||||
"x": 816,
|
||||
"y": 1000
|
||||
},
|
||||
{
|
||||
"x": 1000,
|
||||
"y": 750
|
||||
}
|
||||
],
|
||||
"targetRegisters": [
|
||||
1227,
|
||||
1245,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"children": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 911,
|
||||
"type": 1,
|
||||
"name": "Post-Die-Pressing",
|
||||
"description": "",
|
||||
"duration": 3600000,
|
||||
"max": 150,
|
||||
"slot": 1,
|
||||
"enabled": false,
|
||||
"signalPlot": -1,
|
||||
"pressureProfile": -1,
|
||||
"controlPoints": [
|
||||
{
|
||||
"x": 0,
|
||||
"y": 950
|
||||
},
|
||||
{
|
||||
"x": 663,
|
||||
"y": 1000
|
||||
},
|
||||
{
|
||||
"x": 1000,
|
||||
"y": 0
|
||||
}
|
||||
],
|
||||
"targetRegisters": [
|
||||
1227,
|
||||
1245,
|
||||
1299,
|
||||
1317,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"children": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 912,
|
||||
"type": 1,
|
||||
"name": "TempProfile_912_Slot_2",
|
||||
"description": "",
|
||||
"duration": 0,
|
||||
"max": 0,
|
||||
"slot": 2,
|
||||
"enabled": false,
|
||||
"signalPlot": -1,
|
||||
"pressureProfile": -1,
|
||||
"controlPoints": [
|
||||
{
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"x": 1000,
|
||||
"y": 1000
|
||||
}
|
||||
],
|
||||
"targetRegisters": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"children": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 913,
|
||||
"type": 1,
|
||||
"name": "TempProfile_913_Slot_3",
|
||||
"description": "",
|
||||
"duration": 0,
|
||||
"max": 0,
|
||||
"slot": 3,
|
||||
"enabled": false,
|
||||
"signalPlot": -1,
|
||||
"pressureProfile": -1,
|
||||
"controlPoints": [
|
||||
{
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"x": 1000,
|
||||
"y": 1000
|
||||
}
|
||||
],
|
||||
"targetRegisters": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"children": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
82
config/profile_defaults2.json
Normal file
@ -0,0 +1,82 @@
|
||||
[
|
||||
{
|
||||
"slot": 0,
|
||||
"name": "Cas-Master-Lower-Cell-HDPE",
|
||||
"description": "",
|
||||
"duration": 5400000,
|
||||
"max": 165,
|
||||
"controlPoints": [
|
||||
{
|
||||
"x": 0,
|
||||
"y": 895
|
||||
},
|
||||
{
|
||||
"x": 400,
|
||||
"y": 1000
|
||||
},
|
||||
{
|
||||
"x": 816,
|
||||
"y": 1000
|
||||
},
|
||||
{
|
||||
"x": 1000,
|
||||
"y": 750
|
||||
}
|
||||
],
|
||||
"targetRegisters": [
|
||||
1227,
|
||||
1245,
|
||||
1209,
|
||||
1191
|
||||
],
|
||||
"signalPlot": 0,
|
||||
"children": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"enabled": 1,
|
||||
"pressureProfile": 0,
|
||||
"pressureTargetRegisters": [
|
||||
703
|
||||
]
|
||||
},
|
||||
{
|
||||
"slot": 1,
|
||||
"name": "Post-Die-Pressing",
|
||||
"description": "",
|
||||
"duration": 3600000,
|
||||
"max": 150,
|
||||
"controlPoints": [
|
||||
{
|
||||
"x": 0,
|
||||
"y": 950
|
||||
},
|
||||
{
|
||||
"x": 663,
|
||||
"y": 1000
|
||||
},
|
||||
{
|
||||
"x": 1000,
|
||||
"y": 0
|
||||
}
|
||||
],
|
||||
"targetRegisters": [
|
||||
1227,
|
||||
1245,
|
||||
1299,
|
||||
1317
|
||||
],
|
||||
"signalPlot": -1,
|
||||
"children": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"enabled": 0,
|
||||
"pressureProfile": -1,
|
||||
"pressureTargetRegisters": []
|
||||
}
|
||||
]
|
||||
168
config/settings.json
Normal file
@ -0,0 +1,168 @@
|
||||
{
|
||||
"master": "plc",
|
||||
"slaves": [
|
||||
"cassandra-1"
|
||||
],
|
||||
"partitions": [
|
||||
{
|
||||
"name": "Cassandra Left",
|
||||
"controllers": [
|
||||
{
|
||||
"slaveid": 10,
|
||||
"name": "Carina",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"slaveid": 11,
|
||||
"name": "Castor",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Cassandra Right",
|
||||
"controllers": [
|
||||
{
|
||||
"slaveid": 12,
|
||||
"name": "Coma B",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"slaveid": 13,
|
||||
"name": "Corvus",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"startslaveid": 14,
|
||||
"numcontrollers": 4
|
||||
}
|
||||
],
|
||||
"settings": [
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "ALWAYS_WARMUP",
|
||||
"group": "heating",
|
||||
"flags": 0,
|
||||
"parent": 0,
|
||||
"type": "bool",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "PID_LAG_COMPENSATION",
|
||||
"group": "heating",
|
||||
"flags": 0,
|
||||
"parent": 0,
|
||||
"type": "bool",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "ALWAYS_STOP_PIDS_ON_PROFILE_FINISHED",
|
||||
"group": "heating",
|
||||
"flags": 0,
|
||||
"parent": 0,
|
||||
"type": "bool",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "MAX_TEMPERATURE",
|
||||
"group": "heating",
|
||||
"flags": 0,
|
||||
"parent": 0,
|
||||
"type": "long",
|
||||
"value": 280
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "MIN_TEMPERATURE",
|
||||
"group": "heating",
|
||||
"flags": 0,
|
||||
"parent": 0,
|
||||
"type": "long",
|
||||
"value": 10
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "SP_DEADBAND",
|
||||
"group": "heating",
|
||||
"flags": 0,
|
||||
"parent": 0,
|
||||
"type": "long",
|
||||
"value": 10
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "SEQUENTIAL_HEATING_EXCLUSIVE",
|
||||
"group": "heating",
|
||||
"flags": 0,
|
||||
"parent": 0,
|
||||
"type": "bool",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "LOADCELL_SLAVE_ID_0",
|
||||
"group": "press",
|
||||
"flags": 0,
|
||||
"parent": 0,
|
||||
"type": "long",
|
||||
"value": 20
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "LOADCELL_SLAVE_ID_1",
|
||||
"group": "press",
|
||||
"flags": 0,
|
||||
"parent": 0,
|
||||
"type": "long",
|
||||
"value": 21
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "SOLENOID_0_MB_ADDR",
|
||||
"group": "press",
|
||||
"flags": 0,
|
||||
"parent": 0,
|
||||
"type": "long",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "OPERATION_TIMEOUT",
|
||||
"group": "Modbus RTU",
|
||||
"flags": 0,
|
||||
"parent": 0,
|
||||
"type": "long",
|
||||
"value": 100
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "OMRON_E5_READ_BLOCK_INTERVAL",
|
||||
"group": "Modbus RTU",
|
||||
"flags": 0,
|
||||
"parent": 0,
|
||||
"type": "long",
|
||||
"value": 80
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "ALWAYS_USE_SEQUENTIAL_HEATING",
|
||||
"group": "heating",
|
||||
"flags": 0,
|
||||
"parent": 0,
|
||||
"type": "bool",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "SOLENOID_1_MB_ADDR",
|
||||
"group": "press",
|
||||
"flags": 0,
|
||||
"parent": 0,
|
||||
"type": "long",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
167
config/signal_plots.json
Normal file
@ -0,0 +1,167 @@
|
||||
[
|
||||
{
|
||||
"slot": 0,
|
||||
"name": "SampleSignalPlot_0",
|
||||
"duration": 1800000,
|
||||
"status": 4,
|
||||
"enabled": true,
|
||||
"elapsed": 0,
|
||||
"remaining": 0,
|
||||
"controlPoints": [
|
||||
{
|
||||
"id": 3,
|
||||
"time": 27,
|
||||
"name": "Start PID Controllers",
|
||||
"description": "New control point",
|
||||
"state": 0,
|
||||
"type": 11,
|
||||
"arg_0": 0,
|
||||
"arg_1": 0,
|
||||
"arg_2": 0
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"time": 1,
|
||||
"name": "Write Holding Register",
|
||||
"description": "null",
|
||||
"state": 2,
|
||||
"type": 2,
|
||||
"arg_0": 704,
|
||||
"arg_1": 7,
|
||||
"arg_2": 0
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"time": 352,
|
||||
"name": "Display Message",
|
||||
"description": "Press !",
|
||||
"state": 0,
|
||||
"type": 7,
|
||||
"arg_0": 0,
|
||||
"arg_1": 0,
|
||||
"arg_2": 0
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"time": 627,
|
||||
"name": "Plastic melted",
|
||||
"description": "New control point",
|
||||
"state": 0,
|
||||
"type": 7,
|
||||
"arg_0": 0,
|
||||
"arg_1": 0,
|
||||
"arg_2": 0
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"time": 678,
|
||||
"name": "Display Message",
|
||||
"description": "Cooling down",
|
||||
"state": 0,
|
||||
"type": 7,
|
||||
"arg_0": 0,
|
||||
"arg_1": 0,
|
||||
"arg_2": 0
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"time": 695,
|
||||
"name": "Display Message",
|
||||
"description": "Insert Die !",
|
||||
"state": 0,
|
||||
"type": 7,
|
||||
"arg_0": 0,
|
||||
"arg_1": 0,
|
||||
"arg_2": 0
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"time": 1000,
|
||||
"name": "Stop PID Controllers",
|
||||
"description": "New control point",
|
||||
"state": 0,
|
||||
"type": 10,
|
||||
"arg_0": 0,
|
||||
"arg_1": 0,
|
||||
"arg_2": 0
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"slot": 1,
|
||||
"name": "ShortPlot_70s",
|
||||
"duration": 70000,
|
||||
"status": 0,
|
||||
"enabled": false,
|
||||
"elapsed": 0,
|
||||
"remaining": 0,
|
||||
"controlPoints": [
|
||||
{
|
||||
"id": 1,
|
||||
"time": 250,
|
||||
"name": "Valve Open",
|
||||
"description": "Open valve at address 1",
|
||||
"state": 2,
|
||||
"type": 1,
|
||||
"arg_0": 1,
|
||||
"arg_1": 1,
|
||||
"arg_2": 0
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"time": 750,
|
||||
"name": "Valve Close",
|
||||
"description": "Close valve at address 1",
|
||||
"state": 2,
|
||||
"type": 1,
|
||||
"arg_0": 1,
|
||||
"arg_1": 0,
|
||||
"arg_2": 0
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"slot": 2,
|
||||
"name": "SignalPlot_922_Slot_2",
|
||||
"duration": 0,
|
||||
"status": 0,
|
||||
"enabled": false,
|
||||
"elapsed": 0,
|
||||
"remaining": 0,
|
||||
"controlPoints": [],
|
||||
"children": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"slot": 3,
|
||||
"name": "SignalPlot_923_Slot_3",
|
||||
"duration": 0,
|
||||
"status": 0,
|
||||
"enabled": false,
|
||||
"elapsed": 0,
|
||||
"remaining": 0,
|
||||
"controlPoints": [],
|
||||
"children": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
90
config/state.json
Normal file
@ -0,0 +1,90 @@
|
||||
{
|
||||
"tempProfiles": [
|
||||
{
|
||||
"elapsedSec": 0,
|
||||
"enabled": true,
|
||||
"running": false
|
||||
},
|
||||
{
|
||||
"elapsedSec": 0,
|
||||
"enabled": false,
|
||||
"running": false
|
||||
},
|
||||
{
|
||||
"elapsedSec": 0,
|
||||
"enabled": false,
|
||||
"running": false
|
||||
},
|
||||
{
|
||||
"elapsedSec": 0,
|
||||
"enabled": false,
|
||||
"running": false
|
||||
}
|
||||
],
|
||||
"omronPids": [
|
||||
{
|
||||
"slaveId": 10,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"slaveId": 11,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"slaveId": 12,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"slaveId": 13,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"slaveId": 14,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"slaveId": 15,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"slaveId": 16,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"slaveId": 17,
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"totalKwh": 0,
|
||||
"totalCents": 0,
|
||||
"totalRuntimeHours": 0,
|
||||
"totalHeatingTimeS": 0,
|
||||
"totalHeatingTimePidS": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"totalCycleCount": 0,
|
||||
"isSlave": false,
|
||||
"allOmronStop": false,
|
||||
"allOmronComWrite": true,
|
||||
"managedComponents": [
|
||||
{
|
||||
"id": 701,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": 730,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": 850,
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
5
custom_partitions.csv
Normal file
@ -0,0 +1,5 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xe000, 0x2000,
|
||||
app0, app, factory, 0x10000, 0x300000,
|
||||
spiffs, data, spiffs, 0x310000, 0xCF0000
|
||||
|
6
docs/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
docs/html
|
||||
.DS_Store
|
||||
.idea
|
||||
|
||||
node_modules
|
||||
*.tgz
|
||||
3
docs/.npmignore
Normal file
@ -0,0 +1,3 @@
|
||||
*
|
||||
!doxygen-awesome*
|
||||
|
||||
2898
docs/Doxyfile
Normal file
21
docs/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 - 2023 jothepro
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
39
docs/Makefile
Normal file
@ -0,0 +1,39 @@
|
||||
# SPDX-FileCopyrightText: 2022 Andrea Pappacoda <andrea@pappacoda.it>
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
.POSIX:
|
||||
|
||||
PROJECT = doxygen-awesome-css
|
||||
|
||||
# Paths
|
||||
PREFIX = /usr/local
|
||||
DATADIR = share
|
||||
INSTALLDIR = $(DESTDIR)$(PREFIX)/$(DATADIR)/$(PROJECT)
|
||||
|
||||
# Utilities
|
||||
INSTALL = install -m 644
|
||||
MKDIR = mkdir -p
|
||||
RM = rm -f
|
||||
|
||||
# Files to be installed
|
||||
FILES = doxygen-awesome-darkmode-toggle.js \
|
||||
doxygen-awesome-fragment-copy-button.js \
|
||||
doxygen-awesome-interactive-toc.js \
|
||||
doxygen-awesome-paragraph-link.js \
|
||||
doxygen-awesome-sidebar-only-darkmode-toggle.css \
|
||||
doxygen-awesome-sidebar-only.css \
|
||||
doxygen-awesome-tabs.js \
|
||||
doxygen-awesome.css
|
||||
|
||||
# Empty targets so that `make` and `make clean` do not cause errors
|
||||
all:
|
||||
clean:
|
||||
|
||||
install:
|
||||
$(MKDIR) $(INSTALLDIR)
|
||||
$(INSTALL) $(FILES) $(INSTALLDIR)/
|
||||
|
||||
uninstall:
|
||||
$(RM) -r $(INSTALLDIR)/
|
||||
|
||||
.PHONY: all clean install uninstall
|
||||
10
docs/Readme.md
Normal file
@ -0,0 +1,10 @@
|
||||
# Documentation
|
||||
|
||||
## Elena
|
||||
|
||||
- [Wifi Settings](./wifi.md)
|
||||
- [Plunger Manual](./plunger-manual.md)
|
||||
- [Plunger Settings](./plunger-settings.md)
|
||||
- [Wifi Settings](./wifi-settings.md)
|
||||
- [Elena - RC2 - Official](https://polymech.io/en/store/products/injection/elena-zmax-rc1/)
|
||||
|
||||
314
docs/amp-budget.md
Normal file
@ -0,0 +1,314 @@
|
||||
# Amperage Budget Manager Design
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
This document outlines the design for the `AmperageBudgetManager` component. This component is responsible for managing a group of `OmronE5` temperature controllers (representing heating partitions) to ensure their total power consumption does not exceed a predefined budget. It achieves this by serializing the activation (`run()`) of the controllers based on heating demand (PV < SP) and available budget, using a fair scheduling algorithm.
|
||||
|
||||
## 2. Goals
|
||||
|
||||
* Limit the instantaneous total power consumption of a group of `OmronE5` devices.
|
||||
* Provide a configurable power budget (in Watts).
|
||||
* Implement a fair scheduling mechanism (round-robin with preemption) to ensure all partitions needing heat get a chance to run over time.
|
||||
* Integrate seamlessly with the existing `PHApp` and `OmronE5` components.
|
||||
* Control `OmronE5` devices using their existing `run()` and `stop()` methods.
|
||||
|
||||
## 3. Component Architecture
|
||||
|
||||
### 3.1 Class Diagram
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
Component <|-- AmperageBudgetManager
|
||||
AmperageBudgetManager o-- "0..*" ManagedDevice : contains
|
||||
ManagedDevice o-- OmronE5 : references
|
||||
PHApp o-- AmperageBudgetManager : owns
|
||||
PHApp o-- "*" OmronE5 : owns
|
||||
|
||||
class Component {
|
||||
+setup() virtual
|
||||
+loop() virtual
|
||||
+info() virtual
|
||||
}
|
||||
|
||||
class OmronE5 {
|
||||
+run() bool
|
||||
+stop() bool
|
||||
+getPV(uint16_t&) bool
|
||||
+getSP(uint16_t&) bool
|
||||
+isRunning() bool
|
||||
+getConsumption() uint32_t
|
||||
-_consumption : uint32_t
|
||||
}
|
||||
|
||||
class ManagedDevice {
|
||||
+OmronE5* device
|
||||
+ManagedState state
|
||||
+uint8_t originalIndex
|
||||
}
|
||||
|
||||
class AmperageBudgetManager {
|
||||
+AmperageBudgetManager(uint32_t wattBudget)
|
||||
+addManagedDevice(OmronE5* device)
|
||||
+setup() override
|
||||
+loop() override
|
||||
+info() override
|
||||
-uint32_t _wattBudget
|
||||
-ManagedDevice _managedDevices[]
|
||||
-uint8_t _numDevices
|
||||
-uint8_t _maxDevices
|
||||
-uint8_t _nextDeviceIndex
|
||||
-ManagedState state // Enum definition reference
|
||||
-_allocateBudget()
|
||||
}
|
||||
|
||||
class PHApp {
|
||||
+setup()
|
||||
+loop()
|
||||
}
|
||||
|
||||
enum ManagedState {
|
||||
UNKNOWN
|
||||
IDLE
|
||||
REQUESTING_HEAT
|
||||
HEATING
|
||||
}
|
||||
|
||||
AmperageBudgetManager ..> ManagedState : uses
|
||||
ManagedDevice ..> ManagedState : uses
|
||||
```
|
||||
|
||||
### 3.2 Data Structures
|
||||
|
||||
* **`_wattBudget`**: `uint32_t` - The maximum combined wattage allowed for managed devices in the `HEATING` state.
|
||||
* **`_managedDevices`**: An array or similar structure holding pointers to the managed `OmronE5` instances along with their current state (`ManagedState`) and original registration order for stable sorting.
|
||||
* **`_nextDeviceIndex`**: `uint8_t` - Index used for the round-robin starting point.
|
||||
* **`ManagedState`**: Enum (`UNKNOWN`, `IDLE`, `REQUESTING_HEAT`, `HEATING`) - Tracks the state of each managed device from the budget manager's perspective.
|
||||
|
||||
### 3.3 Key Methods
|
||||
|
||||
* **`AmperageBudgetManager(uint32_t wattBudget)`**: Constructor, sets the budget.
|
||||
* **`addManagedDevice(OmronE5* device)`**: Registers an `OmronE5` instance to be managed.
|
||||
* **`setup()`**: Initializes the manager, sets initial states.
|
||||
* **`loop()`**: The core logic, executed repeatedly. Checks device status, calculates needs, allocates budget, and sends `run()`/`stop()` commands.
|
||||
* **`info()`**: Prints debugging information about managed devices and budget status.
|
||||
* **`_allocateBudget()`**: Internal helper encapsulating the budget allocation logic described in section 4.
|
||||
|
||||
## 4. Scheduling Logic (Round-Robin with Preemption)
|
||||
|
||||
The `loop()` method executes the following logic periodically:
|
||||
|
||||
1. **Identify Needs & Stop Unneeded:**
|
||||
* Iterate through all `_managedDevices`.
|
||||
* For each device, get its current PV, SP, and `isRunning()` status.
|
||||
* If `PV >= SP` (wants to stop) and its state is `HEATING` or `REQUESTING_HEAT`:
|
||||
* Call `device->stop()`.
|
||||
* Set its state to `IDLE`.
|
||||
* If `PV < SP` (wants to run):
|
||||
* If its state is `IDLE`, change state to `REQUESTING_HEAT`.
|
||||
* Keep track of devices currently in `HEATING` state and those in `REQUESTING_HEAT`.
|
||||
|
||||
2. **Prioritize and Allocate Budget (`_allocateBudget`):**
|
||||
* Create a list `potentialRunners` containing all devices currently in the `REQUESTING_HEAT` or `HEATING` state.
|
||||
* Sort `potentialRunners` based on their original registration order, but starting the comparison cycle from `_nextDeviceIndex` to implement round-robin priority. (Effectively, devices closer to `_nextDeviceIndex` in the circular list get higher priority for this cycle).
|
||||
* Initialize `currentWattage = 0`.
|
||||
* Create an empty list `willRun`.
|
||||
* Iterate through the prioritized `potentialRunners`:
|
||||
* Get the device's consumption: `device->getConsumption()`.
|
||||
* If `currentWattage + device->getConsumption() <= _wattBudget`:
|
||||
* Add the device to the `willRun` list.
|
||||
* `currentWattage += device->getConsumption()`.
|
||||
|
||||
3. **Apply Changes:**
|
||||
* Iterate through all `_managedDevices` again:
|
||||
* If the device is in the `willRun` list:
|
||||
* If its state was `REQUESTING_HEAT`, call `device->run()`.
|
||||
* Set its state to `HEATING`.
|
||||
* Else (device is *not* in `willRun` list):
|
||||
* If its state was `HEATING`, call `device->stop()`.
|
||||
* Set its state to `REQUESTING_HEAT` (if PV < SP) or `IDLE` (if PV >= SP - handled in step 1).
|
||||
|
||||
4. **Update Robin Index:**
|
||||
* Increment `_nextDeviceIndex` (wrapping around) to ensure the priority shifts in the next cycle: `_nextDeviceIndex = (_nextDeviceIndex + 1) % _numDevices`.
|
||||
|
||||
### 4.1 State Diagram (for a single managed OmronE5)
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> IDLE : Initialized / PV >= SP
|
||||
|
||||
IDLE --> REQUESTING_HEAT : PV < SP detected
|
||||
IDLE --> IDLE : PV >= SP
|
||||
|
||||
REQUESTING_HEAT --> HEATING : Budget allocated & run() called
|
||||
REQUESTING_HEAT --> IDLE : PV >= SP detected & stop() called
|
||||
REQUESTING_HEAT --> REQUESTING_HEAT : Budget not available
|
||||
|
||||
HEATING --> IDLE : PV >= SP detected & stop() called
|
||||
HEATING --> REQUESTING_HEAT : Budget revoked & stop() called
|
||||
HEATING --> HEATING : Budget remains allocated & PV < SP
|
||||
```
|
||||
|
||||
### 4.2 Sequence Diagram (Simplified `loop` cycle)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant L as loop()
|
||||
participant BM as AmperageBudgetManager
|
||||
participant O1 as OmronE5_1
|
||||
participant O2 as OmronE5_2
|
||||
participant O3 as OmronE5_3
|
||||
|
||||
loop over Managed Devices
|
||||
L->>O1: getPV(), getSP(), isRunning()
|
||||
L->>BM: Update O1 State (e.g., IDLE to REQUESTING_HEAT)
|
||||
L->>O2: getPV(), getSP(), isRunning()
|
||||
L->>BM: Update O2 State (e.g., HEATING to IDLE)
|
||||
L->>O2: stop()
|
||||
L->>O3: getPV(), getSP(), isRunning()
|
||||
L->>BM: Update O3 State (e.g., HEATING remains HEATING)
|
||||
end
|
||||
|
||||
L->>BM: _allocateBudget()
|
||||
BM->>O1: getConsumption()
|
||||
BM->>O3: getConsumption()
|
||||
BM-->>L: Return willRun = [O1, O3]
|
||||
|
||||
loop over Managed Devices
|
||||
L->>BM: Check if O1 in willRun -> Yes
|
||||
BM->>O1: run() (if state was REQUESTING_HEAT)
|
||||
BM->>BM: Set O1 state = HEATING
|
||||
|
||||
L->>BM: Check if O2 in willRun -> No
|
||||
BM->>BM: Set O2 state = IDLE (already stopped)
|
||||
|
||||
L->>BM: Check if O3 in willRun -> Yes
|
||||
BM->>BM: Set O3 state = HEATING (no change needed)
|
||||
end
|
||||
|
||||
L->>BM: Increment _nextDeviceIndex
|
||||
```
|
||||
|
||||
## 5. Configuration and Integration
|
||||
|
||||
* **Instantiation:** An instance of `AmperageBudgetManager` should be created in `PHApp`.
|
||||
```cpp
|
||||
// In PHApp.h (example)
|
||||
#include "components/AmperageBudgetManager.h"
|
||||
// ...
|
||||
AmperageBudgetManager* budgetManager;
|
||||
|
||||
// In PHApp::setup() (example)
|
||||
_amperageBudget = new AmperageBudgetManager(10000); // Example: 10kW budget
|
||||
_amperageBudget->addManagedDevice(_omron1); // Assuming _omron1 is an OmronE5*
|
||||
_amperageBudget->addManagedDevice(_omron2);
|
||||
// ... add other Omrons
|
||||
_amperageBudget->setup();
|
||||
```
|
||||
* **Budget Value:** The power budget (Watts) is passed to the constructor. This could be made configurable via Modbus or REST API by adding setter methods and potentially exposing them through `PHApp`.
|
||||
* **Device Consumption:** The manager relies on `OmronE5::getConsumption()`. This method needs to be added to the `OmronE5` class, returning the `_consumption` value (which might also need to be made configurable per `OmronE5` instance).
|
||||
* **PHApp Loop:** `AmperageBudgetManager::loop()` must be called from `PHApp::loop()`.
|
||||
```cpp
|
||||
// In PHApp::loop()
|
||||
if (_amperageBudget) {
|
||||
_amperageBudget->loop();
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Potential Issues and Refinements
|
||||
|
||||
* **SP Reachability:** If the budget is too low relative to the heat loss and the number/power of partitions, the system might struggle or fail to reach the Set Point (SP) on all devices. This requires careful tuning of the budget.
|
||||
* **Rapid Cycling:** If the budget forces frequent starting/stopping, it might cause wear on relays (if used by the Omron output). Hysteresis could be added (e.g., require PV to be `SP + delta` before stopping, or `SP - delta` before wanting heat).
|
||||
* **Consumption Accuracy:** The accuracy depends on the configured `_consumption` value in each `OmronE5`. If actual consumption varies significantly, the budget management might be inaccurate.
|
||||
* **Error Handling:** The design assumes `OmronE5` methods succeed. Robust error handling (checking return values of `run()`, `stop()`, `getPV`, etc.) should be added.
|
||||
* **Dynamic Budget:** The budget could be adjusted dynamically based on external factors (e.g., total system load).
|
||||
* **Advanced Scheduling:** More complex scheduling (e.g., prioritizing devices further from SP) could be implemented if simple round-robin proves insufficient.
|
||||
|
||||
## 7. Mathematical Modeling and Optimization Considerations
|
||||
|
||||
This section explores the mathematical underpinnings and potential optimizations for the budget management system.
|
||||
|
||||
### 7.1 Core Budget Constraint
|
||||
|
||||
Let N be the number of managed `OmronE5` devices. Let P_i be the power consumption (Watts) of device i when heating, obtained via `device[i]->getConsumption()`. Let S_i(t) be the state of device i at time t, where S_i(t) = 1 if the device is actively heating (state `HEATING`) and S_i(t) = 0 otherwise (`IDLE`, `REQUESTING_HEAT`, or stopped by the manager).
|
||||
|
||||
The fundamental constraint enforced by the `AmperageBudgetManager` at any given time t is:
|
||||
|
||||
```
|
||||
Sum(S_i(t) * P_i for i=1 to N) <= W_budget
|
||||
```
|
||||
|
||||
where W_budget is the configured maximum power budget.
|
||||
|
||||
The scheduling algorithm (Section 4, Step 2) effectively selects the subset of devices H(t) = { i | S_i(t) = 1 } such that this inequality holds, prioritizing devices based on need (PV_i < SP_i) and the round-robin index.
|
||||
|
||||
### 7.2 Error Sources
|
||||
|
||||
Potential sources of error can affect the system's ability to precisely meet the budget or achieve optimal heating:
|
||||
|
||||
1. **Consumption Estimation Error (epsilon_P):** The actual power P_i_actual drawn by a heater might differ from the configured P_i. The total actual power is `Sum(P_i_actual for i in H(t))`. The budget calculation error is `Sum(P_i - P_i_actual for i in H(t))`.
|
||||
* *Mitigation:* Calibration, using more accurate power measurement if available, adding a safety margin to W_budget.
|
||||
2. **Measurement Delay (tau_m):** Time lag between a temperature change, its measurement by the `OmronE5` (PV update), and the `AmperageBudgetManager` reading it.
|
||||
3. **Control Delay (tau_c):** Time lag between the manager deciding to change a device's state (`run()`/`stop()`) and the command being executed and having an effect (e.g., RS485 communication delay, relay actuation time).
|
||||
4. **Sampling Rate:** The `loop()` frequency of the `AmperageBudgetManager` determines how quickly changes are detected and acted upon. A slower rate increases the effective delay.
|
||||
|
||||
### 7.3 Thermal Inertia and Dynamics
|
||||
|
||||
Each heating partition i has thermal characteristics. A simplified model might be a first-order system:
|
||||
|
||||
```
|
||||
C_i * d(PV_i)/dt = Q_heat_i(t) - Q_loss_i(t)
|
||||
```
|
||||
|
||||
where:
|
||||
* C_i is the thermal capacitance of partition i.
|
||||
* PV_i is the process variable (temperature) of partition i.
|
||||
* Q_heat_i(t) is the heat input rate. Q_heat_i(t) = P_i if S_i(t) = 1, and 0 otherwise (assuming idealized heater).
|
||||
* Q_loss_i(t) is the heat loss rate, often modeled as `U_i * (PV_i(t) - T_ambient)`, where U_i is a heat transfer coefficient and T_ambient is the ambient temperature.
|
||||
|
||||
**Implications:**
|
||||
|
||||
* **Lag:** Due to C_i, the temperature PV_i changes gradually in response to Q_heat_i(t). Stopping heat doesn't instantly stop the temperature rise, potentially leading to overshoot, especially if the Omron's internal PID isn't tuned for intermittent operation.
|
||||
* **Interdependence:** Heat loss Q_loss_i(t) might depend not only on T_ambient but also on the temperatures of adjacent partitions (PV_j), creating thermal coupling.
|
||||
* **SP Reachability:** If the average heat input allowed by the budget over time (Avg_Q_heat_i) is less than the heat loss at the setpoint (Q_loss_i(SP_i)), the partition may never reach SP_i.
|
||||
```
|
||||
Avg_Q_heat_i = P_i * (Average Duty Cycle for i) < Q_loss_i(SP_i)
|
||||
```
|
||||
* **Minimum Effective On-Time (tau_min_on_i):** Due to thermal capacitance (C_i) and initial heat loss rate (Q_loss_i(t)), there might be a minimum duration tau_min_on_i for which a heater i must be active (S_i(t) = 1) to produce a significant or useful increase in PV_i. Activating a heater for durations less than tau_min_on_i could be inefficient, consuming power without meaningfully contributing to reaching the setpoint. The scheduling algorithm should ideally ensure that when a device is granted budget, it runs for at least this duration, or consider this constraint when deciding which devices to activate.
|
||||
|
||||
### 7.4 Optimization Objectives
|
||||
|
||||
Beyond simply staying within budget, optimization could target:
|
||||
|
||||
1. **Minimize Time-to-Setpoint:** Reduce the total time for all devices requesting heat to reach their respective SP_i.
|
||||
2. **Maximize Fairness:** Ensure all devices requesting heat receive a proportional amount of heating time over a longer window, preventing starvation.
|
||||
3. **Minimize Overshoot:** Reduce the amount PV_i exceeds SP_i after heating stops.
|
||||
4. **Minimize Control Effort:** Reduce the frequency of `run()`/`stop()` commands to minimize wear on physical components (like relays).
|
||||
5. **Maximize Weighted Priority:** Allow certain devices to have higher priority in budget allocation.
|
||||
|
||||
### 7.5 Potential Control Improvements
|
||||
|
||||
1. **Hysteresis:** Introduce a deadband around SP. Only request heat if `PV_i < SP_i - delta_lower` and only stop requesting heat if `PV_i > SP_i + delta_upper`. This reduces rapid cycling near the setpoint.
|
||||
2. **Priority Scheduling:** Instead of pure round-robin, prioritize devices based on:
|
||||
* Error magnitude: `SP_i - PV_i`
|
||||
* Time since last heated
|
||||
* Configured static priority
|
||||
3. **Predictive Control:** Model the thermal dynamics (C_i, U_i) and predict future PV_i to make more informed decisions about when to start/stop heating, potentially anticipating overshoot or allocating budget more effectively.
|
||||
4. **Duty Cycle Modulation:** If the heating elements support it (unlikely with simple `run`/`stop`), modulate the *power* P_i rather than just on/off state, allowing finer budget control.
|
||||
5. **Adaptive Budget:** Adjust W_budget based on overall system state or external inputs.
|
||||
|
||||
## 8. Required Changes to OmronE5
|
||||
|
||||
* Add a public method `uint32_t getConsumption() const;` to `OmronE5.h`.
|
||||
* Implement `OmronE5::getConsumption()` in `OmronE5.cpp` to return the value of the `_consumption` member variable.
|
||||
|
||||
```cpp
|
||||
// In OmronE5.h
|
||||
public:
|
||||
// ... other methods ...
|
||||
uint32_t getConsumption() const; // Add this
|
||||
|
||||
// In OmronE5.cpp
|
||||
uint32_t OmronE5::getConsumption() const {
|
||||
return _consumption; // Assuming _consumption holds Watts
|
||||
}
|
||||
```
|
||||
68
docs/astro/changelog.mdx
Normal file
@ -0,0 +1,68 @@
|
||||
---
|
||||
title: Change Log
|
||||
image:
|
||||
url: "./main.png"
|
||||
alt: "Cassandra Firmware Update"
|
||||
tags: ["firmware", "update", "release"]
|
||||
---
|
||||
|
||||
import LGallery from "@polymech/astro-base/components/GalleryK.astro";
|
||||
import RelativeImage from "@polymech/astro-base/components/RelativeImage.astro";
|
||||
import RelativePicture from "@polymech/astro-base/components/RelativePicture.astro";
|
||||
import RelativeGallery from "@polymech/astro-base/components/RelativeGallery.astro";
|
||||
import FileTree from "@polymech/astro-base/components/FileTree.astro";
|
||||
|
||||
## v0.6.1 (2025-12-07)
|
||||
|
||||
### New Features
|
||||
|
||||
- **Pressure Profile Editor**:
|
||||
- Launched a dedicated editor for Pressure Profiles w/ Bezier curves.
|
||||
- Optimized for 0-100% pressure scaling (removed temperature fields).
|
||||
- Added "Edit" shortcut button to Pressure Profile rows for quick access.
|
||||
- **Pressure Control**:
|
||||
- Added `+` and `-` buttons for precise Setpoint adjustment.
|
||||
- Hold `Shift` while clicking to adjust by 10 units.
|
||||
- Improved layout for better accessibility.
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Profile Playback**:
|
||||
- Fixed duration display for Pressure Profiles (was showing seconds as ms).
|
||||
- Updated graph axis labels to "Pressure (%)" for clarity.
|
||||
- **Modbus Communication**:
|
||||
- Implemented `PriorityFilter` to dynamically prioritize critical operations.
|
||||
- **Loadcell**: Mandatory read blocks are now flagged as **High Priority** to ensure timely data updates despite bus load.
|
||||
|
||||
## v0.6.0 (2025-12-05)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
### New Features
|
||||
|
||||
- **Timeline Scrubbing**: You can now jump to any point in a profile's timeline using the new slider. This is available in both the Profile Card and the main Playback controls, allowing for quick adjustments to the elapsed time.
|
||||
- **Pressure Cylinder HOLD**: Double-tapping UP on the joystick now triggers a "HOLD" function. This captures the current pressure as the new setpoint and automatically switches to Auto mode to maintain it. This feature can be enabled/disabled via settings.
|
||||
- **Settings Memory**: The machine now remembers your "Interlocked" and "Mode" settings for the Pressure Cylinder even after a restart.
|
||||
- **Pressure Profiles**: There are now 4 instead of 1 pressure profiles.
|
||||
- **Warmup Notification**: A buzzer now sounds when a temperature profile completes its warmup phase and begins running.
|
||||
- **Sound feedback for Temperature Profiles**: The machine now makes a sound when a temperature profile warmup phase is completed.
|
||||
|
||||
### Improvements & Fixes
|
||||
|
||||
- **Pressure Cylinder**:
|
||||
- Unified manual control logic for single and multi-cylinder modes, ensuring consistent overload protection.
|
||||
- Improved stability of mode transitions and loadcell handling.
|
||||
- **Visual Feedback**:
|
||||
- Added a "Disconnected" icon (crossed-out WiFi) to Controller Cards and Pressure Cylinder controls. This appears immediately if the connection is lost, so you know when data is stale.
|
||||
- Added a "Profile" badge to Controller Cards to clearly show which profile is currently active.
|
||||
- Enabled buzzer sounds for key profile events.
|
||||
- **Profile Editor**:
|
||||
- Fixed text readability in the description field when using Dark Mode.
|
||||
- Optimized button layout for better usability on mobile devices.
|
||||
- **Safety**:
|
||||
- Added safeguards to prevent accidental pressure application when no profile is running.
|
||||
- **Fixes**:
|
||||
- Better handling of disconnected devices over Modbus.
|
||||
- Restore IsSlave, ComWrite after reboot.
|
||||
- Restore Omon & Temperature profile enabled state after reboot.
|
||||
- **Profile Timing**: Fixed an issue where the profile timer would start counting during the warmup phase. The timer now correctly starts only after the profile enters the running state.
|
||||
416
docs/casting.md
Normal file
@ -0,0 +1,416 @@
|
||||
# Component Inheritance and Casting Strategy
|
||||
|
||||
## 1. The Problem
|
||||
|
||||
The `Loadcell` component requires functionality from both `RTU_Base` (for RS485 communication) and `NetworkComponent` (for Modbus TCP exposure). Both of these base classes inherit from `Component`.
|
||||
|
||||
This creates a "diamond inheritance" pattern:
|
||||
|
||||
```
|
||||
Component
|
||||
/ \
|
||||
RTU_Base NetworkComponent
|
||||
\ /
|
||||
Loadcell
|
||||
```
|
||||
|
||||
This leads to two critical issues in C++:
|
||||
|
||||
1. **Ambiguous Base Class:** Any code trying to access members of `Component` through a `Loadcell` pointer (e.g., `loadcell->id`) is ambiguous. Does it access `RTU_Base::Component` or `NetworkComponent::Component`?
|
||||
2. **Casting Failures:** The standard solution to ambiguity is `virtual` inheritance. However, if `Component` is a virtual base, downcasting from a `Component*` to a derived class pointer (e.g., `OmronE5*`) using `static_cast` becomes illegal. Since the project does not use RTTI, `dynamic_cast` is not available as an alternative.
|
||||
|
||||
This document outlines the viable and non-viable options to resolve this.
|
||||
|
||||
## 2. Option A: Virtual Inheritance (Non-Viable)
|
||||
|
||||
This is the classic C++ solution for the diamond problem.
|
||||
|
||||
- **Concept:** Have `RTU_Base` and `NetworkComponent` inherit `virtual public Component`. This ensures that `Loadcell` receives only one shared instance of the `Component` base class, resolving the ambiguity.
|
||||
- **Why it Fails:** This approach makes it impossible to safely downcast. Code elsewhere in the project iterates through a list of `Component*` and casts them to specific types like `OmronE5*`.
|
||||
```cpp
|
||||
// This becomes illegal if Component is a virtual base
|
||||
OmronE5* omron = static_cast<OmronE5*>(component_ptr);
|
||||
```
|
||||
The C++ standard forbids `static_cast` for downcasting from a virtual base because the memory layout is not known at compile time. The only standard-compliant tool for this is `dynamic_cast`, which requires RTTI.
|
||||
|
||||
- **Conclusion:** Due to the hard requirement of downcasting `Component` pointers without RTTI, this option is not feasible.
|
||||
|
||||
## 3. Option B: Composition over Inheritance (Recommended)
|
||||
|
||||
This option redesigns the relationship to avoid multiple inheritance of `Component`.
|
||||
|
||||
- **Concept:** Instead of `Loadcell` "being a" `NetworkComponent`, it will "have a" network helper. We will refactor `NetworkComponent` into a helper class that does *not* inherit from `Component`. `Loadcell` will continue to inherit from `RTU_Base` (and thus `Component`), but will contain the new network helper as a member variable.
|
||||
|
||||
- **Implementation Steps:**
|
||||
1. Create a new template class, `NetworkHelper<N>`, from the code in `NetworkComponent`.
|
||||
2. `NetworkHelper` will **not** inherit from `Component`.
|
||||
3. The constructor for `NetworkHelper` will take a `Component* owner` to get access to `id`, `name`, `slaveId`, etc.
|
||||
4. Modify `Loadcell` to have a member variable: `NetworkHelper<6> _netHelper;`.
|
||||
5. The `Loadcell` constructor will initialize `_netHelper`, passing `this` as the owner.
|
||||
6. `Loadcell` will implement network-related virtual functions (like `mb_tcp_blocks`) by forwarding the calls to its `_netHelper` member.
|
||||
7. The `INIT_NETWORK_VALUE` macro must be updated to work with this new structure, or a new macro must be created.
|
||||
|
||||
- **Pros:**
|
||||
* **Solves the Problem:** Completely avoids the diamond inheritance and eliminates all ambiguity and casting issues.
|
||||
* **Clear Relationship:** Arguably, a component "having" network capabilities is a clearer design than it "being" a network interface.
|
||||
* **Safe:** No unsafe casting or compiler-specific tricks are needed.
|
||||
|
||||
- **Cons:**
|
||||
* **Refactoring Effort:** `NetworkComponent` must be refactored. Any other components that currently inherit from it (like `NetworkValueTest`) will also need to be updated to use the new `NetworkHelper` via composition.
|
||||
* **Boilerplate Code:** Components like `Loadcell` will need to explicitly forward calls to their helper member, adding a small amount of boilerplate code.
|
||||
|
||||
## 4. Option C: Component-centric Feature System
|
||||
|
||||
This is a more advanced alternative that moves the "feature" concept from `NetworkValue` directly into the `Component` base class.
|
||||
|
||||
- **Concept:** Instead of creating helper classes or using multiple inheritance, the `Component` class itself would be designed to be composed of optional features. Any component could be configured to include capabilities like `Modbus`, `ValueNotify`, etc.
|
||||
|
||||
- **Implementation Sketch:**
|
||||
1. **Generalize Feature Classes:** The `NV_Modbus`, `NV_Logging`, and `NV_Notify` classes would be refactored into generic, standalone "Feature" classes that do not inherit from `Component`.
|
||||
2. **Adapt `Component`:** The `Component` class would be modified to aggregate these features. This could be done in two ways:
|
||||
* **Compile-Time (Templates):** `Component` could become a variadic template class that inherits from the features it's given. This is powerful but creates a cascade of templates through all inheriting classes (`RTU_Base`, `Loadcell`, etc.), significantly increasing complexity.
|
||||
```cpp
|
||||
template<typename... Features>
|
||||
class Component : public Features... { /* ... */ };
|
||||
|
||||
// Usage would be complex:
|
||||
class Loadcell : public RTU_Base<Feature_Modbus, ...> {};
|
||||
```
|
||||
* **Runtime (Composition):** `Component` could hold pointers to optional feature objects, allocated on the heap. This avoids the template complexity but introduces dynamic memory management.
|
||||
```cpp
|
||||
class Component {
|
||||
std::vector<IFeature*> _features;
|
||||
public:
|
||||
template<typename F, typename... Args>
|
||||
void addFeature(Args&&... args) {
|
||||
_features.push_back(new F(std::forward<Args>(args)...));
|
||||
}
|
||||
};
|
||||
```
|
||||
3. **Update `Loadcell`:** `Loadcell` would inherit from `RTU_Base` as usual. In its constructor, it would call `this->addFeature<Feature_Modbus>(...)` and `this->addFeature<Feature_ValueNotify<uint32_t>>(...)` to gain the required functionality.
|
||||
|
||||
- **Pros:**
|
||||
* **Ultimate Flexibility:** Provides a unified way to add any capability to any component.
|
||||
* **Clean Hierarchy:** Completely avoids all inheritance problems. `Loadcell` has a clear, single inheritance path from `Component` via `RTU_Base`.
|
||||
* **No Boilerplate Forwarding:** The `Component` base class could manage calling `loop()` or `setup()` on all its registered features automatically.
|
||||
|
||||
- **Cons:**
|
||||
* **Highest Refactoring Effort:** This is a fundamental change to the core `Component` class and would require updating every component in the system.
|
||||
* **Architectural Complexity:** Designing a robust and easy-to-use feature system is a significant undertaking. The choice between compile-time templates and runtime pointers has major implications for code complexity and memory usage.
|
||||
|
||||
## 5. Option D: Targeted Static Casting (Minimalist Fix)
|
||||
|
||||
This option resolves the ambiguity only at the specific lines of code where the compiler errors occur, without changing any class definitions.
|
||||
|
||||
- **Concept:** Use `static_cast` to explicitly tell the compiler which inheritance path to take when converting a `Loadcell*` to a `Component*` or when accessing a member of `Component`. We can choose either `RTU_Base` or `NetworkComponent` as the intermediate path. Since `RTU_Base` is the primary functional parent, it's the more logical choice.
|
||||
|
||||
- **Implementation Steps:**
|
||||
1. **Fixing `components.push_back(loadCell_0)`:** In `src/PHApp.cpp`, cast the `loadCell_0` pointer to an `RTU_Base*` before adding it to the vector. This resolves the ambiguity of which `Component` base to use.
|
||||
```cpp
|
||||
// src/PHApp.cpp
|
||||
components.push_back(static_cast<RTU_Base*>(phApp->loadCell_0));
|
||||
```
|
||||
2. **Fixing `loadCell_0->owner = rs485`:** In `src/RS485Devices.cpp`, cast the pointer to `RTU_Base*` before accessing the `owner` member.
|
||||
```cpp
|
||||
// src/RS485Devices.cpp
|
||||
static_cast<RTU_Base*>(phApp->loadCell_0)->owner = rs485;
|
||||
```
|
||||
|
||||
- **Pros:**
|
||||
* **Minimal Change:** Requires modifying only the two lines of code that produce compiler errors. It is by far the least invasive solution.
|
||||
* **No Class Refactoring:** Avoids any changes to `Component`, `RTU_Base`, `NetworkComponent`, or `Loadcell`.
|
||||
* **No Performance Overhead:** `static_cast` is a compile-time operation with zero runtime cost.
|
||||
|
||||
- **Cons:**
|
||||
* **Brittle & Localized:** This is a tactical patch, not a strategic architectural solution. It fixes these two errors but doesn't solve the underlying diamond inheritance problem. If `Loadcell` is used in any other ambiguous context in the future, that new code will also fail to compile and will require a similar explicit cast.
|
||||
* **Technical Debt:** This approach knowingly papers over a design flaw. It can make the code harder to reason about for future developers who will have to understand why this specific component needs special casting.
|
||||
|
||||
## 6. Option E: Virtual Inheritance + Manual RTTI (LLVM-style `dyn_cast`)
|
||||
|
||||
This option revisits virtual inheritance (Option A) and solves its downcasting problem by implementing a manual, type-safe casting function.
|
||||
|
||||
- **Concept:** The `Component` base class already has a `type` member that serves as a manual run-time type identifier. We can use this to create a `dyn_cast` function that checks this type before performing a safe `static_cast`. This avoids the need for the compiler's RTTI and `dynamic_cast`.
|
||||
|
||||
- **Implementation Steps:**
|
||||
1. **Enable Virtual Inheritance:** First, implement Option A. `RTU_Base` and `NetworkComponent` must inherit `virtual public Component`. This solves the diamond inheritance ambiguity.
|
||||
2. **Ensure Type ID is Set:** Every class that inherits from `Component` must set its `type` member correctly in its constructor (e.g., `type = COMPONENT_TYPE::COMPONENT_TYPE_LOADCELL;`).
|
||||
3. **Implement `dyn_cast`:** Create a `dyn_cast` template function that checks the component's type before casting.
|
||||
|
||||
```cpp
|
||||
// in a new component_cast.h
|
||||
template<typename To, typename From>
|
||||
To* dyn_cast(From* src) {
|
||||
using T = std::remove_pointer_t<To>;
|
||||
if (!src) return nullptr;
|
||||
if (src->type == T::COMPONENT_TYPE_ENUM) {
|
||||
return static_cast<To*>(src);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Note: This requires each castable class (e.g., OmronE5)
|
||||
// to expose its type enum, for example:
|
||||
// class OmronE5 : public RTU_Base {
|
||||
// public:
|
||||
// static const COMPONENT_TYPE COMPONENT_TYPE_ENUM = COMPONENT_TYPE::COMPONENT_TYPE_PID;
|
||||
// OmronE5() { type = COMPONENT_TYPE_ENUM; }
|
||||
// ...
|
||||
// };
|
||||
```
|
||||
4. **Replace Casts:** Replace the `static_cast` calls that were failing in `src/PHApp.cpp` with the new `dyn_cast`.
|
||||
```cpp
|
||||
// src/PHApp.cpp
|
||||
if (auto* omron = dyn_cast<OmronE5>(comp)) {
|
||||
omron->setConsumption(...);
|
||||
}
|
||||
```
|
||||
|
||||
- **Pros:**
|
||||
* **"Correct" Inheritance:** Allows for a true "is-a" relationship using standard C++ virtual inheritance, which correctly models the single `Component` base.
|
||||
* **Type Safe:** The `dyn_cast` check prevents incorrect casts at runtime.
|
||||
* **Centralized Logic:** The casting logic is contained within the `dyn_cast` function, not scattered across the application.
|
||||
|
||||
- **Cons:**
|
||||
* **Requires Discipline:** Developers must remember to set the `type` and `COMPONENT_TYPE_ENUM` for every new component.
|
||||
* **Two-Step Refactor:** Requires implementing virtual inheritance *first* and *then* fixing the resulting casting issues. This is more involved than the targeted patch of Option D.
|
||||
* **`slaveId` Ambiguity:** The `slaveId` member exists in both `Component` and `RTU_Base`. Virtual inheritance will not solve this ambiguity; it must be resolved manually (e.g., by removing it from one of the classes or renaming it).
|
||||
|
||||
## 7. Recommendation
|
||||
|
||||
**Option B (Composition over Inheritance) remains the most robust long-term solution.** It correctly models the relationships between the classes ("has-a" vs "is-a") and eliminates the root cause of all ambiguity problems without manual workarounds.
|
||||
|
||||
However, if maintaining the "is-a" relationship with multiple inheritance is a design goal, then **Option E (Virtual Inheritance + `dyn_cast`) is the most architecturally sound way to achieve it.** It is superior to the simple `static_cast` patches of Option D because it correctly solves the underlying single-base-instance problem first, and then provides a safe way to handle the consequences.
|
||||
|
||||
**Updated Recommendation:**
|
||||
- For a quick, low-impact fix that accepts some technical debt: **Choose Option D.**
|
||||
- For the most robust, unambiguous, and maintainable architecture: **Choose Option B.**
|
||||
- For a "correct" multiple inheritance architecture: **Choose Option E.** It is a valid but more involved alternative to Option B.
|
||||
|
||||
## 8. Option G: Refactoring RTU_Base into a Feature Mixin
|
||||
|
||||
This section outlines the strategy for refactoring `RTU_Base` into a `NC_Rtu` feature mixin for `NetworkComponent`. This will create a unified base class for all Modbus components, solving the diamond inheritance problem. The documentation will cover the concept, implementation details, a refactoring example using `OmronE5`, and a thorough analysis of the pros and cons of this advanced architectural approach.
|
||||
|
||||
### 8.1. Concept: A Single, Powerful Base Class
|
||||
|
||||
The core idea is to treat "being a Modbus RTU client" not as a base class to inherit from, but as a *capability* or *feature* that a `NetworkComponent` can possess. This elevates `NetworkComponent` to be the single, unified base for any component that needs to communicate over Modbus, whether it's acting as a TCP server endpoint, an RTU client, or both.
|
||||
|
||||
### 8.2. How It Would Work: The `NC_Rtu` Feature
|
||||
|
||||
We would introduce a new feature class, analogous to `NV_Logging` or `NV_Modbus` from `NetworkValue`, but for `NetworkComponent` itself.
|
||||
|
||||
1. **Create `NC_Rtu`:** A new feature class, `NC_Rtu`, would be created. This class would **not** inherit from `Component`. It would contain the logic currently found in `RTU_Base`, such as:
|
||||
* The `addMandatoryReadBlock()` method.
|
||||
* The logic to queue write operations with the global `ModbusRTU` manager.
|
||||
* State tracking for RTU communication (timeouts, errors, etc.).
|
||||
|
||||
2. **Enhance `NetworkComponent`:** `NetworkComponent` would be modified to optionally inherit from `NC_Rtu` based on a template parameter or feature flag.
|
||||
* It would gain the `onRegisterUpdate(uint16_t address, uint16_t newValue)` virtual function, which would be called by the `NC_Rtu` feature when new data arrives from the bus.
|
||||
* The `NetworkComponent` would manage a pointer to the global `ModbusRTU` instance, passing it to the `NC_Rtu` feature upon initialization.
|
||||
|
||||
### 8.3. Blueprint: Refactoring `OmronE5`
|
||||
|
||||
The `OmronE5` component provides a clear "after" picture for this architecture.
|
||||
|
||||
**Current (Problematic) Structure:**
|
||||
```cpp
|
||||
// Inherits from RTU_Base, which creates a diamond with NetworkComponent
|
||||
class OmronE5 : public RTU_Base { /* ... */ };
|
||||
```
|
||||
|
||||
**Proposed Unified Structure:**
|
||||
```cpp
|
||||
// A template parameter could enable the RTU feature
|
||||
template <size_t N, bool HasRtu = false>
|
||||
class NetworkComponent : public Component, public maybe<HasRtu, NC_Rtu>
|
||||
{ /* ... */ };
|
||||
|
||||
// OmronE5 inherits from ONE base class and declares its needs
|
||||
class OmronE5 : public NetworkComponent<OMRON_TCP_BLOCK_COUNT, /* HasRtu = */ true>
|
||||
{
|
||||
public:
|
||||
OmronE5(Component* owner, uint8_t slaveId, ...)
|
||||
: NetworkComponent(owner, ...)
|
||||
{
|
||||
// Initialize the RTU feature with its slaveId
|
||||
this->initRtuFeature(slaveId);
|
||||
}
|
||||
|
||||
short setup() override {
|
||||
// TCP setup is handled by NetworkComponent's base setup
|
||||
NetworkComponent::setup();
|
||||
|
||||
// Add RTU read blocks. This method now comes from the NC_Rtu mixin.
|
||||
this->addMandatoryReadBlock(0x0000, 6, FN_READ_HOLD_REGISTER);
|
||||
|
||||
// ... other setup ...
|
||||
return E_OK;
|
||||
}
|
||||
|
||||
// This gets called by the NC_Rtu feature when data arrives from the bus
|
||||
void onRegisterUpdate(uint16_t address, uint16_t newValue) override {
|
||||
// Handle RTU register updates here...
|
||||
}
|
||||
|
||||
// TCP methods remain unchanged
|
||||
short mb_tcp_read(MB_Registers* reg) override { /* ... */ }
|
||||
short mb_tcp_write(MB_Registers* reg, short value) override { /* ... */ }
|
||||
uint16_t mb_tcp_base_address() const override { /* ... */ }
|
||||
};
|
||||
```
|
||||
|
||||
### 8.4. Analysis of the Unified Approach
|
||||
|
||||
This model represents a highly integrated and powerful design.
|
||||
|
||||
**Pros:**
|
||||
|
||||
* **Solves Diamond Inheritance:** It completely eliminates the diamond inheritance problem by establishing a single, clear inheritance path for all networked components.
|
||||
* **Reduces Boilerplate:** Components like `OmronE5` and `Loadcell` become much simpler. They no longer need to manually implement forwarding to a helper class; they simply inherit the required functionality from their single `NetworkComponent` base.
|
||||
* **True "Is-A" Relationship:** A component truly "is a" `NetworkComponent`, with all the capabilities that entails, rather than having to delegate to helper objects.
|
||||
|
||||
**Cons:**
|
||||
|
||||
* **"God Class" Risk:** This approach concentrates a significant amount of responsibility into `NetworkComponent`, making it a large and complex class that handles both TCP server logic and RTU client logic. This can make the base class harder to understand and maintain.
|
||||
* **High Refactoring Cost:** This is the most invasive refactoring option. It requires fundamentally redesigning `NetworkComponent` and `RTU_Base`, and would necessitate updating every single component that currently uses either of them.
|
||||
* **Blurred Responsibilities:** It merges the very different concerns of being a passive TCP endpoint and an active RTU client into a single class, which can be seen as a violation of the Single Responsibility Principle.
|
||||
|
||||
### 8.5. Conclusion on Option F
|
||||
|
||||
Option F is an architecturally elegant but pragmatically challenging solution. It offers the cleanest public API for components like `OmronE5` and `Loadcell`, but at the cost of creating a highly complex base class and requiring a system-wide refactoring effort. It should be considered a long-term architectural goal, to be approached with caution after the more immediate problems are solved using the less invasive compositional approach (Option B).
|
||||
|
||||
## 9. Final Architecture: A Unified `NetworkComponent` with Composable Features
|
||||
|
||||
This section details the definitive architectural solution that resolves the diamond inheritance problem by refactoring `NetworkComponent` into a versatile, feature-based class. This approach draws inspiration from the design of `NetworkValue`, using template-based composition to mix in required capabilities like Modbus TCP and RTU, rather than relying on multiple inheritance or helper objects.
|
||||
|
||||
### 9.1. Core Concept: From Inheritance to Feature Composition
|
||||
|
||||
The fundamental shift is to stop treating network protocols as base classes to inherit from. Instead, they become **features** that are composed at compile time into a single, lean `NetworkComponent` base.
|
||||
|
||||
1. **A Lean `NetworkComponent` Base:** The `NetworkComponent` class will be refactored into a variadic template class. It will inherit from `Component` and also from any feature classes passed to it as template arguments.
|
||||
2. **Standalone Feature Classes (`NC_ModbusTcp`, `NC_ModbusRtu`):**
|
||||
* The logic from the current `NetworkComponent` will be extracted into a feature class named `NC_ModbusTcp`.
|
||||
* The logic from `RTU_Base` will be extracted into a feature class named `NC_ModbusRtu`.
|
||||
* Crucially, these feature classes will **not** inherit from `Component`. They are pure feature mixins. They will be given an owner pointer to the `NetworkComponent` instance to access shared data (`id`, `name`, etc.).
|
||||
|
||||
### 9.2. Architecture Diagram
|
||||
|
||||
This diagram illustrates the final proposed architecture. `NetworkComponent` acts as an aggregator, composing the TCP and RTU features. `Loadcell` then inherits this aggregated functionality.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
direction LR
|
||||
Component <|-- NetworkComponent
|
||||
NetworkComponent <|-- Loadcell
|
||||
|
||||
class Component {
|
||||
+id
|
||||
+name
|
||||
+owner
|
||||
+enabled()
|
||||
}
|
||||
|
||||
class NetworkComponent {
|
||||
<<template (T...)>>
|
||||
+mb_tcp_register()
|
||||
+onRegisterUpdate()
|
||||
}
|
||||
|
||||
NetworkComponent --o NC_ModbusTcp : composes
|
||||
NetworkComponent --o NC_ModbusRtu : composes
|
||||
|
||||
class NC_ModbusTcp {
|
||||
<<feature>>
|
||||
#registerBlock()
|
||||
#mb_tcp_read()
|
||||
#mb_tcp_write()
|
||||
}
|
||||
class NC_ModbusRtu {
|
||||
<<feature>>
|
||||
#addMandatoryReadBlock()
|
||||
#onRegisterUpdate()
|
||||
}
|
||||
class Loadcell {
|
||||
+getWeight()
|
||||
}
|
||||
```
|
||||
|
||||
### 9.3. Blueprint: Refactoring `Loadcell`
|
||||
|
||||
This code demonstrates how `Loadcell` would be implemented under the new architecture.
|
||||
|
||||
1. **Refactored `NetworkComponent` Header (Conceptual):**
|
||||
```cpp
|
||||
// In a new file, e.g., features/NC_ModbusTcp.h
|
||||
class NC_ModbusTcp { /* ... TCP logic ... */ };
|
||||
|
||||
// In a new file, e.g., features/NC_ModbusRtu.h
|
||||
class NC_ModbusRtu { /* ... RTU logic ... */ };
|
||||
|
||||
// The new NetworkComponent.h
|
||||
#include "features/NC_ModbusTcp.h"
|
||||
#include "features/NC_ModbusRtu.h"
|
||||
|
||||
template <typename... Features>
|
||||
class NetworkComponent : public Component, public Features... {
|
||||
public:
|
||||
NetworkComponent(Component* owner, ...) : Component(owner, ...) {
|
||||
// Pass 'this' pointer to each feature
|
||||
(Features::initFeature(this), ...);
|
||||
}
|
||||
// ... common network component logic ...
|
||||
};
|
||||
```
|
||||
|
||||
2. **`Loadcell` Implementation:**
|
||||
The `Loadcell` class becomes significantly cleaner. It declares its needs through the template arguments of its base class.
|
||||
|
||||
```cpp
|
||||
// In Loadcell.h
|
||||
#include "modbus/NetworkComponent.h"
|
||||
#include "features/NC_ModbusTcp.h"
|
||||
#include "features/NC_ModbusRtu.h"
|
||||
|
||||
class Loadcell : public NetworkComponent<NC_ModbusTcp, NC_ModbusRtu>
|
||||
{
|
||||
public:
|
||||
Loadcell(Component* owner, uint8_t slaveId, ...);
|
||||
|
||||
short setup() override;
|
||||
short loop() override;
|
||||
|
||||
// This is an RTU-specific callback, now part of the class interface
|
||||
void onRegisterUpdate(uint16_t address, uint16_t newValue) override;
|
||||
|
||||
// These are TCP-specific methods, also part of the class interface
|
||||
short mb_tcp_read(MB_Registers* reg) override;
|
||||
short mb_tcp_write(MB_Registers* reg, short value) override;
|
||||
uint16_t mb_tcp_base_address() const override;
|
||||
|
||||
// ... other Loadcell methods
|
||||
};
|
||||
|
||||
// In Loadcell.cpp
|
||||
Loadcell::Loadcell(Component* owner, uint8_t slaveId, ...)
|
||||
: NetworkComponent(owner, ...) // Base class constructor
|
||||
{
|
||||
// Feature constructors (if any) are handled by NetworkComponent
|
||||
this->setRtuSlaveId(slaveId); // Method inherited from NC_ModbusRtu feature
|
||||
}
|
||||
|
||||
short Loadcell::setup() {
|
||||
NetworkComponent::setup(); // Calls setup on all features
|
||||
|
||||
// RTU setup
|
||||
addMandatoryReadBlock(...);
|
||||
|
||||
// TCP setup
|
||||
registerBlock(...);
|
||||
|
||||
return E_OK;
|
||||
}
|
||||
```
|
||||
|
||||
### 9.4. Final Recommendation
|
||||
|
||||
This unified, feature-based composition model is the recommended architectural direction. It is the most robust, flexible, and scalable solution, completely eliminating the diamond inheritance problem while providing a clean and expressive API for component development. While it represents a significant refactoring effort, the long-term benefits to code clarity, maintainability, and extensibility are substantial.
|
||||
|
||||
0
docs/checklist.md
Normal file
164
docs/classes.drawio
Normal file
@ -0,0 +1,164 @@
|
||||
<mxfile host="app.diagrams.net" modified="2024-07-30T12:00:00.000Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" etag="unique_etag" version="24.4.9" type="device">
|
||||
<diagram name="Class Diagram" id="diagram_id">
|
||||
<mxGraphModel dx="1434" dy="780" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
|
||||
<!-- Classes -->
|
||||
<mxCell id="App_Class" value="App
<i>(from polymech-base)</i>" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;" vertex="1" parent="1">
|
||||
<mxGeometry x="350" y="40" width="180" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="Component_Class" value="Component
<i>(from polymech-base)</i>
+id: short
+owner: Component*
+setup() : short
+loop() : short
+writeNetworkValue(...) : short
+readNetworkValue(...) : short
+registerModbusBlocks(...)" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;" vertex="1" parent="1">
|
||||
<mxGeometry x="590" y="160" width="220" height="150" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="PHApp_Class" value="PHApp
-modbusManager: ModbusTCP*
-modbusRTU: ModbusRTU*
-rtuProxy: RTUProxyComponent*
-e5Proxy: E5Proxy*
-components: vector&lt;Component*&gt;
+setup() : short
+loop() : short" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;" vertex="1" parent="1">
|
||||
<mxGeometry x="300" y="160" width="240" height="150" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="ModbusRTU_Class" value="ModbusRTU
+begin(...) : MB_Error
+process() : MB_Error
+writeRegister(...) : MB_Error
+readRegister(...) : MB_Error
+getRegisterValue(...) : bool
+writeCoil(...) : MB_Error
+getCoilValue(...) : bool" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="400" width="200" height="170" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="ModbusTCP_Class" value="ModbusTCP
-modbusServer: ModbusServerTCPasync*
-addressMappings: Vector&lt;MB_Registers&gt;
+setup() : short
+registerModbus(...) : bool
+findComponentForAddress(...) : Component*" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;" vertex="1" parent="1">
|
||||
<mxGeometry x="590" y="400" width="220" height="130" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="RTUProxyComponent_Class" value="RTUProxyComponent
-modbusRTU: ModbusRTU*
-mappings: Vector&lt;RTUMapping&gt;
+addMapping(...) : bool
+writeNetworkValue(...) : short
+readNetworkValue(...) : short
+registerModbusBlocks(...)" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;" vertex="1" parent="1">
|
||||
<mxGeometry x="300" y="400" width="240" height="150" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="E5Proxy_Class" value="E5Proxy
-rtuProxy: RTUProxyComponent*
+setup() : short" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;" vertex="1" parent="1">
|
||||
<mxGeometry x="300" y="620" width="240" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="Relay_Class" value="Relay" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;" vertex="1" parent="1">
|
||||
<mxGeometry x="700" y="620" width="110" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Relationships -->
|
||||
<mxCell id="Rel_PHApp_Inherit_App" value="" style="endArrow=block;endFill=0;html=1;edgeStyle=orthogonalEdgeStyle;align=center;verticalAlign=bottom;rounded=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="PHApp_Class" target="App_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="420" y="160" as="sourcePoint"/>
|
||||
<mxPoint x="420" y="100" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_ModbusTCP_Inherit_Component" value="" style="endArrow=block;endFill=0;html=1;edgeStyle=orthogonalEdgeStyle;align=center;verticalAlign=bottom;rounded=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="ModbusTCP_Class" target="Component_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="700" y="400" as="sourcePoint"/>
|
||||
<mxPoint x="700" y="310" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_RTUProxy_Inherit_Component" value="" style="endArrow=block;endFill=0;html=1;edgeStyle=orthogonalEdgeStyle;align=center;verticalAlign=bottom;rounded=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="RTUProxyComponent_Class" target="Component_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="420" y="400" as="sourcePoint"/>
|
||||
<mxPoint x="590" y="235" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="420" y="340"/>
|
||||
<mxPoint x="570" y="340"/>
|
||||
<mxPoint x="570" y="235"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_E5Proxy_Inherit_Component" value="" style="endArrow=block;endFill=0;html=1;edgeStyle=orthogonalEdgeStyle;align=center;verticalAlign=bottom;rounded=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="E5Proxy_Class" target="Component_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="420" y="620" as="sourcePoint"/>
|
||||
<mxPoint x="645" y="310" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="420" y="590"/>
|
||||
<mxPoint x="645" y="590"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_Relay_Inherit_Component" value="" style="endArrow=block;endFill=0;html=1;edgeStyle=orthogonalEdgeStyle;align=center;verticalAlign=bottom;rounded=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="Relay_Class" target="Component_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="755" y="620" as="sourcePoint"/>
|
||||
<mxPoint x="755" y="310" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="Rel_PHApp_Owns_ModbusTCP" value="" style="endArrow=none;html=1;edgeStyle=entityRelationEdgeStyle;startArrow=diamondThin;startFill=0;sourcePerimeterSpacing=0;startSize=12;endFill=0;" edge="1" parent="1" source="PHApp_Class" target="ModbusTCP_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="540" y="235" as="sourcePoint"/>
|
||||
<mxPoint x="590" y="465" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="570" y="235"/>
|
||||
<mxPoint x="570" y="465"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_PHApp_Owns_ModbusRTU" value="" style="endArrow=none;html=1;edgeStyle=entityRelationEdgeStyle;startArrow=diamondThin;startFill=0;sourcePerimeterSpacing=0;startSize=12;endFill=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="PHApp_Class" target="ModbusRTU_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="300" y="235" as="sourcePoint"/>
|
||||
<mxPoint x="40" y="485" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="270" y="235"/>
|
||||
<mxPoint x="270" y="485"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_PHApp_Owns_RTUProxy" value="" style="endArrow=none;html=1;edgeStyle=entityRelationEdgeStyle;startArrow=diamondThin;startFill=0;sourcePerimeterSpacing=0;startSize=12;endFill=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="PHApp_Class" target="RTUProxyComponent_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="420" y="310" as="sourcePoint"/>
|
||||
<mxPoint x="420" y="400" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_PHApp_Owns_E5Proxy" value="" style="endArrow=none;html=1;edgeStyle=entityRelationEdgeStyle;startArrow=diamondThin;startFill=0;sourcePerimeterSpacing=0;startSize=12;endFill=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="PHApp_Class" target="E5Proxy_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="360" y="310" as="sourcePoint"/>
|
||||
<mxPoint x="360" y="620" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="360" y="340"/>
|
||||
<mxPoint x="300" y="340"/>
|
||||
<mxPoint x="300" y="660"/>
|
||||
<mxPoint x="300" y="660"/>
|
||||
<mxPoint x="420" y="620"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_PHApp_Owns_Relay" value="" style="endArrow=none;html=1;edgeStyle=entityRelationEdgeStyle;startArrow=diamondThin;startFill=0;sourcePerimeterSpacing=0;startSize=12;endFill=0;" edge="1" parent="1" source="PHApp_Class" target="Relay_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="540" y="235" as="sourcePoint"/>
|
||||
<mxPoint x="755" y="620" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="570" y="235"/>
|
||||
<mxPoint x="570" y="645"/>
|
||||
<mxPoint x="700" y="645"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="Rel_RTUProxy_Uses_ModbusRTU" value="uses" style="endArrow=open;html=1;align=center;verticalAlign=middle;dashed=1;endFill=0;" edge="1" parent="1" source="RTUProxyComponent_Class" target="ModbusRTU_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="300" y="475" as="sourcePoint"/>
|
||||
<mxPoint x="240" y="475" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_E5Proxy_Uses_RTUProxy" value="configures" style="endArrow=open;html=1;align=center;verticalAlign=middle;dashed=1;endFill=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="E5Proxy_Class" target="RTUProxyComponent_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="420" y="620" as="sourcePoint"/>
|
||||
<mxPoint x="420" y="550" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_ModbusTCP_Uses_Component" value="calls read/write
finds component" style="endArrow=open;html=1;align=center;verticalAlign=bottom;dashed=1;endFill=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="ModbusTCP_Class" target="Component_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="700" y="400" as="sourcePoint"/>
|
||||
<mxPoint x="810" y="235" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="700" y="340"/>
|
||||
<mxPoint x="840" y="340"/>
|
||||
<mxPoint x="840" y="235"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_Component_Uses_ModbusTCP" value="calls registerModbusBlocks" style="endArrow=open;html=1;align=center;verticalAlign=top;dashed=1;endFill=0;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="Component_Class" target="ModbusTCP_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="810" y="273" as="sourcePoint"/>
|
||||
<mxPoint x="700" y="400" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="840" y="273"/>
|
||||
<mxPoint x="840" y="550"/>
|
||||
<mxPoint x="700" y="550"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
168
docs/components.md
Normal file
@ -0,0 +1,168 @@
|
||||
# Component System Documentation
|
||||
|
||||
This document describes the component-based architecture used in the firmware.
|
||||
|
||||
## Overview
|
||||
|
||||
The firmware utilizes a modular design where different functionalities (hardware interfaces, communication managers, etc.) are implemented as separate "Components". This promotes code reusability, organization, and easier maintenance.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
1. **`Component` Base Class (`src/Component.h`):**
|
||||
* All functional units inherit from the base `Component` class.
|
||||
* Provides common properties like `name`, `id` (unique identifier from `COMPONENT_KEY` enum in `src/enums.h`), `owner` (pointer to the parent component, usually `PHApp`), `flags` (for controlling behavior like setup/loop execution), and `nFlags` (for network capabilities).
|
||||
* Defines standard virtual methods that can be overridden by derived classes:
|
||||
* `setup()`: Called once during application initialization.
|
||||
* `loop()`: Called repeatedly in the main application loop.
|
||||
* `info()`: Used for printing component status/information (often called via serial command).
|
||||
* `debug()`: Used for printing debugging information.
|
||||
* `onRegisterMethods(Bridge* bridge)`: Used to register methods that can be called via the `Bridge` (e.g., through serial commands).
|
||||
* `readNetworkValue(short address)` / `writeNetworkValue(short address, short value)`: Interface for network managers (like `ModbusManager`) to interact with the component's data.
|
||||
* `notifyStateChange()`: Intended to be called by components when their state changes, although currently not actively used by managers.
|
||||
|
||||
2. **`App` / `PHApp` (`src/App.h`, `src/PHApp.h`):**
|
||||
* `App` is the base application class, inheriting from `Component`.
|
||||
* `PHApp` is the specific application implementation for this project.
|
||||
* `PHApp` acts as the central orchestrator and owner of most components.
|
||||
* It maintains a `Vector<Component*> components` list to hold pointers to all active components.
|
||||
* **Initialization (`PHApp::setup()`):**
|
||||
* Instantiates necessary components (like `Bridge`, `SerialMessage`, `ModbusManager`, `Relay`, etc.).
|
||||
* Adds component pointers to the `components` vector using `components.push_back()`. **Crucially, `components.setStorage(componentsArray);` must be called in the `App` constructor (`App.cpp`) for the vector to function correctly.**
|
||||
* Calls `App::setup()`, which iterates through the `components` vector and calls the `setup()` method of each component (if the `E_OF_SETUP` flag is set).
|
||||
* Registers components with managers (e.g., `modbusManager->registerComponentAddress(...)`).
|
||||
* Registers component methods with the `Bridge` (`registerComponents(bridge)`).
|
||||
* **Main Loop (`PHApp::loop()`):**
|
||||
* Calls `App::loop()`, which iterates through the `components` vector and calls the `loop()` method of each component (if the `E_OF_LOOP` flag is set).
|
||||
|
||||
3. **Configuration (`src/config.h`, `src/features.h`, `src/config_secrets.h`, `src/config-modbus.h`):**
|
||||
* Hardware pin assignments (e.g., `MB_RELAY_0`, `STATUS_WARNING_PIN`) are typically defined in `config.h`.
|
||||
* Feature flags (e.g., `ENABLE_MODBUS_TCP`, `HAS_STATUS`) used for conditional compilation are often in `features.h` or `config_adv.h`.
|
||||
* Sensitive information like WiFi credentials should be in `config_secrets.h` (which should not be committed to version control).
|
||||
* Modbus addresses are centralized in `config-modbus.h`.
|
||||
* Component IDs are defined in `src/enums.h`.
|
||||
|
||||
## Adding a New Component
|
||||
|
||||
Let's illustrate with a hypothetical example: adding a simple Temperature Sensor component.
|
||||
|
||||
1. **Define Configuration:**
|
||||
* Add pin definition to `config.h`: `#define TEMP_SENSOR_PIN 34`
|
||||
* Add component ID to `enums.h` (`COMPONENT_KEY` enum): `COMPONENT_KEY_TEMP_SENSOR_0 = 800,`
|
||||
* (Optional) Add Modbus address to `config-modbus.h`: `#define MB_IREG_TEMP_SENSOR_0 800`
|
||||
* (Optional) Add feature flag to `features.h`: `#define HAS_TEMP_SENSOR_0`
|
||||
|
||||
2. **Create Component Class (`src/TempSensor.h`):**
|
||||
```cpp
|
||||
#ifndef TEMP_SENSOR_H
|
||||
#define TEMP_SENSOR_H
|
||||
|
||||
#include "Component.h"
|
||||
#include "enums.h"
|
||||
#include <ArduinoLog.h>
|
||||
// Include any specific sensor libraries if needed
|
||||
|
||||
class Bridge;
|
||||
|
||||
class TempSensor : public Component {
|
||||
private:
|
||||
const short pin;
|
||||
float currentValue;
|
||||
short modbusAddress;
|
||||
|
||||
public:
|
||||
TempSensor(Component* owner, short _pin, short _id, short _modbusAddr)
|
||||
: Component("TempSensor", _id, COMPONENT_DEFAULT, owner),
|
||||
pin(_pin),
|
||||
currentValue(0.0),
|
||||
modbusAddress(_modbusAddr)
|
||||
{
|
||||
// Enable Modbus capability if needed
|
||||
setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS);
|
||||
// Ensure loop() is called
|
||||
// setFlag(OBJECT_RUN_FLAGS::E_OF_LOOP); // Already in COMPONENT_DEFAULT
|
||||
}
|
||||
|
||||
short setup() override {
|
||||
Component::setup();
|
||||
// Initialize sensor library, set pin mode, etc.
|
||||
// pinMode(pin, INPUT); // Example
|
||||
Log.verboseln("TempSensor::setup - ID: %d, Pin: %d, Modbus Addr: %d", id, pin, modbusAddress);
|
||||
return E_OK;
|
||||
}
|
||||
|
||||
short loop() override {
|
||||
Component::loop();
|
||||
// Read sensor value periodically
|
||||
// currentValue = analogRead(pin); // Simplified example
|
||||
// Add proper timing logic (e.g., read every second)
|
||||
return E_OK;
|
||||
}
|
||||
|
||||
short info(short arg1 = 0, short arg2 = 0) override {
|
||||
Log.verboseln("TempSensor::info - ID: %d, Pin: %d, Value: %F, Modbus Addr: %d", id, pin, currentValue, modbusAddress);
|
||||
return E_OK;
|
||||
}
|
||||
|
||||
// --- Network Interface ---
|
||||
short readNetworkValue(short address) override {
|
||||
if (address == modbusAddress) {
|
||||
// Convert float to Modbus register format (e.g., integer degrees * 10)
|
||||
return (short)(currentValue * 10.0);
|
||||
}
|
||||
return 0; // Not handled
|
||||
}
|
||||
|
||||
// Optional: Implement writeNetworkValue if temperature could be set (e.g., for simulation)
|
||||
// virtual short writeNetworkValue(short address, short value) override { ... }
|
||||
|
||||
// Register methods for serial commands
|
||||
short onRegisterMethods(Bridge* bridge) override {
|
||||
Component::onRegisterMethods(bridge);
|
||||
bridge->registerMemberFunction(id, this, C_STR("info"), (ComponentFnPtr)&TempSensor::info);
|
||||
// Add other methods if needed
|
||||
return E_OK;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // TEMP_SENSOR_H
|
||||
```
|
||||
|
||||
3. **Instantiate and Register in `PHApp`:**
|
||||
* **`PHApp.h`:**
|
||||
* Include `TempSensor.h` (likely within `#ifdef HAS_TEMP_SENSOR_0`).
|
||||
* Declare a pointer: `TempSensor* tempSensor_0;`.
|
||||
* **`PHApp.cpp` (`PHApp::setup()`):**
|
||||
```cpp
|
||||
// ... other includes ...
|
||||
#ifdef HAS_TEMP_SENSOR_0
|
||||
#include "TempSensor.h"
|
||||
#endif
|
||||
|
||||
// ... inside PHApp::setup() ...
|
||||
|
||||
#ifdef HAS_TEMP_SENSOR_0
|
||||
tempSensor_0 = new TempSensor(this,
|
||||
TEMP_SENSOR_PIN,
|
||||
COMPONENT_KEY_TEMP_SENSOR_0,
|
||||
MB_IREG_TEMP_SENSOR_0);
|
||||
components.push_back(tempSensor_0);
|
||||
#else
|
||||
tempSensor_0 = nullptr;
|
||||
#endif
|
||||
|
||||
// ... later, after App::setup() ...
|
||||
|
||||
// --- Register Components with Modbus Manager ---
|
||||
#if defined(ENABLE_MODBUS_TCP)
|
||||
if (modbusManager) {
|
||||
// ... other registrations ...
|
||||
#ifdef HAS_TEMP_SENSOR_0
|
||||
if (tempSensor_0) modbusManager->registerComponentAddress(tempSensor_0, MB_IREG_TEMP_SENSOR_0, 1);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
// ... registerComponents(bridge) will handle calling TempSensor::onRegisterMethods ...
|
||||
```
|
||||
|
||||
4. **Build and Test:** Recompile (`npm run build`), upload (`npm run upload`), and test using serial commands (`npm run send -- "<<800;2;64;info:0:0>>"`) and Modbus tools (`python scripts/modbus_read_registers.py --address 800 --ip-address <IP>`).
|
||||
212
docs/di.md
Normal file
@ -0,0 +1,212 @@
|
||||
# Dependency Injection in Polymech Firmware
|
||||
|
||||
This document outlines dependency injection (DI) patterns applicable to the Polymech firmware, with a focus on lightweight ("cheap") techniques suitable for an embedded C++ environment.
|
||||
|
||||
## 1. What is Dependency Injection?
|
||||
|
||||
Dependency Injection is a design pattern where a component (an object) receives its dependencies from an external source rather than creating them internally. This promotes loose coupling, making the system more modular, easier to test, and more maintainable.
|
||||
|
||||
In an embedded context, DI techniques must be "cheap" in terms of:
|
||||
- **CPU Overhead**: The mechanism should not consume significant processing time.
|
||||
- **Memory Footprint**: The solution should not require large amounts of RAM or flash.
|
||||
- **Cognitive Overhead**: The pattern should be simple to understand and use.
|
||||
|
||||
## 2. Current Approach: Manual Constructor Injection
|
||||
|
||||
The firmware currently uses **Manual Constructor Injection**. The central application class (e.g., `PHApp`) instantiates components and their dependencies, and then "injects" the dependencies into the components by passing them as pointers to the constructor.
|
||||
|
||||
### Example: `Plunger` Component
|
||||
|
||||
The instantiation of the `Plunger` component in `src/PHApp.cpp` illustrates this pattern perfectly:
|
||||
|
||||
```cpp
|
||||
// In PHApp, dependencies are created first:
|
||||
vfd_0 = new SAKO_VFD(this, MB_SAKO_VFD_SLAVE_ID);
|
||||
joystick_0 = new Joystick(this, PIN_JOYSTICK_UP, ...);
|
||||
pot_0 = new POT(this, MB_ANALOG_0);
|
||||
pot_1 = new POT(this, MB_ANALOG_1);
|
||||
|
||||
// Then, the Plunger is created with its dependencies injected via the constructor:
|
||||
plunger_0 = new Plunger(this, vfd_0, joystick_0, pot_0, pot_1);
|
||||
```
|
||||
|
||||
The `Plunger` class constructor receives and stores pointers to the objects it depends on:
|
||||
|
||||
```cpp
|
||||
// lib/polymech-base/src/components/Plunger.h
|
||||
class Plunger : public Component {
|
||||
public:
|
||||
Plunger(Component* owner, SAKO_VFD* vfd, Joystick* joystick, POT* speedPot, POT* torquePot);
|
||||
// ...
|
||||
private:
|
||||
SAKO_VFD* _vfd;
|
||||
Joystick* _joystick;
|
||||
POT* _speedPot;
|
||||
POT* _torquePot;
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
### Advantages
|
||||
|
||||
- **Explicit Dependencies**: A component's dependencies are clear from its constructor signature.
|
||||
- **Compile-Time Safety**: Missing dependencies will result in a compile-time error.
|
||||
- **Zero Runtime Overhead**: It's just a function call. No lookups or dynamic resolutions are needed.
|
||||
- **Simplicity**: The pattern is easy to understand and implement without any special libraries.
|
||||
|
||||
### Disadvantages
|
||||
|
||||
- **Boilerplate**: The main application class can become large and cluttered with object creation logic as the application grows.
|
||||
- **Initialization Order**: The developer must manually manage the correct order of object creation.
|
||||
|
||||
## 3. Alternative "Cheap" DI Options
|
||||
|
||||
While the current approach is effective, here are some alternative patterns that can be considered.
|
||||
|
||||
### Option 1: Factory Pattern
|
||||
|
||||
This pattern encapsulates the creation logic for a set of related objects into a separate `Factory` class. The main application asks the factory for a component, and the factory handles creating the component and its dependencies.
|
||||
|
||||
#### Example
|
||||
|
||||
```cpp
|
||||
class ComponentFactory {
|
||||
public:
|
||||
ComponentFactory(App* owner) : _owner(owner) {}
|
||||
|
||||
Plunger* createPlunger() {
|
||||
SAKO_VFD* vfd = new SAKO_VFD(_owner, MB_SAKO_VFD_SLAVE_ID);
|
||||
Joystick* joystick = new Joystick(_owner, ...);
|
||||
// ... create other dependencies
|
||||
return new Plunger(_owner, vfd, joystick, ...);
|
||||
}
|
||||
private:
|
||||
App* _owner;
|
||||
};
|
||||
|
||||
// In main app:
|
||||
auto factory = new ComponentFactory(this);
|
||||
plunger_0 = factory->createPlunger();
|
||||
```
|
||||
|
||||
- **Pros**: Cleans up the main application logic, centralizes creation logic.
|
||||
- **Cons**: Adds another layer of abstraction and more classes to maintain.
|
||||
|
||||
### Option 2: Service Locator
|
||||
|
||||
A Service Locator is a central registry where dependencies ("services") are registered and can be requested by components.
|
||||
|
||||
#### Example
|
||||
|
||||
```cpp
|
||||
// A simple Service Locator
|
||||
class ServiceLocator {
|
||||
public:
|
||||
static SAKO_VFD* getVFD() { return _vfd; }
|
||||
static void registerVFD(SAKO_VFD* vfd) { _vfd = vfd; }
|
||||
private:
|
||||
static SAKO_VFD* _vfd;
|
||||
};
|
||||
|
||||
// In main app:
|
||||
vfd_0 = new SAKO_VFD(...);
|
||||
ServiceLocator::registerVFD(vfd_0);
|
||||
|
||||
// In a component's constructor or setup:
|
||||
_vfd = ServiceLocator::getVFD();
|
||||
```
|
||||
|
||||
- **Pros**: Can simplify constructors, decouples components from the main app.
|
||||
- **Cons**: **(Anti-Pattern Warning)** Hides dependencies, making the code harder to understand and test. It's essentially a glorified global variable, which can lead to maintenance issues. It also introduces runtime lookup overhead.
|
||||
|
||||
### Option 3: Compile-Time DI with Templates
|
||||
|
||||
This advanced technique uses C++ templates to inject dependencies at compile-time. The dependency is a type parameter, not an object instance passed to the constructor.
|
||||
|
||||
#### Example
|
||||
|
||||
```cpp
|
||||
template<typename VfdType, typename JoystickType>
|
||||
class Plunger : public Component {
|
||||
public:
|
||||
Plunger(Component* owner) {
|
||||
// Here we assume VfdType and JoystickType can be instantiated
|
||||
// or accessed somehow. This is a very simplified example.
|
||||
}
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
- **Pros**: Maximum performance (zero runtime overhead), strong type safety.
|
||||
- **Cons**: Can be complex to implement and lead to verbose compiler errors ("template vomit"). Can increase compile times.
|
||||
|
||||
## 4. Recommendation
|
||||
|
||||
The current **Manual Constructor Injection** pattern is robust, simple, and highly efficient. It is well-suited for the project and should remain the primary DI pattern.
|
||||
|
||||
- **Stick with Manual Constructor Injection** for its clarity and performance.
|
||||
- Consider using the **Factory Pattern** if the object creation logic in `PHApp` becomes overly complex and difficult to manage. This can help organize the setup phase without adding significant runtime overhead.
|
||||
- **Avoid the Service Locator** pattern. While it may seem convenient, it obscures dependencies and can lead to less maintainable code in the long run.
|
||||
- **Template-based DI** is a powerful but complex option. It could be considered for highly performance-critical sections where every CPU cycle counts, but it's likely overkill for general component wiring.
|
||||
|
||||
## 5. Compile-Time DI via Feature Flags
|
||||
|
||||
In addition to runtime object injection, the firmware uses a powerful form of compile-time dependency management using preprocessor feature flags.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Configuration (`src/config.h`)**: The central `config.h` file contains a series of `#define` statements that act as feature flags (e.g., `#define ENABLE_PLUNGER`).
|
||||
2. **Conditional Compilation**: Throughout the codebase, these flags are used in `#ifdef` blocks to conditionally compile code. This includes object instantiation, method calls, and even member variable declarations. If a flag is not defined, the corresponding code is completely excluded from the final binary.
|
||||
3. **Dependency Validation (`src/config-validate.h`)**: A validation header checks for logical inconsistencies in the configuration at compile time. For example, it ensures that if `ENABLE_PLUNGER` is active, its own dependencies like `ENABLE_SAKO_VFD` are also active, throwing a compiler error if they are not.
|
||||
|
||||
This mechanism ensures that only the necessary components are compiled into the firmware, which is a highly effective way to manage dependencies and resource usage on an embedded system.
|
||||
|
||||
### The `menu.tsx` Configuration Tool
|
||||
|
||||
To simplify the management of these feature flags, the project includes a command-line tool located at `cli-ts/src/commands/menu.tsx`.
|
||||
|
||||
- **Functionality**: This tool provides a terminal-based interactive menu for editing `src/config.h`. It parses the header file to find feature flags and their associated settings.
|
||||
- **Technology**: It uses the `tree-sitter-cpp` library to accurately parse the C++ header file syntax and identify `#define` directives, including those that are commented out.
|
||||
- **Role in DI**: The `menu.tsx` script acts as a user-friendly interface for the compile-time DI system. It allows a developer to easily enable or disable high-level features.
|
||||
|
||||
It is important to note that **the `menu.tsx` script is a configuration editor, not a dependency resolver**. It does not automatically enable `ENABLE_SAKO_VFD` when you enable `ENABLE_PLUNGER`. The developer is still responsible for managing these dependencies, with `config-validate.h` serving as the safety net during the build process.
|
||||
|
||||
## 6. Potential Enhancement: Automated Dependency Analysis
|
||||
|
||||
The `menu.tsx` script could be evolved from a simple configuration editor into a dependency-aware provisioning tool. By leveraging `tree-sitter-cpp` more deeply, it could automatically analyze and resolve dependencies between components.
|
||||
|
||||
### Conceptual Workflow
|
||||
|
||||
1. **Build a Component Map**: The script would first parse `src/features.h` to build a comprehensive map linking feature flags (e.g., `ENABLE_PLUNGER`) to the header files they include (e.g., `components/Plunger.h`) and the primary class names defined within them (e.g., `Plunger`).
|
||||
|
||||
2. **Analyze Component Dependencies**: When a feature is selected in the menu, the script would parse the corresponding header file. It would traverse the Abstract Syntax Tree to identify dependency clues, such as:
|
||||
- The types of pointers passed into the class constructor (`SAKO_VFD* vfd`).
|
||||
- The types of member variables.
|
||||
- Local `#include` directives.
|
||||
|
||||
3. **Resolve and Report**: The script would then cross-reference these discovered dependency class names (e.g., `SAKO_VFD`) with the component map from step 1 to find their associated feature flags (`ENABLE_SAKO_VFD`).
|
||||
|
||||
This would allow the tool to generate a dependency object for any given component. For example, analyzing `Plunger` could produce:
|
||||
|
||||
```json
|
||||
{
|
||||
"feature": "ENABLE_PLUNGER",
|
||||
"header": "src/components/Plunger.h",
|
||||
"dependencies": [
|
||||
{
|
||||
"className": "SAKO_VFD",
|
||||
"feature": "ENABLE_SAKO_VFD",
|
||||
"header": "src/components/SAKO_VFD.h"
|
||||
},
|
||||
{
|
||||
"className": "Joystick",
|
||||
"feature": "ENABLE_JOYSTICK",
|
||||
"header": "src/components/Joystick.h"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Benefits
|
||||
|
||||
With this capability, the `menu.tsx` script could proactively assist the developer. For instance, upon enabling `ENABLE_PLUNGER`, it could automatically detect that `ENABLE_SAKO_VFD` is also required and either enable it automatically or prompt the user to do so. This would prevent many `config-validate.h` errors before the compilation stage, streamlining the development process significantly.
|
||||
230
docs/diagrams/classes-all.drawio
Normal file
@ -0,0 +1,230 @@
|
||||
<mxfile host="app.diagrams.net" modified="2024-07-30T12:10:00.000Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" etag="yet_another_unique_etag" version="24.4.9" type="device">
|
||||
<diagram name="Detailed Class Diagram" id="detailed_diagram_id">
|
||||
<mxGraphModel dx="1600" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1100" pageHeight="1700" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
|
||||
<!-- Core App/Component Classes -->
|
||||
<mxCell id="App_Class" value="App
<i>(from polymech-base)</i>" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;" vertex="1" parent="1">
|
||||
<mxGeometry x="560" y="40" width="180" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="Component_Class" value="Component
<i>(from polymech-base)</i>
+id: short
+owner: Component*
+name: const char*
#nFlags: byte" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;" vertex="1" parent="1">
|
||||
<mxGeometry x="840" y="130" width="220" height="90" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="PHApp_Class" value="PHApp
-components: vector&lt;Component*&gt;
-com_serial: SerialMessage*
-modbusManager: ModbusTCP*
-webServer: RESTServer*
-modbusRTU: ModbusRTU*
-rtuProxy: RTUProxyComponent*
-e5Proxy: E5Proxy*
... (other component pointers) ..." style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;" vertex="1" parent="1">
|
||||
<mxGeometry x="510" y="130" width="240" height="160" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Modbus TCP Related -->
|
||||
<mxCell id="ModbusTCP_Class" value="ModbusTCP
-modbusServer: ModbusServerTCPasync*
-addressMappings: Vector&lt;MB_Registers&gt;
-mappingStorage: MB_Registers[]" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;" vertex="1" parent="1">
|
||||
<mxGeometry x="840" y="330" width="220" height="100" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="MB_Registers_Struct" value="MB_Registers
+startAddress: short
+count: short
+type: E_FN_CODE
+access: E_ModbusAccess
+componentId: short" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;componentStyle=struct;" vertex="1" parent="1">
|
||||
<mxGeometry x="840" y="480" width="220" height="120" as="geometry"/>
|
||||
</mxCell>
|
||||
|
||||
<!-- Modbus RTU Client Related -->
|
||||
<mxCell id="ModbusRTU_Class" value="ModbusRTU
-client: ModbusClientRTU*
-serial: HardwareSerial*
-operationQueue: ModbusOperation[]
-operationCount: uint8_t
-slaveData: SlaveData[]
-firstFilter: ModbusOperationFilter*" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="330" width="220" height="150" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="SlaveData_Struct" value="SlaveData
+coils: ModbusValueEntry[]
+registers: ModbusValueEntry[]
+coilCount: uint8_t
+registerCount: uint8_t" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;componentStyle=struct;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="530" width="220" height="110" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="ModbusValueEntry_Struct" value="ModbusValueEntry
+lastUpdate: unsigned long
+address: uint16_t
+value: uint16_t
+flags: uint8_t" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;componentStyle=struct;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="700" width="220" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="ModbusOperation_Struct" value="ModbusOperation
+timestamp: unsigned long
+token: uint32_t
+address: uint16_t
+value: uint16_t
+quantity: uint16_t
+slaveId: uint8_t
+retries: uint8_t
+flags: uint8_t
+type: E_FN_CODE
+status: E_MB_OpStatus" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;componentStyle=struct;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="850" width="220" height="180" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="ModbusOperationFilter_Class" value="ModbusOperationFilter
#nextFilter: ModbusOperationFilter*" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="1080" width="220" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
|
||||
<!-- RTU Base/State (Potentially used by specific RTU device components, not directly by client) -->
|
||||
<mxCell id="RTU_Base_Class" value="RTU_Base
+deviceId: uint8_t
+state: E_DeviceState
+registers: RegisterState*[]
+registerCount: int" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;" vertex="1" parent="1">
|
||||
<mxGeometry x="310" y="850" width="180" height="100" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="RegisterState_Class" value="RegisterState
+type: E_RegType
+address: uint16_t
+value: uint16_t
+minValue: uint16_t
+maxValue: uint16_t
+priority: uint8_t" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;" vertex="1" parent="1">
|
||||
<mxGeometry x="310" y="1000" width="180" height="130" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- RTU Proxy Related -->
|
||||
<mxCell id="RTUProxyComponent_Class" value="RTUProxyComponent
-modbusRTU: ModbusRTU*
-mappings: Vector&lt;RTUMapping&gt;
-mappingStorage: RTUMapping[]" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;" vertex="1" parent="1">
|
||||
<mxGeometry x="510" y="330" width="240" height="100" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="RTUMapping_Struct" value="RTUMapping
+tcpStartAddress: short
+rtuSlaveId: uint8_t
+rtuStartAddress: short
+count: short
+type: E_FN_CODE
+access: E_ModbusAccess" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;componentStyle=struct;" vertex="1" parent="1">
|
||||
<mxGeometry x="510" y="480" width="240" height="130" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="E5Proxy_Class" value="E5Proxy
-rtuProxy: RTUProxyComponent*" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;" vertex="1" parent="1">
|
||||
<mxGeometry x="510" y="660" width="240" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Other Example Component -->
|
||||
<mxCell id="Relay_Class" value="Relay
+pin: const short
+value: bool
#modbusAddress: const short" style="shape=umlClass;verticalAlign=top;align=center;html=1;margin=10;whiteSpace=wrap;overflow=hidden;" vertex="1" parent="1">
|
||||
<mxGeometry x="840" y="660" width="220" height="90" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Relationships -->
|
||||
<!-- Inheritance -->
|
||||
<mxCell id="Rel_PHApp_Inherit_App" value="" style="endArrow=block;endFill=0;html=1;edgeStyle=orthogonalEdgeStyle;align=center;verticalAlign=bottom;rounded=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="PHApp_Class" target="App_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="630" y="130" as="sourcePoint"/>
|
||||
<mxPoint x="630" y="90" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_Component_Generalization" value="" style="endArrow=block;endFill=0;html=1;edgeStyle=orthogonalEdgeStyle;align=center;verticalAlign=bottom;rounded=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="ModbusTCP_Class" target="Component_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="950" y="330" as="sourcePoint"/>
|
||||
<mxPoint x="950" y="220" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_RTUProxy_Inherit_Component" value="" style="endArrow=block;endFill=0;html=1;edgeStyle=orthogonalEdgeStyle;align=center;verticalAlign=bottom;rounded=0;" edge="1" parent="1" source="RTUProxyComponent_Class" target="Component_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="750" y="380" as="sourcePoint"/>
|
||||
<mxPoint x="840" y="175" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="780" y="380"/>
|
||||
<mxPoint x="780" y="175"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_E5Proxy_Inherit_Component" value="" style="endArrow=block;endFill=0;html=1;edgeStyle=orthogonalEdgeStyle;align=center;verticalAlign=bottom;rounded=0;" edge="1" parent="1" source="E5Proxy_Class" target="Component_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="750" y="690" as="sourcePoint"/>
|
||||
<mxPoint x="840" y="175" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="780" y="690"/>
|
||||
<mxPoint x="780" y="175"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_Relay_Inherit_Component" value="" style="endArrow=block;endFill=0;html=1;edgeStyle=orthogonalEdgeStyle;align=center;verticalAlign=bottom;rounded=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="Relay_Class" target="Component_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="950" y="660" as="sourcePoint"/>
|
||||
<mxPoint x="950" y="220" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<!-- PHApp Ownership -->
|
||||
<mxCell id="Rel_PHApp_Owns_ModbusTCP" value="" style="endArrow=none;html=1;edgeStyle=entityRelationEdgeStyle;startArrow=diamondThin;startFill=0;sourcePerimeterSpacing=0;startSize=12;endFill=0;" edge="1" parent="1" source="PHApp_Class" target="ModbusTCP_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="750" y="210" as="sourcePoint"/>
|
||||
<mxPoint x="840" y="380" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="780" y="210"/>
|
||||
<mxPoint x="780" y="380"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_PHApp_Owns_ModbusRTU" value="" style="endArrow=none;html=1;edgeStyle=entityRelationEdgeStyle;startArrow=diamondThin;startFill=0;sourcePerimeterSpacing=0;startSize=12;endFill=0;" edge="1" parent="1" source="PHApp_Class" target="ModbusRTU_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="510" y="210" as="sourcePoint"/>
|
||||
<mxPoint x="260" y="405" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="480" y="210"/>
|
||||
<mxPoint x="480" y="405"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_PHApp_Owns_RTUProxy" value="" style="endArrow=none;html=1;edgeStyle=entityRelationEdgeStyle;startArrow=diamondThin;startFill=0;sourcePerimeterSpacing=0;startSize=12;endFill=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="PHApp_Class" target="RTUProxyComponent_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="630" y="290" as="sourcePoint"/>
|
||||
<mxPoint x="630" y="330" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_PHApp_Owns_E5Proxy" value="" style="endArrow=none;html=1;edgeStyle=entityRelationEdgeStyle;startArrow=diamondThin;startFill=0;sourcePerimeterSpacing=0;startSize=12;endFill=0;" edge="1" parent="1" source="PHApp_Class" target="E5Proxy_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="630" y="290" as="sourcePoint"/>
|
||||
<mxPoint x="630" y="660" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_PHApp_Owns_Relay" value="" style="endArrow=none;html=1;edgeStyle=entityRelationEdgeStyle;startArrow=diamondThin;startFill=0;sourcePerimeterSpacing=0;startSize=12;endFill=0;" edge="1" parent="1" source="PHApp_Class" target="Relay_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="750" y="210" as="sourcePoint"/>
|
||||
<mxPoint x="950" y="660" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="780" y="210"/>
|
||||
<mxPoint x="780" y="705"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<!-- Usage / Association -->
|
||||
<mxCell id="Rel_RTUProxy_Uses_ModbusRTU" value="uses" style="endArrow=open;html=1;align=center;verticalAlign=middle;dashed=1;endFill=0;" edge="1" parent="1" source="RTUProxyComponent_Class" target="ModbusRTU_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="510" y="380" as="sourcePoint"/>
|
||||
<mxPoint x="260" y="380" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_E5Proxy_Uses_RTUProxy" value="configures" style="endArrow=open;html=1;align=center;verticalAlign=middle;dashed=1;endFill=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="E5Proxy_Class" target="RTUProxyComponent_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="630" y="660" as="sourcePoint"/>
|
||||
<mxPoint x="630" y="430" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_RTUProxy_Uses_RTUMapping" value="uses" style="endArrow=open;html=1;align=center;verticalAlign=middle;dashed=1;endFill=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="RTUProxyComponent_Class" target="RTUMapping_Struct">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="630" y="430" as="sourcePoint"/>
|
||||
<mxPoint x="630" y="480" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_ModbusTCP_Uses_MBRegisters" value="uses" style="endArrow=open;html=1;align=center;verticalAlign=middle;dashed=1;endFill=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="ModbusTCP_Class" target="MB_Registers_Struct">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="950" y="430" as="sourcePoint"/>
|
||||
<mxPoint x="950" y="480" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_ModbusRTU_Uses_SlaveData" value="uses" style="endArrow=open;html=1;align=center;verticalAlign=middle;dashed=1;endFill=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="ModbusRTU_Class" target="SlaveData_Struct">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="150" y="480" as="sourcePoint"/>
|
||||
<mxPoint x="150" y="530" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_SlaveData_Uses_ValueEntry" value="uses" style="endArrow=open;html=1;align=center;verticalAlign=middle;dashed=1;endFill=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="SlaveData_Struct" target="ModbusValueEntry_Struct">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="150" y="640" as="sourcePoint"/>
|
||||
<mxPoint x="150" y="700" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_ModbusRTU_Uses_Operation" value="uses" style="endArrow=open;html=1;align=center;verticalAlign=middle;dashed=1;endFill=0;" edge="1" parent="1" source="ModbusRTU_Class" target="ModbusOperation_Struct">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="40" y="405" as="sourcePoint"/>
|
||||
<mxPoint x="40" y="940" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="0" y="405"/>
|
||||
<mxPoint x="0" y="940"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_ModbusRTU_Uses_Filter" value="uses" style="endArrow=open;html=1;align=center;verticalAlign=middle;dashed=1;endFill=0;" edge="1" parent="1" source="ModbusRTU_Class" target="ModbusOperationFilter_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="40" y="440" as="sourcePoint"/>
|
||||
<mxPoint x="40" y="1110" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="0" y="440"/>
|
||||
<mxPoint x="0" y="1110"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_Filter_SelfRef" value="" style="endArrow=open;html=1;align=center;verticalAlign=middle;dashed=1;endFill=0;edgeStyle=entityRelationEdgeStyle;" edge="1" parent="1" source="ModbusOperationFilter_Class" target="ModbusOperationFilter_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="260" y="1110" as="sourcePoint"/>
|
||||
<mxPoint x="260" y="1140" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="280" y="1110"/>
|
||||
<mxPoint x="280" y="1140"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="Rel_RTUBase_Uses_RegState" value="uses" style="endArrow=open;html=1;align=center;verticalAlign=middle;dashed=1;endFill=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="RTU_Base_Class" target="RegisterState_Class">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="400" y="950" as="sourcePoint"/>
|
||||
<mxPoint x="400" y="1000" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
1
docs/diagrams/classes-template.drawio
Normal file
@ -0,0 +1 @@
|
||||
<mxfile host="Electron" modified="2025-04-25T12:13:56.406Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.7.9 Chrome/85.0.4183.121 Electron/10.1.3 Safari/537.36" etag="daU4OmGsIa-WwX0TNA18" version="13.7.9" type="device"><diagram name="Class Template" id="diagram_id_template">7VhNb+IwEP01kdhKrfIBtHuELG0vrKrS7R4rk5jEqhNHtiGwv37HiRPikpS0Sy9oJQ72zPN4PPPybGF5frK94yiL5yzE1HLtcGt5PyzXdYaua6mfHe5Ky01liDgJNWhvWJA/WBttbV2TEAsDKBmjkmSmMWBpigNZ2vRaxDnLhWFaMWrumqEIHxgWAaKH1t8klLE+xcje2+8xiWK9s+PY2pOgCqwNIkYhyxsmb2Z5PmdMlqNk62OqilfVpVx32+GtE+M4lX0WTLLsxadIiHLhBtE1rj2WO7ZcD4ruTWFIIeCUwCBSg8GKs0RtxeguwUF8uUQCf6tgsGGN1AeVu6p6cOZMDdcJLbf2phvMJYH6TiiJUnBJloEV6VkAh8EcDLFMKMwdGCaIR0Q5i/TymEi8yFCg4uZAOrAxCLqiRXFjEoY4rXNplkdXTCWAtw2TLtcdZgmWfAcQ7fWqLmvuDvU03xPBudG2uEGCccU/zb2ojrxvDwx0h9q75bMkYynk3tGz2n+qzjWiTFUSk4KyjJsbTFmeqv4ob53ChQkRWK6zAWxjd0ahjB1B5Bza/BPLnPHXZ3XswdXV1bsrOEbhBxdERADbQLSWawE5Ba9CLzorGo++mzR2xoc8rlWqyWNndAIiP9x3C0/hazTlMilaMUcpyKpmWdmdJ//h4hD4+PSrCYKpAeJy/cDZdldiwFvMWml7iUcN6KycGICgWiZKzAZuG8YBgRLVu+KLMkKX9pav69Pfx1mx0rN7sHL4VaysCdPBzNpv9mUJkpGawjKfvszgncFNYMZZgIUYHMMVMveohahPYKVyH8FHWFbwFlVcwlOqJSGfEdozuIJ2Bj4rwg7fPgZa+Gq38fX6ZHwFFXyXr+A/FMkF5htTTEsLgJHYpYGhcigMOfB2jrKMpJGWuucWqQNCVLwS/yh25j18jKArkoa1zt4yPikzNpc1hfisSPj2Lm9lYetd7p2AhQdXaAcbD3AfvrqTowSEFZql79EP+KxRvYTv/4PzK672VpJ+2dWuH28d1NTeTz4TuwXtrBs4dns28OYUIoMp6upe4TuzYl/3KLbjtBT7Ex8LTPd/OBW+xt923uwv</diagram></mxfile>
|
||||
1
docs/diagrams/classes.drawio
Normal file
@ -0,0 +1 @@
|
||||
<mxfile host="Electron" modified="2025-04-25T11:40:34.195Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.7.9 Chrome/85.0.4183.121 Electron/10.1.3 Safari/537.36" etag="f92YHXGdbOW1TIcyQ-Ic" version="13.7.9" type="device"><diagram name="Class Diagram" id="diagram_id">7ZvdU6Q4EMD/mqnytkoLhvnQR2fU3XvwzlrdvbsnKwMRUguECowzc3/9dSBAwtcggnqU+7Kk02lC949OJ4wTY+3tvzIUOLfUwu5kqln7iXE1mcK/5RT+45JDItEvpkJiM2IJWS64J/9iIdSEdEssHCqKEaVuRAJVaFLfx2akyBBjdKeqPVFXvWuAbFwS3JvILUv/IlbkJNLzuZbLv2FiO+mddU30eChVFoLQQRbdSSLjemKsGaVRcuXt19jl3kv9koy7qenNJsawH7UZcBkEj2sXhWEy8Bm5W5z1TKaLydQApxsruHTB4IrAhc0vTp4Y9fitqHvwsOmcblCIf0vV4IaZpnjQ6JB6D5454Jdbz01ubayeMYsI+PfSJbYPXRENQIpEy4SHwQwETuS50Nbh0kPMJrwznt7OIRG+D5DJ7e6AOpBRMPrkxs51iGVhP5uL7B7hMT4BvJdEwl1fMfVwxA6gInoNfZkMEfCe6jMRy12Ogn4uZI6EwULIkKDPzmznAYILEaPqeK2pF1AfZl8Ttay/r9hJVlZ8EpcxtJSpN1jRnc8jxHuzKXxRVUIcbYMTuI1Wa8Wl9IjGjkGg/8DRjrJfP/ljn5ydnTWOYBhZLxxgkxB4g7y12YYwJ/NXKAaNCuRlmq5SkC/KHGd5SuZYn/cA8t23+tQT90lBOfXiUNwiHxKroCyJzsP67ktZ8fvDD1kJmooSi7Z3jO4PiQ70xq1KbE/xXFK9ThqKgpkOCxOdZ1hvKAMN5PHYxW+UYjqRV7xdnd+PUVGZEZdSOa2gcjYUlRkwNWRm/WpcNpAyfDWx3K4er6HSYKpiwKiJw/DkmF6c5r6LRNTGMM9yL9G3cZSqV2TFDRRTFRNaU+K2NM5Vaw2PCthT3VCJNaryqFZF7LI3YiEPNhIL/eU0eY/Zs5pOEwkoo/Dgm0qeQ5bFgNxbFATEt0Wy+1mR7ACJlKzwlelOXYmPIfpEfCvLtDeUXSYzVofJqXhUGOrafK5gqC/aLudGDxiWVtEaHEt6L169vaMEwgiBaRN/ALTQapX7PmvOXlb3Qs05r9g7Dbe6i/qtBk3R27FSrM9oowrgohDA2bJlAM/7SDLYRXXRi/tG5mxjqTp7WpHSdb3C2X28LODQx2ST9rvvYEiBj/GmrOD2ksexb13yEzZobXgaA0+A6Ibw+1xpqmOxZeN7MRReFYfa1EfudS4th6UYvQ2NIupBB6Nb38KWuAmos8PfvHE2T5v/pHfljau9rHl1EK3aoIV0y0x5eypjGAEgWOgXuvgjNoaaAbcReVYPFqviJobeUcIXzRSRWWG7pBcPmZKJi1HyQeAxQ1rBUPKQJUMxRtnztCYrL1tTuqSK4JOx6rJe5qyy0BqatqVWyP5FSNrSVjRk6IPSlq7cY4BNRk0i7zWwNRbv7w5dMTN1hm5+UVhOjfmQ0KXV5giYO5v2n+EqivEPh9pi2hG1xUzdjA+d3+LCeAygLfsHrbRreHfMloWjms6YFQ0NjFlS+P6588NH6VjxBYz54Op6qCAKJDp8j31LfRmsEJ4jSo1YBHnUtx4cvidLuiRghevgRYCHw4xv1PhBkOjkyuLnBPq0BHsrnBqr/8qybWicskOUunWt6wI5Wwy6QJZwir+rjAenQWu2FhgWPmMNjaGh9YRhAefZ+VtRmNbB44RQ3aVqg0PYuKt460qutDJ2PRcp7T4GozE7j/6E8fUwVuw2Bs+Hi54QLBoqlYvDJcT8UH0kAO5JJPEHrWybAdc5fbzRYyYs7kD+NxXh0Z3KQOd0P0LcVBRuobuJQhrEn2AkCo9uTT1iWTGJFgqdeGeqd9sftD5M+xBF2mzZkYxSlVY0NMxhWgxGbZ1mUv+J2Fv2LnQM+j3gyGnZh6q1Oh9nFA3Ni98xh/oGFVNVf2wGILhh7CdkgeH49yDSp3/+wyPebaoGeoMvOzVrgq9yYdMaF7YMWF3Gtaf96cg/YJ3rb/otIXeWtDJWnb7lqJZ/UNQzlskPJ1owqctEihPe41AOkUVbrslvfH5XYmlpdKzWjtHdGUpo5n+TlKjnf9plXP8H</diagram></mxfile>
|
||||
10
docs/diagrams/overview.drawio
Normal file
@ -0,0 +1,10 @@
|
||||
<mxfile host="Electron" modified="2025-04-25T11:38:08.720Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.7.9 Chrome/85.0.4183.121 Electron/10.1.3 Safari/537.36" etag="NBRJG0iJUKs-MZMhq_51" compressed="false" version="13.7.9" type="device">
|
||||
<diagram id="6UpJVxfaZ7YgvcYAvHzd" name="Page-1">
|
||||
<mxGraphModel dx="1086" dy="806" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
459
docs/docs-rest.md
Normal file
@ -0,0 +1,459 @@
|
||||
# REST API Documentation
|
||||
|
||||
This document provides an overview of the RESTful API and WebSocket interface for interacting with the device.
|
||||
|
||||
## Authentication
|
||||
|
||||
The API does not currently require authentication.
|
||||
|
||||
## Base URL
|
||||
|
||||
All API endpoints are relative to the device's IP address. For example, if the device IP is `192.168.1.100`, the full URL for `/api/v1/system/info` would be `http://192.168.1.100/api/v1/system/info`.
|
||||
|
||||
---
|
||||
|
||||
## REST API Endpoints
|
||||
|
||||
### System Endpoints
|
||||
|
||||
#### Get System Information
|
||||
|
||||
- **Endpoint:** `GET /api/v1/system/info`
|
||||
- **Description:** Retrieves general system information like firmware version, uptime, and memory usage.
|
||||
- **Response:**
|
||||
```json
|
||||
{
|
||||
"version": "3ce112f",
|
||||
"board": "esp32dev",
|
||||
"uptime": 12345,
|
||||
"timestamp": 12345678,
|
||||
"freeHeapKb": 150.5,
|
||||
"maxFreeBlockKb": 100.2,
|
||||
"cpuTicks": 123456789,
|
||||
"loopDurationMs": 500,
|
||||
"cpuLoadPercent": null
|
||||
}
|
||||
```
|
||||
- **cURL Example:**
|
||||
```bash
|
||||
curl http://<DEVICE_IP>/api/v1/system/info
|
||||
```
|
||||
- **Node.js Example:**
|
||||
```javascript
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
async function getSystemInfo() {
|
||||
try {
|
||||
const response = await fetch('http://<DEVICE_IP>/api/v1/system/info');
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching system info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
getSystemInfo();
|
||||
```
|
||||
|
||||
#### Get System Logs
|
||||
|
||||
- **Endpoint:** `GET /api/v1/system/logs`
|
||||
- **Description:** Retrieves the system logs.
|
||||
- **Query Parameters:**
|
||||
- `level` (optional): The minimum log level to retrieve. Can be one of `none`, `error`, `warning`, `notice`, `trace`, `verbose`. Defaults to `verbose`.
|
||||
- **Response:** An array of log strings.
|
||||
```json
|
||||
[
|
||||
"I: System initialized.",
|
||||
"W: Network connection weak."
|
||||
]
|
||||
```
|
||||
- **cURL Example:**
|
||||
```bash
|
||||
curl http://<DEVICE_IP>/api/v1/system/logs?level=warning
|
||||
```
|
||||
- **Node.js Example:**
|
||||
```javascript
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
async function getLogs() {
|
||||
try {
|
||||
const response = await fetch('http://<DEVICE_IP>/api/v1/system/logs?level=warning');
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching logs:', error);
|
||||
}
|
||||
}
|
||||
|
||||
getLogs();
|
||||
```
|
||||
|
||||
#### Get/Set Log Level
|
||||
|
||||
- **Endpoint:**
|
||||
- `GET /api/v1/system/log-level`
|
||||
- `GET /api/v1/system/log-level?level=<level>`
|
||||
- **Description:**
|
||||
- `GET` without parameters retrieves the current log level.
|
||||
- `GET` with the `level` parameter sets a new log level.
|
||||
- **Query Parameters:**
|
||||
- `level` (for setting): The new log level. One of `none`, `error`, `info`, `warning`, `notice`, `trace`, `verbose`.
|
||||
- **Response (Get):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"level": "verbose"
|
||||
}
|
||||
```
|
||||
- **Response (Set):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"level": "warning"
|
||||
}
|
||||
```
|
||||
- **cURL Example (Get):**
|
||||
```bash
|
||||
curl http://<DEVICE_IP>/api/v1/system/log-level
|
||||
```
|
||||
- **cURL Example (Set):**
|
||||
```bash
|
||||
curl http://<DEVICE_IP>/api/v1/system/log-level?level=warning
|
||||
```
|
||||
- **Node.js Example (Set):**
|
||||
```javascript
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
async function setLogLevel(level) {
|
||||
try {
|
||||
const response = await fetch(`http://<DEVICE_IP>/api/v1/system/log-level?level=${level}`);
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
} catch (error) {
|
||||
console.error('Error setting log level:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setLogLevel('warning');
|
||||
```
|
||||
|
||||
#### List Filesystem
|
||||
|
||||
- **Endpoint:** `GET /api/v1/fs/list`
|
||||
- **Description:** Lists files and directories in the root of the device's filesystem (LittleFS).
|
||||
- **Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "/index.html",
|
||||
"type": "file",
|
||||
"size": 1234
|
||||
},
|
||||
{
|
||||
"name": "/config",
|
||||
"type": "directory",
|
||||
"size": 0
|
||||
}
|
||||
]
|
||||
```
|
||||
- **cURL Example:**
|
||||
```bash
|
||||
curl http://<DEVICE_IP>/api/v1/fs/list
|
||||
```
|
||||
|
||||
### Modbus Endpoints
|
||||
|
||||
These endpoints provide access to Modbus data (coils and registers).
|
||||
|
||||
#### Get All Coils
|
||||
|
||||
- **Endpoint:** `GET /api/v1/coils`
|
||||
- **Description:** Retrieves the state of all mapped coils.
|
||||
- **Response:**
|
||||
```json
|
||||
{
|
||||
"coils": [
|
||||
{
|
||||
"address": 1,
|
||||
"value": 1,
|
||||
"name": "Motor_On",
|
||||
"component": "Motor",
|
||||
"id": 101,
|
||||
"type": 1,
|
||||
"flags": 0,
|
||||
"group": "Motors"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
- **cURL Example:**
|
||||
```bash
|
||||
curl http://<DEVICE_IP>/api/v1/coils
|
||||
```
|
||||
|
||||
#### Get Single Coil
|
||||
|
||||
- **Endpoint:** `GET /api/v1/coils?address=<address>`
|
||||
- **Description:** Retrieves the state of a single coil by its address.
|
||||
- **Query Parameters:**
|
||||
- `address`: The address of the coil.
|
||||
- **Response:**
|
||||
```json
|
||||
{
|
||||
"address": 1,
|
||||
"value": true
|
||||
}
|
||||
```
|
||||
- **cURL Example:**
|
||||
```bash
|
||||
curl http://<DEVICE_IP>/api/v1/coils?address=1
|
||||
```
|
||||
|
||||
#### Set Single Coil
|
||||
|
||||
- **Endpoint:** `POST /api/v1/coils/<address>?value=<value>`
|
||||
- **Description:** Sets the state of a single coil.
|
||||
- **URL Parameters:**
|
||||
- `address`: The address of the coil.
|
||||
- **Query Parameters:**
|
||||
- `value`: The new state of the coil (`1`/`true` or `0`/`false`).
|
||||
- **Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"address": 51,
|
||||
"value": true
|
||||
}
|
||||
```
|
||||
- **cURL Example:**
|
||||
```bash
|
||||
curl -X POST "http://<DEVICE_IP>/api/v1/coils/51?value=true"
|
||||
```
|
||||
- **Node.js Example:**
|
||||
```javascript
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
async function setCoil(address, value) {
|
||||
try {
|
||||
const response = await fetch(`http://<DEVICE_IP>/api/v1/coils/${address}?value=${value}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
} catch (error) {
|
||||
console.error(`Error setting coil ${address}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
setCoil(51, true);
|
||||
```
|
||||
|
||||
#### Get All Registers
|
||||
|
||||
- **Endpoint:** `GET /api/v1/registers`
|
||||
- **Description:** Retrieves the value of all mapped registers.
|
||||
- **Query Parameters (optional):**
|
||||
- `page`: The page number to retrieve (0-indexed). Defaults to 0.
|
||||
- `pageSize`: The number of registers per page. Defaults to 20.
|
||||
- **Response (with pagination):**
|
||||
```json
|
||||
{
|
||||
"registers": [
|
||||
{
|
||||
"error": 0,
|
||||
"address": 100,
|
||||
"value": 1234,
|
||||
"name": "Temperature_Setpoint",
|
||||
"component": "Heater",
|
||||
"id": 201,
|
||||
"type": 3,
|
||||
"slaveId": 1,
|
||||
"flags": 0,
|
||||
"group": "Heating"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"page": 0,
|
||||
"pageSize": 20,
|
||||
"totalRegisters": 256,
|
||||
"totalPages": 13
|
||||
}
|
||||
}
|
||||
```
|
||||
- **cURL Example (with pagination):**
|
||||
```bash
|
||||
curl http://<DEVICE_IP>/api/v1/registers?page=1&pageSize=50
|
||||
```
|
||||
```bash
|
||||
curl http://<DEVICE_IP>/api/v1/registers
|
||||
```
|
||||
|
||||
#### Get Single Register
|
||||
|
||||
- **Endpoint:** `GET /api/v1/registers?address=<address>`
|
||||
- **Description:** Retrieves the value of a single register by its address.
|
||||
- **Query Parameters:**
|
||||
- `address`: The address of the register.
|
||||
- **Response:**
|
||||
```json
|
||||
{
|
||||
"address": 100,
|
||||
"value": 1234
|
||||
}
|
||||
```
|
||||
- **cURL Example:**
|
||||
```bash
|
||||
curl http://<DEVICE_IP>/api/v1/registers?address=100
|
||||
```
|
||||
|
||||
#### Set Single Register
|
||||
|
||||
- **Endpoint:** `POST /api/v1/registers/<address>?value=<value>`
|
||||
- **Description:** Sets the value of a single holding register.
|
||||
- **URL Parameters:**
|
||||
- `address`: The address of the register.
|
||||
- **Query Parameters:**
|
||||
- `value`: The new integer value for the register (16-bit signed).
|
||||
- **Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"address": 20,
|
||||
"value": 42
|
||||
}
|
||||
```
|
||||
- **cURL Example:**
|
||||
```bash
|
||||
curl -X POST "http://<DEVICE_IP>/api/v1/registers/20?value=42"
|
||||
```
|
||||
- **Node.js Example:**
|
||||
```javascript
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
async function setRegister(address, value) {
|
||||
try {
|
||||
const response = await fetch(`http://<DEVICE_IP>/api/v1/registers/${address}?value=${value}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
} catch (error) {
|
||||
console.error(`Error setting register ${address}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
setRegister(20, 42);
|
||||
```
|
||||
|
||||
#### Get Modbus Address Mappings
|
||||
|
||||
- **Endpoint:** `GET /api/v1/mappings`
|
||||
- **Description:** Retrieves the list of all Modbus address mappings.
|
||||
- **cURL Example:**
|
||||
```bash
|
||||
curl http://<DEVICE_IP>/api/v1/mappings
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WebSocket API
|
||||
|
||||
For real-time data streaming and more complex interactions, a WebSocket interface is provided.
|
||||
|
||||
- **Endpoint:** `ws://<DEVICE_IP>/ws`
|
||||
|
||||
### Connection
|
||||
|
||||
A client can connect to the WebSocket endpoint to receive real-time updates and send commands.
|
||||
|
||||
**Node.js Example:**
|
||||
```javascript
|
||||
const WebSocket = require('ws');
|
||||
|
||||
const ws = new WebSocket('ws://<DEVICE_IP>/ws');
|
||||
|
||||
ws.on('open', function open() {
|
||||
console.log('Connected to WebSocket');
|
||||
// Send a command to get system info
|
||||
ws.send(JSON.stringify({ command: 'get_sysinfo' }));
|
||||
});
|
||||
|
||||
ws.on('message', function incoming(data) {
|
||||
console.log('Received:', JSON.parse(data));
|
||||
});
|
||||
|
||||
ws.on('close', function close() {
|
||||
console.log('Disconnected from WebSocket');
|
||||
});
|
||||
|
||||
ws.on('error', function error(err) {
|
||||
console.error('WebSocket error:', err);
|
||||
});
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
Commands are sent as JSON strings.
|
||||
|
||||
- `get_sysinfo`: Get system information.
|
||||
- `get_logs`: Get log history.
|
||||
- `get_coils`: Get all coil values.
|
||||
- `get_registers`: Get all register values. Supports paging.
|
||||
- `write_register`: Write to a register. Payload: `{ "command": "write_register", "address": 100, "value": 1234 }`
|
||||
- `write_coil`: Write to a coil. Payload: `{ "command": "write_coil", "address": 50, "value": true }`
|
||||
|
||||
### Register Paging via WebSocket
|
||||
|
||||
When requesting all registers via the `get_registers` command, the response can be paginated to handle a large number of registers efficiently.
|
||||
|
||||
**Request Payload:**
|
||||
```json
|
||||
{
|
||||
"command": "get_registers",
|
||||
"page": 0,
|
||||
"pageSize": 50
|
||||
}
|
||||
```
|
||||
|
||||
- `page` (optional): The page number to retrieve (0-indexed). Defaults to 0.
|
||||
- `pageSize` (optional): The number of registers per page. Defaults to 20.
|
||||
|
||||
**Response Payload:**
|
||||
|
||||
The response is a JSON object containing the data for the requested page and metadata about the pagination.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "registers",
|
||||
"data": [
|
||||
{
|
||||
"error": 0,
|
||||
"address": 100,
|
||||
"value": 1234,
|
||||
"name": "Register_100",
|
||||
"component": "CompA",
|
||||
"id": 1,
|
||||
"type": 3,
|
||||
"slaveId": 1,
|
||||
"flags": 1,
|
||||
"group": "Group1"
|
||||
}
|
||||
// ... up to pageSize registers
|
||||
],
|
||||
"meta": {
|
||||
"page": 0,
|
||||
"pageSize": 50,
|
||||
"totalRegisters": 256,
|
||||
"totalPages": 6
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `type`: The type of the message (`registers`).
|
||||
- `data`: An array of register objects for the current page.
|
||||
- `meta`: An object containing pagination details:
|
||||
- `page`: The current page number.
|
||||
- `pageSize`: The number of items per page.
|
||||
- `totalRegisters`: The total number of registers available.
|
||||
- `totalPages`: The total number of pages.
|
||||
157
docs/doxygen-awesome-darkmode-toggle.js
Normal file
@ -0,0 +1,157 @@
|
||||
/**
|
||||
|
||||
Doxygen Awesome
|
||||
https://github.com/jothepro/doxygen-awesome-css
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 - 2023 jothepro
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
class DoxygenAwesomeDarkModeToggle extends HTMLElement {
|
||||
// SVG icons from https://fonts.google.com/icons
|
||||
// Licensed under the Apache 2.0 license:
|
||||
// https://www.apache.org/licenses/LICENSE-2.0.html
|
||||
static lightModeIcon = `<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#FCBF00"><rect fill="none" height="24" width="24"/><circle cx="12" cy="12" opacity=".3" r="3"/><path d="M12,9c1.65,0,3,1.35,3,3s-1.35,3-3,3s-3-1.35-3-3S10.35,9,12,9 M12,7c-2.76,0-5,2.24-5,5s2.24,5,5,5s5-2.24,5-5 S14.76,7,12,7L12,7z M2,13l2,0c0.55,0,1-0.45,1-1s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S1.45,13,2,13z M20,13l2,0c0.55,0,1-0.45,1-1 s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S19.45,13,20,13z M11,2v2c0,0.55,0.45,1,1,1s1-0.45,1-1V2c0-0.55-0.45-1-1-1S11,1.45,11,2z M11,20v2c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1C11.45,19,11,19.45,11,20z M5.99,4.58c-0.39-0.39-1.03-0.39-1.41,0 c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0s0.39-1.03,0-1.41L5.99,4.58z M18.36,16.95 c-0.39-0.39-1.03-0.39-1.41,0c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0c0.39-0.39,0.39-1.03,0-1.41 L18.36,16.95z M19.42,5.99c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06c-0.39,0.39-0.39,1.03,0,1.41 s1.03,0.39,1.41,0L19.42,5.99z M7.05,18.36c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06 c-0.39,0.39-0.39,1.03,0,1.41s1.03,0.39,1.41,0L7.05,18.36z"/></svg>`
|
||||
static darkModeIcon = `<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#FE9700"><rect fill="none" height="24" width="24"/><path d="M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27 C17.45,17.19,14.93,19,12,19c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z" opacity=".3"/><path d="M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"/></svg>`
|
||||
static title = "Toggle Light/Dark Mode"
|
||||
|
||||
static prefersLightModeInDarkModeKey = "prefers-light-mode-in-dark-mode"
|
||||
static prefersDarkModeInLightModeKey = "prefers-dark-mode-in-light-mode"
|
||||
|
||||
static _staticConstructor = function() {
|
||||
DoxygenAwesomeDarkModeToggle.enableDarkMode(DoxygenAwesomeDarkModeToggle.userPreference)
|
||||
// Update the color scheme when the browsers preference changes
|
||||
// without user interaction on the website.
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
|
||||
DoxygenAwesomeDarkModeToggle.onSystemPreferenceChanged()
|
||||
})
|
||||
// Update the color scheme when the tab is made visible again.
|
||||
// It is possible that the appearance was changed in another tab
|
||||
// while this tab was in the background.
|
||||
document.addEventListener("visibilitychange", visibilityState => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
DoxygenAwesomeDarkModeToggle.onSystemPreferenceChanged()
|
||||
}
|
||||
});
|
||||
}()
|
||||
|
||||
static init() {
|
||||
$(function() {
|
||||
$(document).ready(function() {
|
||||
const toggleButton = document.createElement('doxygen-awesome-dark-mode-toggle')
|
||||
toggleButton.title = DoxygenAwesomeDarkModeToggle.title
|
||||
toggleButton.updateIcon()
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
|
||||
toggleButton.updateIcon()
|
||||
})
|
||||
document.addEventListener("visibilitychange", visibilityState => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
toggleButton.updateIcon()
|
||||
}
|
||||
});
|
||||
|
||||
$(document).ready(function(){
|
||||
document.getElementById("MSearchBox").parentNode.appendChild(toggleButton)
|
||||
})
|
||||
$(window).resize(function(){
|
||||
document.getElementById("MSearchBox").parentNode.appendChild(toggleButton)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.onclick=this.toggleDarkMode
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` for dark-mode, `false` for light-mode system preference
|
||||
*/
|
||||
static get systemPreference() {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` for dark-mode, `false` for light-mode user preference
|
||||
*/
|
||||
static get userPreference() {
|
||||
return (!DoxygenAwesomeDarkModeToggle.systemPreference && localStorage.getItem(DoxygenAwesomeDarkModeToggle.prefersDarkModeInLightModeKey)) ||
|
||||
(DoxygenAwesomeDarkModeToggle.systemPreference && !localStorage.getItem(DoxygenAwesomeDarkModeToggle.prefersLightModeInDarkModeKey))
|
||||
}
|
||||
|
||||
static set userPreference(userPreference) {
|
||||
DoxygenAwesomeDarkModeToggle.darkModeEnabled = userPreference
|
||||
if(!userPreference) {
|
||||
if(DoxygenAwesomeDarkModeToggle.systemPreference) {
|
||||
localStorage.setItem(DoxygenAwesomeDarkModeToggle.prefersLightModeInDarkModeKey, true)
|
||||
} else {
|
||||
localStorage.removeItem(DoxygenAwesomeDarkModeToggle.prefersDarkModeInLightModeKey)
|
||||
}
|
||||
} else {
|
||||
if(!DoxygenAwesomeDarkModeToggle.systemPreference) {
|
||||
localStorage.setItem(DoxygenAwesomeDarkModeToggle.prefersDarkModeInLightModeKey, true)
|
||||
} else {
|
||||
localStorage.removeItem(DoxygenAwesomeDarkModeToggle.prefersLightModeInDarkModeKey)
|
||||
}
|
||||
}
|
||||
DoxygenAwesomeDarkModeToggle.onUserPreferenceChanged()
|
||||
}
|
||||
|
||||
static enableDarkMode(enable) {
|
||||
if(enable) {
|
||||
DoxygenAwesomeDarkModeToggle.darkModeEnabled = true
|
||||
document.documentElement.classList.add("dark-mode")
|
||||
document.documentElement.classList.remove("light-mode")
|
||||
} else {
|
||||
DoxygenAwesomeDarkModeToggle.darkModeEnabled = false
|
||||
document.documentElement.classList.remove("dark-mode")
|
||||
document.documentElement.classList.add("light-mode")
|
||||
}
|
||||
}
|
||||
|
||||
static onSystemPreferenceChanged() {
|
||||
DoxygenAwesomeDarkModeToggle.darkModeEnabled = DoxygenAwesomeDarkModeToggle.userPreference
|
||||
DoxygenAwesomeDarkModeToggle.enableDarkMode(DoxygenAwesomeDarkModeToggle.darkModeEnabled)
|
||||
}
|
||||
|
||||
static onUserPreferenceChanged() {
|
||||
DoxygenAwesomeDarkModeToggle.enableDarkMode(DoxygenAwesomeDarkModeToggle.darkModeEnabled)
|
||||
}
|
||||
|
||||
toggleDarkMode() {
|
||||
DoxygenAwesomeDarkModeToggle.userPreference = !DoxygenAwesomeDarkModeToggle.userPreference
|
||||
this.updateIcon()
|
||||
}
|
||||
|
||||
updateIcon() {
|
||||
if(DoxygenAwesomeDarkModeToggle.darkModeEnabled) {
|
||||
this.innerHTML = DoxygenAwesomeDarkModeToggle.darkModeIcon
|
||||
} else {
|
||||
this.innerHTML = DoxygenAwesomeDarkModeToggle.lightModeIcon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("doxygen-awesome-dark-mode-toggle", DoxygenAwesomeDarkModeToggle);
|
||||
85
docs/doxygen-awesome-fragment-copy-button.js
Normal file
@ -0,0 +1,85 @@
|
||||
/**
|
||||
|
||||
Doxygen Awesome
|
||||
https://github.com/jothepro/doxygen-awesome-css
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 - 2023 jothepro
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
class DoxygenAwesomeFragmentCopyButton extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.onclick=this.copyContent
|
||||
}
|
||||
static title = "Copy to clipboard"
|
||||
static copyIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>`
|
||||
static successIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/></svg>`
|
||||
static successDuration = 980
|
||||
static init() {
|
||||
$(function() {
|
||||
$(document).ready(function() {
|
||||
if(navigator.clipboard) {
|
||||
const fragments = document.getElementsByClassName("fragment")
|
||||
for(const fragment of fragments) {
|
||||
const fragmentWrapper = document.createElement("div")
|
||||
fragmentWrapper.className = "doxygen-awesome-fragment-wrapper"
|
||||
const fragmentCopyButton = document.createElement("doxygen-awesome-fragment-copy-button")
|
||||
fragmentCopyButton.innerHTML = DoxygenAwesomeFragmentCopyButton.copyIcon
|
||||
fragmentCopyButton.title = DoxygenAwesomeFragmentCopyButton.title
|
||||
|
||||
fragment.parentNode.replaceChild(fragmentWrapper, fragment)
|
||||
fragmentWrapper.appendChild(fragment)
|
||||
fragmentWrapper.appendChild(fragmentCopyButton)
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
copyContent() {
|
||||
const content = this.previousSibling.cloneNode(true)
|
||||
// filter out line number from file listings
|
||||
content.querySelectorAll(".lineno, .ttc").forEach((node) => {
|
||||
node.remove()
|
||||
})
|
||||
let textContent = content.textContent
|
||||
// remove trailing newlines that appear in file listings
|
||||
let numberOfTrailingNewlines = 0
|
||||
while(textContent.charAt(textContent.length - (numberOfTrailingNewlines + 1)) == '\n') {
|
||||
numberOfTrailingNewlines++;
|
||||
}
|
||||
textContent = textContent.substring(0, textContent.length - numberOfTrailingNewlines)
|
||||
navigator.clipboard.writeText(textContent);
|
||||
this.classList.add("success")
|
||||
this.innerHTML = DoxygenAwesomeFragmentCopyButton.successIcon
|
||||
window.setTimeout(() => {
|
||||
this.classList.remove("success")
|
||||
this.innerHTML = DoxygenAwesomeFragmentCopyButton.copyIcon
|
||||
}, DoxygenAwesomeFragmentCopyButton.successDuration);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("doxygen-awesome-fragment-copy-button", DoxygenAwesomeFragmentCopyButton)
|
||||
81
docs/doxygen-awesome-interactive-toc.js
Normal file
@ -0,0 +1,81 @@
|
||||
/**
|
||||
|
||||
Doxygen Awesome
|
||||
https://github.com/jothepro/doxygen-awesome-css
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 - 2023 jothepro
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
class DoxygenAwesomeInteractiveToc {
|
||||
static topOffset = 38
|
||||
static hideMobileMenu = true
|
||||
static headers = []
|
||||
|
||||
static init() {
|
||||
window.addEventListener("load", () => {
|
||||
let toc = document.querySelector(".contents > .toc")
|
||||
if(toc) {
|
||||
toc.classList.add("interactive")
|
||||
if(!DoxygenAwesomeInteractiveToc.hideMobileMenu) {
|
||||
toc.classList.add("open")
|
||||
}
|
||||
document.querySelector(".contents > .toc > h3")?.addEventListener("click", () => {
|
||||
if(toc.classList.contains("open")) {
|
||||
toc.classList.remove("open")
|
||||
} else {
|
||||
toc.classList.add("open")
|
||||
}
|
||||
})
|
||||
|
||||
document.querySelectorAll(".contents > .toc > ul a").forEach((node) => {
|
||||
let id = node.getAttribute("href").substring(1)
|
||||
DoxygenAwesomeInteractiveToc.headers.push({
|
||||
node: node,
|
||||
headerNode: document.getElementById(id)
|
||||
})
|
||||
|
||||
document.getElementById("doc-content")?.addEventListener("scroll", () => {
|
||||
DoxygenAwesomeInteractiveToc.update()
|
||||
})
|
||||
})
|
||||
DoxygenAwesomeInteractiveToc.update()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static update() {
|
||||
let active = DoxygenAwesomeInteractiveToc.headers[0]?.node
|
||||
DoxygenAwesomeInteractiveToc.headers.forEach((header) => {
|
||||
let position = header.headerNode.getBoundingClientRect().top
|
||||
header.node.classList.remove("active")
|
||||
header.node.classList.remove("aboveActive")
|
||||
if(position < DoxygenAwesomeInteractiveToc.topOffset) {
|
||||
active = header.node
|
||||
active?.classList.add("aboveActive")
|
||||
}
|
||||
})
|
||||
active?.classList.add("active")
|
||||
active?.classList.remove("aboveActive")
|
||||
}
|
||||
}
|
||||
51
docs/doxygen-awesome-paragraph-link.js
Normal file
@ -0,0 +1,51 @@
|
||||
/**
|
||||
|
||||
Doxygen Awesome
|
||||
https://github.com/jothepro/doxygen-awesome-css
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 - 2023 jothepro
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
class DoxygenAwesomeParagraphLink {
|
||||
// Icon from https://fonts.google.com/icons
|
||||
// Licensed under the Apache 2.0 license:
|
||||
// https://www.apache.org/licenses/LICENSE-2.0.html
|
||||
static icon = `<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 0 24 24" width="20px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17 7h-4v2h4c1.65 0 3 1.35 3 3s-1.35 3-3 3h-4v2h4c2.76 0 5-2.24 5-5s-2.24-5-5-5zm-6 8H7c-1.65 0-3-1.35-3-3s1.35-3 3-3h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-2zm-3-4h8v2H8z"/></svg>`
|
||||
static title = "Permanent Link"
|
||||
static init() {
|
||||
$(function() {
|
||||
$(document).ready(function() {
|
||||
document.querySelectorAll(".contents a.anchor[id], .contents .groupheader > a[id]").forEach((node) => {
|
||||
let anchorlink = document.createElement("a")
|
||||
anchorlink.setAttribute("href", `#${node.getAttribute("id")}`)
|
||||
anchorlink.setAttribute("title", DoxygenAwesomeParagraphLink.title)
|
||||
anchorlink.classList.add("anchorlink")
|
||||
node.classList.add("anchor")
|
||||
anchorlink.innerHTML = DoxygenAwesomeParagraphLink.icon
|
||||
node.parentElement.appendChild(anchorlink)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
40
docs/doxygen-awesome-sidebar-only-darkmode-toggle.css
Normal file
@ -0,0 +1,40 @@
|
||||
|
||||
/**
|
||||
|
||||
Doxygen Awesome
|
||||
https://github.com/jothepro/doxygen-awesome-css
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 - 2023 jothepro
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
|
||||
#MSearchBox {
|
||||
width: calc(var(--side-nav-fixed-width) - calc(2 * var(--spacing-medium)) - var(--searchbar-height) - 1px);
|
||||
}
|
||||
|
||||
#MSearchField {
|
||||
width: calc(var(--side-nav-fixed-width) - calc(2 * var(--spacing-medium)) - 66px - var(--searchbar-height));
|
||||
}
|
||||
}
|
||||
116
docs/doxygen-awesome-sidebar-only.css
Normal file
@ -0,0 +1,116 @@
|
||||
/**
|
||||
|
||||
Doxygen Awesome
|
||||
https://github.com/jothepro/doxygen-awesome-css
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 - 2023 jothepro
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
html {
|
||||
/* side nav width. MUST be = `TREEVIEW_WIDTH`.
|
||||
* Make sure it is wide enough to contain the page title (logo + title + version)
|
||||
*/
|
||||
--side-nav-fixed-width: 335px;
|
||||
--menu-display: none;
|
||||
|
||||
--top-height: 120px;
|
||||
--toc-sticky-top: -25px;
|
||||
--toc-max-height: calc(100vh - 2 * var(--spacing-medium) - 25px);
|
||||
}
|
||||
|
||||
#projectname {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
html {
|
||||
--searchbar-background: var(--page-background-color);
|
||||
}
|
||||
|
||||
#side-nav {
|
||||
min-width: var(--side-nav-fixed-width);
|
||||
max-width: var(--side-nav-fixed-width);
|
||||
top: var(--top-height);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
#nav-tree, #side-nav {
|
||||
height: calc(100vh - var(--top-height)) !important;
|
||||
}
|
||||
|
||||
#nav-tree {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#top {
|
||||
display: block;
|
||||
border-bottom: none;
|
||||
height: var(--top-height);
|
||||
margin-bottom: calc(0px - var(--top-height));
|
||||
max-width: var(--side-nav-fixed-width);
|
||||
overflow: hidden;
|
||||
background: var(--side-nav-background);
|
||||
}
|
||||
#main-nav {
|
||||
float: left;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.ui-resizable-handle {
|
||||
cursor: default;
|
||||
width: 1px !important;
|
||||
background: var(--separator-color);
|
||||
box-shadow: 0 calc(-2 * var(--top-height)) 0 0 var(--separator-color);
|
||||
}
|
||||
|
||||
#nav-path {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
left: var(--side-nav-fixed-width);
|
||||
bottom: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#doc-content {
|
||||
height: calc(100vh - 31px) !important;
|
||||
padding-bottom: calc(3 * var(--spacing-large));
|
||||
padding-top: calc(var(--top-height) - 80px);
|
||||
box-sizing: border-box;
|
||||
margin-left: var(--side-nav-fixed-width) !important;
|
||||
}
|
||||
|
||||
#MSearchBox {
|
||||
width: calc(var(--side-nav-fixed-width) - calc(2 * var(--spacing-medium)));
|
||||
}
|
||||
|
||||
#MSearchField {
|
||||
width: calc(var(--side-nav-fixed-width) - calc(2 * var(--spacing-medium)) - 65px);
|
||||
}
|
||||
|
||||
#MSearchResultsWindow {
|
||||
left: var(--spacing-medium) !important;
|
||||
right: auto;
|
||||
}
|
||||
}
|
||||
90
docs/doxygen-awesome-tabs.js
Normal file
@ -0,0 +1,90 @@
|
||||
/**
|
||||
|
||||
Doxygen Awesome
|
||||
https://github.com/jothepro/doxygen-awesome-css
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 jothepro
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
class DoxygenAwesomeTabs {
|
||||
|
||||
static init() {
|
||||
window.addEventListener("load", () => {
|
||||
document.querySelectorAll(".tabbed:not(:empty)").forEach((tabbed, tabbedIndex) => {
|
||||
let tabLinkList = []
|
||||
tabbed.querySelectorAll(":scope > ul > li").forEach((tab, tabIndex) => {
|
||||
tab.id = "tab_" + tabbedIndex + "_" + tabIndex
|
||||
let header = tab.querySelector(".tab-title")
|
||||
let tabLink = document.createElement("button")
|
||||
tabLink.classList.add("tab-button")
|
||||
tabLink.appendChild(header)
|
||||
header.title = header.textContent
|
||||
tabLink.addEventListener("click", () => {
|
||||
tabbed.querySelectorAll(":scope > ul > li").forEach((tab) => {
|
||||
tab.classList.remove("selected")
|
||||
})
|
||||
tabLinkList.forEach((tabLink) => {
|
||||
tabLink.classList.remove("active")
|
||||
})
|
||||
tab.classList.add("selected")
|
||||
tabLink.classList.add("active")
|
||||
})
|
||||
tabLinkList.push(tabLink)
|
||||
if(tabIndex == 0) {
|
||||
tab.classList.add("selected")
|
||||
tabLink.classList.add("active")
|
||||
}
|
||||
})
|
||||
let tabsOverview = document.createElement("div")
|
||||
tabsOverview.classList.add("tabs-overview")
|
||||
let tabsOverviewContainer = document.createElement("div")
|
||||
tabsOverviewContainer.classList.add("tabs-overview-container")
|
||||
tabLinkList.forEach((tabLink) => {
|
||||
tabsOverview.appendChild(tabLink)
|
||||
})
|
||||
tabsOverviewContainer.appendChild(tabsOverview)
|
||||
tabbed.before(tabsOverviewContainer)
|
||||
|
||||
function resize() {
|
||||
let maxTabHeight = 0
|
||||
tabbed.querySelectorAll(":scope > ul > li").forEach((tab, tabIndex) => {
|
||||
let visibility = tab.style.display
|
||||
tab.style.display = "block"
|
||||
maxTabHeight = Math.max(tab.offsetHeight, maxTabHeight)
|
||||
tab.style.display = visibility
|
||||
})
|
||||
tabbed.style.height = `${maxTabHeight + 10}px`
|
||||
}
|
||||
|
||||
resize()
|
||||
new ResizeObserver(resize).observe(tabbed)
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
static resize(tabbed) {
|
||||
|
||||
}
|
||||
}
|
||||
2677
docs/doxygen-awesome.css
Normal file
54
docs/doxygen-custom/custom-alternative.css
Normal file
@ -0,0 +1,54 @@
|
||||
html.alternative {
|
||||
/* primary theme color. This will affect the entire websites color scheme: links, arrows, labels, ... */
|
||||
--primary-color: #AF7FE4;
|
||||
--primary-dark-color: #9270E4;
|
||||
--primary-light-color: #7aabd6;
|
||||
--primary-lighter-color: #cae1f1;
|
||||
--primary-lightest-color: #e9f1f8;
|
||||
|
||||
/* page base colors */
|
||||
--page-background-color: white;
|
||||
--page-foreground-color: #2c3e50;
|
||||
--page-secondary-foreground-color: #67727e;
|
||||
|
||||
|
||||
--border-radius-large: 22px;
|
||||
--border-radius-small: 9px;
|
||||
--border-radius-medium: 14px;
|
||||
--spacing-small: 8px;
|
||||
--spacing-medium: 14px;
|
||||
--spacing-large: 19px;
|
||||
|
||||
--top-height: 125px;
|
||||
|
||||
--side-nav-background: #324067;
|
||||
--side-nav-foreground: #F1FDFF;
|
||||
--header-foreground: var(--side-nav-foreground);
|
||||
--searchbar-background: var(--side-nav-foreground);
|
||||
--searchbar-border-radius: var(--border-radius-medium);
|
||||
--header-background: var(--side-nav-background);
|
||||
--header-foreground: var(--side-nav-foreground);
|
||||
|
||||
--toc-background: rgb(243, 240, 252);
|
||||
--toc-foreground: var(--page-foreground-color);
|
||||
}
|
||||
|
||||
html.alternative.dark-mode {
|
||||
color-scheme: dark;
|
||||
|
||||
--primary-color: #AF7FE4;
|
||||
--primary-dark-color: #9270E4;
|
||||
--primary-light-color: #4779ac;
|
||||
--primary-lighter-color: #191e21;
|
||||
--primary-lightest-color: #191a1c;
|
||||
|
||||
--page-background-color: #1C1D1F;
|
||||
--page-foreground-color: #d2dbde;
|
||||
--page-secondary-foreground-color: #859399;
|
||||
--separator-color: #3a3246;
|
||||
--side-nav-background: #171D32;
|
||||
--side-nav-foreground: #F1FDFF;
|
||||
--toc-background: #20142C;
|
||||
--searchbar-background: var(--page-background-color);
|
||||
|
||||
}
|
||||
57
docs/doxygen-custom/custom.css
Normal file
@ -0,0 +1,57 @@
|
||||
.github-corner svg {
|
||||
fill: var(--primary-light-color);
|
||||
color: var(--page-background-color);
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.github-corner svg {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
#projectnumber {
|
||||
margin-right: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.alter-theme-button {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
background: var(--primary-color);
|
||||
color: var(--page-background-color) !important;
|
||||
border-radius: var(--border-radius-medium);
|
||||
padding: var(--spacing-small) var(--spacing-medium);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.alter-theme-button:hover {
|
||||
background: var(--primary-dark-color);
|
||||
}
|
||||
|
||||
html.dark-mode .darkmode_inverted_image img, /* < doxygen 1.9.3 */
|
||||
html.dark-mode .darkmode_inverted_image object[type="image/svg+xml"] /* doxygen 1.9.3 */ {
|
||||
filter: brightness(89%) hue-rotate(180deg) invert();
|
||||
}
|
||||
|
||||
.bordered_image {
|
||||
border-radius: var(--border-radius-small);
|
||||
border: 1px solid var(--separator-color);
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
html.dark-mode .bordered_image img, /* < doxygen 1.9.3 */
|
||||
html.dark-mode .bordered_image object[type="image/svg+xml"] /* doxygen 1.9.3 */ {
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
|
||||
.title_screenshot {
|
||||
filter: drop-shadow(0px 3px 10px rgba(0,0,0,0.22));
|
||||
max-width: 500px;
|
||||
margin: var(--spacing-large) 0;
|
||||
}
|
||||
|
||||
.title_screenshot .caption {
|
||||
display: none;
|
||||
}
|
||||
90
docs/doxygen-custom/header.html
Normal file
@ -0,0 +1,90 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/xhtml;charset=UTF-8"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=9"/>
|
||||
<meta name="generator" content="Doxygen $doxygenversion"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
|
||||
<!-- BEGIN opengraph metadata -->
|
||||
<meta property="og:title" content="Doxygen Awesome" />
|
||||
<meta property="og:image" content="https://repository-images.githubusercontent.com/348492097/4f16df80-88fb-11eb-9d31-4015ff22c452" />
|
||||
<meta property="og:description" content="Custom CSS theme for doxygen html-documentation with lots of customization parameters." />
|
||||
<meta property="og:url" content="https://jothepro.github.io/doxygen-awesome-css/" />
|
||||
<!-- END opengraph metadata -->
|
||||
|
||||
<!-- BEGIN twitter metadata -->
|
||||
<meta name="twitter:image:src" content="https://repository-images.githubusercontent.com/348492097/4f16df80-88fb-11eb-9d31-4015ff22c452" />
|
||||
<meta name="twitter:title" content="Doxygen Awesome" />
|
||||
<meta name="twitter:description" content="Custom CSS theme for doxygen html-documentation with lots of customization parameters." />
|
||||
<!-- END twitter metadata -->
|
||||
|
||||
<!--BEGIN PROJECT_NAME--><title>$projectname: $title</title><!--END PROJECT_NAME-->
|
||||
<!--BEGIN !PROJECT_NAME--><title>$title</title><!--END !PROJECT_NAME-->
|
||||
<link href="$relpath^tabs.css" rel="stylesheet" type="text/css"/>
|
||||
<link rel="icon" type="image/svg+xml" href="logo.drawio.svg"/>
|
||||
<script type="text/javascript" src="$relpath^jquery.js"></script>
|
||||
<script type="text/javascript" src="$relpath^dynsections.js"></script>
|
||||
<script type="text/javascript" src="$relpath^doxygen-awesome-darkmode-toggle.js"></script>
|
||||
<script type="text/javascript" src="$relpath^doxygen-awesome-fragment-copy-button.js"></script>
|
||||
<script type="text/javascript" src="$relpath^doxygen-awesome-paragraph-link.js"></script>
|
||||
<script type="text/javascript" src="$relpath^doxygen-awesome-interactive-toc.js"></script>
|
||||
<script type="text/javascript" src="$relpath^doxygen-awesome-tabs.js"></script>
|
||||
<script type="text/javascript" src="$relpath^toggle-alternative-theme.js"></script>
|
||||
<script type="text/javascript">
|
||||
DoxygenAwesomeFragmentCopyButton.init()
|
||||
DoxygenAwesomeDarkModeToggle.init()
|
||||
DoxygenAwesomeParagraphLink.init()
|
||||
DoxygenAwesomeInteractiveToc.init()
|
||||
DoxygenAwesomeTabs.init()
|
||||
</script>
|
||||
$treeview
|
||||
$search
|
||||
$mathjax
|
||||
<link href="$relpath^$stylesheet" rel="stylesheet" type="text/css" />
|
||||
$extrastylesheet
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- https://tholman.com/github-corners/ -->
|
||||
<a href="https://github.com/jothepro/doxygen-awesome-css" class="github-corner" title="View source on GitHub" target="_blank" rel="noopener noreferrer">
|
||||
<svg viewBox="0 0 250 250" width="40" height="40" style="position: absolute; top: 0; border: 0; right: 0; z-index: 99;" aria-hidden="true">
|
||||
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a><style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}</style>
|
||||
|
||||
|
||||
<div id="top"><!-- do not remove this div, it is closed by doxygen! -->
|
||||
|
||||
<!--BEGIN TITLEAREA-->
|
||||
<div id="titlearea">
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
<tbody>
|
||||
<tr style="height: 56px;">
|
||||
<!--BEGIN PROJECT_LOGO-->
|
||||
<td id="projectlogo"><img alt="Logo" src="$relpath^$projectlogo"/></td>
|
||||
<!--END PROJECT_LOGO-->
|
||||
<!--BEGIN PROJECT_NAME-->
|
||||
<td id="projectalign" style="padding-left: 0.5em;">
|
||||
<div id="projectname">$projectname
|
||||
<!--BEGIN PROJECT_NUMBER--> <span id="projectnumber">$projectnumber</span><!--END PROJECT_NUMBER-->
|
||||
</div>
|
||||
<!--BEGIN PROJECT_BRIEF--><div id="projectbrief">$projectbrief</div><!--END PROJECT_BRIEF-->
|
||||
</td>
|
||||
<!--END PROJECT_NAME-->
|
||||
<!--BEGIN !PROJECT_NAME-->
|
||||
<!--BEGIN PROJECT_BRIEF-->
|
||||
<td style="padding-left: 0.5em;">
|
||||
<div id="projectbrief">$projectbrief</div>
|
||||
</td>
|
||||
<!--END PROJECT_BRIEF-->
|
||||
<!--END !PROJECT_NAME-->
|
||||
<!--BEGIN DISABLE_INDEX-->
|
||||
<!--BEGIN SEARCHENGINE-->
|
||||
<td>$searchbox</td>
|
||||
<!--END SEARCHENGINE-->
|
||||
<!--END DISABLE_INDEX-->
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--END TITLEAREA-->
|
||||
<!-- end header part -->
|
||||
12
docs/doxygen-custom/toggle-alternative-theme.js
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
let original_theme_active = true;
|
||||
|
||||
function toggle_alternative_theme() {
|
||||
if(original_theme_active) {
|
||||
document.documentElement.classList.add("alternative")
|
||||
original_theme_active = false;
|
||||
} else {
|
||||
document.documentElement.classList.remove("alternative")
|
||||
original_theme_active = true;
|
||||
}
|
||||
}
|
||||
BIN
docs/img/cab-real.JPG
Normal file
|
After Width: | Height: | Size: 376 KiB |
BIN
docs/img/cab.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
docs/img/cp-air.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/img/cp-cartoon.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
docs/img/cp-max.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
docs/img/cp-real.jpeg
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
docs/img/cp-simple.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/img/elz-m-rc2.jpg
Normal file
|
After Width: | Height: | Size: 764 KiB |
BIN
docs/img/screenshot-latest.jpg
Normal file
|
After Width: | Height: | Size: 308 KiB |
BIN
docs/img/screenshot-modbus.jpg
Normal file
|
After Width: | Height: | Size: 340 KiB |
117
docs/img/theme-variants-base.drawio.svg
Normal file
@ -0,0 +1,117 @@
|
||||
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="491px" height="261px" viewBox="-0.5 -0.5 491 261" content="<mxfile><diagram id="6E4AiNPWWr3a8GvC3Ypl" name="Page-1">xZfLrpswEIafBqndIIzBwLK5tZt2k0pd+wQHrBqcOs6tT98xmABxmrYiUUEK+Pd1vhkPjofn1fmjorvys8yZ8MIgP3t44YVhihL4NcKlFZIkbYVC8byVUC+s+U9mxcCqB56z/aihllJovhuLG1nXbKNHGlVKnsbNtlKMZ93RgjnCekOFq37juS6tWXHQ658YL8puZhTYmop2ja2wL2kuTwMJLz08V1Lq9q06z5kw7Doubb/Vb2qvC1Os1n/ToVvHkYqDNc4uTF86awslDzsPz+BR58x0DKBE1cY6JoWSO69dypEpzc73vELfugl6wyFgmKyYVhdod+rRRpldZznAGhIrUuvO4tq3txherNH3AZA/2z+2+1RyzdY7ujG1Jwhv0EpdwfgLBK97reR3NpdCqqY3JktzQ00h6H5vBwEEmvKaKVt28V0dc8vvP3GKHE5zsMEs97m4ls09FUlXm6V+jNteNtVEkZ/a7DPgBi19Qlx0KI19hKfTCx16X7kW7I0qUN99oUdeUM1l7RkbwfRgzWB/le+nwd1yIQZoF8vlYrV6GKMvjcMGPUqyqPudzhU7XNfwZXCwPpfjKjH36zg+jFQUEj/Br4vU2CG6go8Ra4DOFKP5Rh2q3X4i0deSg4wHu3lwZQ7GuwGKsY9HF5nOM3EjtNnaD/ihKfy2kJUHekjM/aR0ihPkJ2GEcJJhFOAoHmVWdLu9Qzd4A+R3naM0TOLsTiDf+I6k052Q3vnIE6EtruZI0hEjPw7m7DXr2Q0kUphnk7q7AWDqdoy2znEroNWPfFfLmt2kGCtRwYsaioJtzQjGTRyOoB+sXPE8N5PcDZXxZjQLtGe1cPJp43x1U5qROEZdQIT/Ggyw24ahkJL4JcEAxf443dQN/pPg5S8=</diagram></mxfile>">
|
||||
<defs/>
|
||||
<g>
|
||||
<rect x="0" y="0" width="490" height="260" fill="rgb(255, 255, 255)" stroke="#6e6e6e" pointer-events="none"/>
|
||||
<rect x="198.53" y="44.87" width="219.66" height="185.13" fill="rgb(255, 255, 255)" stroke="#e3e3e3" pointer-events="none"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 218px; height: 1px; padding-top: 137px; margin-left: 200px;">
|
||||
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">
|
||||
Content
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="308" y="141" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
Content
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<rect x="0" y="0" width="490" height="44.87" fill="#deedff" stroke="#6e6e6e" pointer-events="none"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 488px; height: 1px; padding-top: 22px; margin-left: 1px;">
|
||||
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">
|
||||
Titlebar (Navigation + Search)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="245" y="26" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
Titlebar (Navigation + Search)
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<rect x="0" y="44.87" width="126.73" height="185.13" fill="#f7f7f7" stroke="#6e6e6e" pointer-events="none"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 125px; height: 1px; padding-top: 137px; margin-left: 1px;">
|
||||
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">
|
||||
Sidebar (Navigation)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="63" y="141" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
Sidebar (Navigation)
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<rect x="0" y="226.67" width="490" height="33.33" fill="rgb(255, 255, 255)" stroke="#6e6e6e" pointer-events="none"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 488px; height: 1px; padding-top: 243px; margin-left: 1px;">
|
||||
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">
|
||||
Footer (Breadcrumbs)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="245" y="247" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
Footer (Breadcrumbs)
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<rect x="371.72" y="14.87" width="101.38" height="16.67" rx="2.5" ry="2.5" fill="rgb(255, 255, 255)" stroke="#6e6e6e" pointer-events="none"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 99px; height: 1px; padding-top: 23px; margin-left: 373px;">
|
||||
<div data-drawio-colors="color: #262626; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(38, 38, 38); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">
|
||||
Search
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="422" y="27" fill="#262626" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
Search
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 32px; height: 1px; padding-top: 23px; margin-left: 19px;">
|
||||
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: left;">
|
||||
<div style="display: inline-block; font-size: 20px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">
|
||||
<font color="#262626">
|
||||
Title
|
||||
</font>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="19" y="29" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="20px">
|
||||
Tit...
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
</g>
|
||||
<switch>
|
||||
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>
|
||||
<a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank">
|
||||
<text text-anchor="middle" font-size="10px" x="50%" y="100%">
|
||||
Text is not SVG - cannot display
|
||||
</text>
|
||||
</a>
|
||||
</switch>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.5 KiB |
102
docs/img/theme-variants-sidebar-only.drawio.svg
Normal file
@ -0,0 +1,102 @@
|
||||
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="491px" height="261px" viewBox="-0.5 -0.5 491 261" content="<mxfile><diagram id="6E4AiNPWWr3a8GvC3Ypl" name="Page-1">xZZNj5swEIZ/DdL2gjAGA8cmm7SX9pJKPTvBAasGU8f56q/vOJjFLOxqpbC7WIrMO/5gnhlP7OFldfmmaFP+kDkTXhjkFw8/emGYogR+jXBthSRJW6FQPG8l1Asb/o9ZMbDqkefsMBiopRSaN0NxJ+ua7fRAo0rJ83DYXorhrg0t2EjY7KgYq795rkvrVhz0+nfGi7LbGQXWUtFusBUOJc3l2ZHwysNLJaVue9VlyYRh13Fp561fsD59mGK1ftOErJ1xouJonbMfpq+dt4WSx2a8sN3rxJRmlynsdNut0HsGGcFkxbS6wrhzzy7KLJDS4RYSK1Ibr+Jpbu8SdKxX0x6+wUHwr86ZGR94eHEuuWabhu6M9Qz5C1qpK1j/EUH3oJX8w5ZSSHWbjcnKNLBMIMpeYPRJLLrz48BYylqb752XyerW7mXSWbPUj8Osf2z1sMUDEZ9YxYEIs3xCJjhi7GM8A0o0QrmBorSlyjPpTgTst9jCCylM7+EX14DWmIBJ8JOeeEE1l/WX+8jvuRAO93Vi2odkKQqJn4TvlajhiO4aaiIzcB8WitF8p45Vc7iT3jtRuriEIoSTDKMARzEe5C2Qggx1n1ESY4L9MIlTEgYkzUgcj3Hf0tl9yAz08Ti3GVW78hXa6B7aeyhCjh4S02aLApQHJwrpsHpEaBiEcSVBAfK7yVEK4cjGUUDPQ5nOEIVo4p+rrSuG1+1/tkNG/h7NjWHRw3OktgLZAtQuAHu3a7S2UVyBrX4teLWs2bPqYyUqeFHDq2B7s4KJE4eL01crVzzPzSaTuTI8u+YD7dUvDD4mGRCaDqN7JLHv5kJKJs7kDNkAr/0l8GZzbtJ49R8=</diagram></mxfile>">
|
||||
<defs/>
|
||||
<g>
|
||||
<rect x="0" y="0" width="490" height="260" fill="rgb(255, 255, 255)" stroke="#6e6e6e" pointer-events="none"/>
|
||||
<rect x="198.53" y="16.67" width="219.66" height="233.33" fill="rgb(255, 255, 255)" stroke="#e3e3e3" pointer-events="none"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 218px; height: 1px; padding-top: 133px; margin-left: 200px;">
|
||||
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">
|
||||
Content
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="308" y="137" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
Content
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<rect x="0" y="0" width="126.72" height="260" fill="#f7f7f7" stroke="#6e6e6e" pointer-events="none"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 125px; height: 1px; padding-top: 130px; margin-left: 1px;">
|
||||
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">
|
||||
Sidebar
|
||||
<br/>
|
||||
(Title + Navigation)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="63" y="134" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
Sidebar...
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<rect x="126.72" y="226.67" width="363.28" height="33.33" fill="rgb(255, 255, 255)" stroke="#6e6e6e" pointer-events="none"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 361px; height: 1px; padding-top: 243px; margin-left: 128px;">
|
||||
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">
|
||||
Footer (Breadcrumbs)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="308" y="247" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
Footer (Breadcrumbs)
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<rect x="12.67" y="41.67" width="101.38" height="16.67" rx="2.5" ry="2.5" fill="rgb(255, 255, 255)" stroke="#6e6e6e" pointer-events="none"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 99px; height: 1px; padding-top: 50px; margin-left: 14px;">
|
||||
<div data-drawio-colors="color: #262626; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(38, 38, 38); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">
|
||||
Search
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="63" y="54" fill="#262626" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
Search
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 32px; height: 1px; padding-top: 20px; margin-left: 15px;">
|
||||
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: left;">
|
||||
<div style="display: inline-block; font-size: 20px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">
|
||||
<font color="#262626">
|
||||
Title
|
||||
</font>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="15" y="26" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="20px">
|
||||
Tit...
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
</g>
|
||||
<switch>
|
||||
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>
|
||||
<a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank">
|
||||
<text text-anchor="middle" font-size="10px" x="50%" y="100%">
|
||||
Text is not SVG - cannot display
|
||||
</text>
|
||||
</a>
|
||||
</switch>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.2 KiB |
245
docs/llm.md
Normal file
@ -0,0 +1,245 @@
|
||||
# LLM Development Workflow Commands
|
||||
|
||||
This document outlines the common `npm` commands used during development and testing of the Cassandra firmware, categorized by their function.
|
||||
|
||||
## Application Architecture and Adding New Components
|
||||
|
||||
The firmware application is primarily managed by the `PHApp` class ([`src/PHApp.h`](../src/PHApp.h), [`src/PHApp.cpp`](../src/PHApp.cpp)). It acts as the central coordinator for various hardware interactions and communication protocols.
|
||||
|
||||
**Key Concepts:**
|
||||
|
||||
* **`PHApp`**: The main application class inheriting from `App`. It initializes and manages all other functional units (Components).
|
||||
* **`Component`**: A base class ([`src/Component.h`](../src/Component.h)) for modular hardware abstractions (e.g., relays, sensors, potentiometers). Each specific component type (like `Relay`, `POT`) inherits from `Component`.
|
||||
* **`ModbusManager`**: Handles Modbus communication ([`src/ModbusManager.h`](../src/ModbusManager.h)). `PHApp` creates and owns the `ModbusManager` instance.
|
||||
* **Component Registration**: Components that need to expose data via Modbus are registered with the `ModbusManager` instance within `PHApp`. This typically happens in `PHApp::setup()` or `PHApp::setupModbus()`.
|
||||
* **Network Values**: Components implement `readNetworkValue(address)` and `writeNetworkValue(address, value)` methods. The `ModbusManager` calls these methods when Modbus requests target the addresses registered for that component.
|
||||
|
||||
**Steps to Add a New Component:**
|
||||
|
||||
1. **Define Modbus Addresses:** Add necessary Modbus register/coil addresses for your new component in [`src/config-modbus.h`](../src/config-modbus.h).
|
||||
2. **Create Component Class:**
|
||||
* Create new header (`.h`) and source (`.cpp`) files for your component (e.g., [`src/MyNewSensor.h`](../src/MyNewSensor.h), [`src/MyNewSensor.cpp`](../src/MyNewSensor.cpp)).
|
||||
* In the header, define a class that inherits from `Component`.
|
||||
* Include necessary headers (e.g., `Component.h`).
|
||||
* Declare constructor, `setup()`, `loop()`, `readNetworkValue()`, `writeNetworkValue()`, and any other required methods.
|
||||
3. **Implement Component Logic:**
|
||||
* In the source file (`.cpp`), implement the methods declared in the header.
|
||||
* `setup()`: Initialize hardware, pins, etc.
|
||||
* `loop()`: Read sensor values, update internal state, etc.
|
||||
* `readNetworkValue()`: Return the component's state based on the requested Modbus `address`.
|
||||
* `writeNetworkValue()`: Update the component's state based on the requested Modbus `address` and `value`. Return `E_OK` on success or an error code (from [`enums.h`](../src/enums.h)) on failure.
|
||||
4. **Integrate into `PHApp`:**
|
||||
* In `PHApp.h`, include the header for your new component.
|
||||
* In `PHApp.h`, declare a pointer to your new component class as a member variable (e.g., `MyNewSensor* mySensor;`).
|
||||
* In `PHApp::setup()`:
|
||||
* Instantiate your component (e.g., `mySensor = new MyNewSensor(/* constructor args */);`).
|
||||
* Call your component's `setup()` method (e.g., `mySensor->setup();`).
|
||||
* If the component uses Modbus, register it with the `ModbusManager`: `modbusManager->registerComponent(mySensor, MY_SENSOR_START_ADDRESS, MY_SENSOR_REGISTER_COUNT);` (using addresses from `config-modbus.h`).
|
||||
5. **Cleanup:** In the `PHApp` destructor (`~PHApp()`), delete the component instance (`delete mySensor;`).
|
||||
6. **Build:** Add your new `.cpp` file to the build system if necessary (PlatformIO typically picks up source files in `src` automatically). Run `npm run build` to compile.
|
||||
|
||||
## Key Packages and Libraries
|
||||
|
||||
This project relies on several key software packages and libraries:
|
||||
|
||||
* **PlatformIO**: The core build system and development ecosystem used to manage the toolchain, libraries, building, and uploading for the ESP32.
|
||||
* **Arduino Framework (for ESP32)**: Provides a simplified C++ API for interacting with the ESP32 hardware (GPIO, Serial, WiFi, etc.). It runs on top of ESP-IDF.
|
||||
* **ESP-IDF (Espressif IoT Development Framework)**: The official low-level development framework from Espressif, providing the drivers and core system functionalities.
|
||||
* **npm**: Used as a task runner to execute build, test, and utility scripts defined in `package.json`.
|
||||
* **Python**: Used for various helper scripts (located in the `scripts/` directory) for testing, sending commands, and interacting with the device over serial, Modbus, or HTTP.
|
||||
* **C++ Libraries (managed by PlatformIO):**
|
||||
* **`ArduinoJson`**: For efficient JSON serialization and deserialization, used heavily in the REST API.
|
||||
* **`ESPAsyncWebServer` / `AsyncTCP`**: Libraries providing asynchronous handling of HTTP requests and TCP connections, forming the basis of the REST API and WebSocket communication.
|
||||
* **`eModbus`**: Handles Modbus RTU/TCP client and server communication.
|
||||
* **`ArduinoLog`**: Provides flexible logging capabilities, including log levels and output redirection (used here for both Serial and the internal log buffer).
|
||||
* **`LittleFS`**: A lightweight filesystem used to store web assets (HTML, CSS, etc.) on the ESP32's flash memory.
|
||||
* **`WiFi` / `ESPmDNS`**: Standard Arduino/ESP32 libraries for network connectivity and service discovery.
|
||||
* **`Vector`**: A C++ standard library container used for dynamic arrays (like the log buffer).
|
||||
|
||||
## Example Scenarios
|
||||
|
||||
Here are some common development workflows and the minimal sequence of commands to execute them:
|
||||
|
||||
**Scenario 1: Making a change to core C++ code (e.g., in `PHApp.cpp` or a `Component`)**
|
||||
|
||||
1. Make your code changes.
|
||||
2. Compile and upload the new firmware:
|
||||
```bash
|
||||
npm run upload
|
||||
```
|
||||
3. Monitor the device for logs/output:
|
||||
```bash
|
||||
npm run build:monitor
|
||||
```
|
||||
(Press Ctrl+C to exit the monitor)
|
||||
|
||||
**Scenario 2: Adding a new Component (`MyNewSensor`)**
|
||||
|
||||
1. Follow the steps in "Steps to Add a New Component" above (define addresses, create class, implement logic, integrate into `PHApp`, cleanup).
|
||||
2. Compile and upload the new firmware:
|
||||
```bash
|
||||
npm run upload
|
||||
```
|
||||
3. Test the new component (e.g., using Modbus or serial commands):
|
||||
```bash
|
||||
# Example: Read a holding register associated with the new component
|
||||
npm run modbus:read:holding -- --address <YOUR_NEW_COMPONENT_ADDRESS>
|
||||
|
||||
# Monitor serial output
|
||||
npm run build:monitor
|
||||
```
|
||||
|
||||
**Scenario 3: Changing the web UI (e.g., editing `front/index.html` or `front/styles.css`)**
|
||||
|
||||
1. Make your changes to the files in the `front/` directory.
|
||||
2. Rebuild the web assets and the firmware:
|
||||
```bash
|
||||
npm run web:build
|
||||
```
|
||||
3. Upload the updated filesystem image:
|
||||
```bash
|
||||
npm run web:uploadfs
|
||||
```
|
||||
4. Upload the firmware (optional, but good practice if `web:build` included firmware changes):
|
||||
```bash
|
||||
npm run upload
|
||||
```
|
||||
5. Access the web UI in your browser (e.g., `http://modbus-esp32.local`) and hard refresh (Ctrl+Shift+R or Cmd+Shift+R) to see changes.
|
||||
|
||||
**Scenario 4: Testing a specific Modbus register**
|
||||
|
||||
1. Read the register:
|
||||
```bash
|
||||
npm run modbus:read:holding -- --address <REGISTER_ADDRESS>
|
||||
```
|
||||
2. Write to the register:
|
||||
```bash
|
||||
npm run modbus:write:holding -- --address <REGISTER_ADDRESS> --value <NEW_VALUE>
|
||||
```
|
||||
3. Read again to verify:
|
||||
```bash
|
||||
npm run modbus:read:holding -- --address <REGISTER_ADDRESS>
|
||||
```
|
||||
|
||||
**Scenario 5: Debugging with logs**
|
||||
|
||||
1. Ensure the device is running and connected to the network.
|
||||
2. Fetch logs via the REST API:
|
||||
```bash
|
||||
npm run debug:serial
|
||||
```
|
||||
3. Alternatively, view live serial output:
|
||||
```bash
|
||||
npm run build:monitor
|
||||
```
|
||||
|
||||
## Core Code / Build
|
||||
|
||||
Commands related to basic compilation, cleaning, and uploading the main firmware code.
|
||||
|
||||
* `npm run build`: Compiles the firmware using PlatformIO (`pio run`).
|
||||
* `npm run build:clean`: Cleans the build artifacts (`pio run -t clean`).
|
||||
* `npm run upload`: Compiles and uploads the firmware via the default upload method (`pio run -t upload`).
|
||||
* `npm run build:run`: A full cycle: compiles, uploads, and opens the serial monitor (`pio run && pio run -t upload && pio device monitor`).
|
||||
|
||||
## Web Assets
|
||||
|
||||
Commands related to generating, building, and uploading the web interface assets (HTML, CSS, Swagger).
|
||||
|
||||
* `npm run web:gen-swagger`: Generates the `swagger_content.h` header file from `swagger.yaml`.
|
||||
* `npm run web:build`: Generates web assets (Swagger header, HTML/CSS headers) and then runs the main firmware build (`npm run build`).
|
||||
* `npm run web:uploadfs`: Uploads the LittleFS filesystem image containing web assets (`pio run -t uploadfs`). Use after `web:build` to update the UI.
|
||||
|
||||
## Modbus
|
||||
|
||||
Commands specifically for interacting with or testing Modbus functionality.
|
||||
|
||||
* `npm run modbus:read:coil`: Reads Modbus coils.
|
||||
* `npm run modbus:read:holding`: Reads Modbus holding registers.
|
||||
* `npm run modbus:write:coil`: Writes to a Modbus coil.
|
||||
* `npm run modbus:write:holding`: Writes to a Modbus holding register.
|
||||
* `npm run test:modbus:counter:read`: Reads the battle counter value via Modbus.
|
||||
* `npm run test:modbus:counter:watch`: Continuously reads the battle counter value via Modbus.
|
||||
* `npm run test:modbus:counter:reset`: Resets the battle counter via Modbus.
|
||||
* `npm run test:modbus:counter:increment`: Increments the battle counter via Modbus.
|
||||
|
||||
## Serial Communication
|
||||
|
||||
Commands for interacting with the device via the serial port.
|
||||
|
||||
* `npm run serial:send-cmd`: Sends a specific, hardcoded `printRegisters` command via serial.
|
||||
* `npm run serial:send`: Sends a generic message defined in `scripts/send_message.py` via serial.
|
||||
* `npm run test:serial:counter:increment`: Sends a command to increment the test counter via serial.
|
||||
* `npm run test:serial:counter:reset`: Sends a command to reset the test counter via serial.
|
||||
* `npm run test:serial:counter:get`: Sends a command to get the test counter value via serial.
|
||||
* `npm run test:serial:client-stats:reset`: Resets Modbus client statistics via serial command.
|
||||
* `npm run test:serial:client-stats:get`: Gets Modbus client statistics via serial command.
|
||||
|
||||
## REST API
|
||||
|
||||
Commands for testing the REST API endpoints.
|
||||
|
||||
* `npm run test:api`: Runs a general REST API test suite.
|
||||
* `npm run test:api:ip`: Runs the general API test suite against a specific IP.
|
||||
* `npm run test:api:working`: Runs a basic "is the API working" test.
|
||||
* `npm run test:api:system-info`: Tests the `/api/v1/system/info` endpoint.
|
||||
* `npm run test:api:system-info:ip`: Tests the system info endpoint against a specific IP.
|
||||
* `npm run test:api:logs`: Tests the `/api/v1/system/logs` endpoint.
|
||||
* `npm run test:api:logs:ip`: Tests the logs endpoint against a specific IP.
|
||||
|
||||
## Performance / Stress Testing
|
||||
|
||||
Commands explicitly designed for performance or stress testing.
|
||||
|
||||
* `npm run test:serial:rate`: Tests the rate of serial command processing.
|
||||
* `npm run test:serial:stress`: Performs a stress test with varying delays for serial commands.
|
||||
* `npm run test:modbus:battle`: Runs a Modbus read/write battle test.
|
||||
* `npm run test:modbus:battle:quick`: Runs a shorter version of the battle test.
|
||||
* `npm run test:modbus:battle:full`: Runs a longer, more intensive version of the battle test.
|
||||
* `npm run test:modbus:multi-client`: Tests handling multiple concurrent Modbus clients.
|
||||
* `npm run test:modbus:multi-client:low`: Runs the multi-client test with fewer clients/operations.
|
||||
* `npm run test:modbus:multi-client:high`: Runs the multi-client test with more clients/operations.
|
||||
* `npm run test:modbus:client-sequential`: Tests sequential connections from multiple Modbus clients.
|
||||
* `npm run test:modbus:client-sequential:max`: Tests sequential connections with the maximum configured clients.
|
||||
* `npm run test:modbus:longpoll`: Runs a long polling test for Modbus.
|
||||
|
||||
## Debugging / Monitoring
|
||||
|
||||
Commands useful for monitoring the device and diagnosing issues.
|
||||
|
||||
* `npm run build:monitor`: Opens the PlatformIO serial monitor (`pio device monitor`).
|
||||
* `npm run debug:serial`: Fetches and prints the buffered logs from the device's REST API (`/api/v1/system/logs`).
|
||||
* `npm run test:modbus:counter:watch`: Continuously reads the battle counter value via Modbus.
|
||||
|
||||
## Documentation
|
||||
|
||||
Commands related to generating documentation.
|
||||
|
||||
* `npm run docs:generate`: Generates code documentation using Doxygen.
|
||||
|
||||
## General
|
||||
|
||||
* `npm test`: Placeholder script, currently does nothing specific (`echo "Error: no test specified" && exit 1`).
|
||||
*
|
||||
|
||||
6. **Build:** Add your new `.cpp` file to the build system if necessary (PlatformIO typically picks up source files in `src` automatically). Run `npm run build` to compile.
|
||||
|
||||
## Key Packages and Libraries
|
||||
|
||||
This project relies on several key software packages and libraries:
|
||||
|
||||
* **PlatformIO**: The core build system and development ecosystem used to manage the toolchain, libraries, building, and uploading for the ESP32.
|
||||
* **Arduino Framework (for ESP32)**: Provides a simplified C++ API for interacting with the ESP32 hardware (GPIO, Serial, WiFi, etc.). It runs on top of ESP-IDF.
|
||||
* **ESP-IDF (Espressif IoT Development Framework)**: The official low-level development framework from Espressif, providing the drivers and core system functionalities.
|
||||
* **npm**: Used as a task runner to execute build, test, and utility scripts defined in `package.json`.
|
||||
* **Python**: Used for various helper scripts (located in the `scripts/` directory) for testing, sending commands, and interacting with the device over serial, Modbus, or HTTP.
|
||||
* **C++ Libraries (managed by PlatformIO):**
|
||||
* **`ArduinoJson`**: For efficient JSON serialization and deserialization, used heavily in the REST API.
|
||||
* **`eModbus`**: Handles Modbus RTU/TCP client and server communication.
|
||||
* **`ArduinoLog`**: Provides flexible logging capabilities, including log levels and output redirection (used here for both Serial and the internal log buffer).
|
||||
* **`LittleFS`**: A lightweight filesystem used to store web assets (HTML, CSS, etc.) on the ESP32's flash memory.
|
||||
* **`WiFi` / `ESPmDNS`**: Standard Arduino/ESP32 libraries for network connectivity and service discovery.
|
||||
* **`Vector`**: A C++ standard library container used for dynamic arrays (like the log buffer).
|
||||
|
||||
## Example Scenarios
|
||||
// ... existing code ...
|
||||
1
docs/logo.drawio.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="61px" height="74px" viewBox="-0.5 -0.5 61 74" content="<mxfile host="drawio-plugin" modified="2021-03-16T23:58:23.462Z" agent="5.0 (Macintosh; Intel Mac OS X 10_16_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36" version="13.7.9" etag="JoeaGLJ54FcERO7YrWLQ" type="embed"><diagram id="JMB9aH8b_oZ7EWDuqJgx" name="Page-1">7VdNc5swEP01HDsjkGPDsSVJe+lMZnzoWYENaAwsI8ux6a+vCCtA4KSu62kmSS+M9LT7tB9P0uDxuDx8VaLOv2MKhRew9ODxay8Igigy3xZoCOC8AzIl0w7yB2AtfwKBjNCdTGHrGGrEQsvaBROsKki0gwmlcO+aPWDh7lqLDGbAOhHFHP0hU513aHjFBvwbyCy3O/uMVkphjQnY5iLF/QjiNx6PFaLuRuUhhqKtna1L53f7zGofmIJKn+RAcTyKYkfJUWC6sdlmCnc1mYHScDhWY3Fvzdk8Br/PzCgCsAStGmNCRJy2JDH4pIV8VMG+edS4rCcZcjMDSu+ZVP3fpwpV+rnVh5ndF5hsPP4l16VhvPbN8AErTWI0re7mMRaonpw5Y8tlHBvcsNzKwnpttVDaslZYgcXIhj3NFW56LS1bbrM44l6m4Wq5MLhxzEDfgZKmAKDWtUhklRFNgqVM7LYb0Enu8I9j9dkVC80KtgS6Lb3fGnYVgXSm/1Ez2fFu7oeTYA/CuIUWU1AILR9d/mN9pR3uUJqde7F88leOWhYLl2GLO5UAOY2FP+GxMm3c6CwNlXlKY9oompFZ3Rps59EOkuw8BoH2BTtNs8EfaZbUdYZkXQGuXhDgR9DYRBycXURj00D+UmMT2ktJLnr9B8HG0IzFcPkHYfUe3oPZqfOjMEiDs1+KEw5n9P/+/1f3f/gq1394lt7erqQ+0HVvpsPPRWc+/KHxm18=</diagram></mxfile>"><defs/><g><path d="M 13 57 L 13.01 57.01 L 15.87 50.14 L 18.37 43.14 L 20.91 36.15 L 23.67 29.25 L 26.4 22.33 Q 30 13 33.71 22.28 L 33.55 22.22 L 35.48 26.91 L 37.49 31.64 L 39.48 36.36 L 41.2 40.97 L 43.05 45.63" fill="none" stroke="#010508" stroke-opacity="0.1" stroke-width="6" stroke-linejoin="round" stroke-linecap="round" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 47.51 56.77 L 47.65 56.93 L 45.43 54.91 L 43.41 53.11 L 41.43 51.35 L 39.63 49.8 L 37.48 47.86 L 37.39 47.64 L 39.79 47.17 L 41.9 45.98 L 44.24 45.37 L 46.48 44.52 L 48.62 43.4 L 48.54 43.39 L 48.58 46.09 L 48.04 48.74 L 48.04 51.43 L 47.8 54.1 L 47.51 56.77 Z Z" fill-opacity="0.1" fill="#010508" stroke="#010508" stroke-opacity="0.1" stroke-width="6" stroke-linejoin="round" stroke-linecap="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 10 43 L 9.94 42.88 L 12.16 41.98 L 14.31 40.96 L 16.51 40.01 L 18.62 38.89 L 20.88 38.1 Q 30 34 40 34 L 40 33.75 L 42 33.83 L 44 33.8 L 46 33.79 L 48 34.05 L 50 34" fill="none" stroke="#010508" stroke-opacity="0.1" stroke-width="7" stroke-linejoin="round" stroke-linecap="round" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 10 54 L 9.97 53.99 L 12.69 47.07 L 15.43 40.16 L 18.07 33.21 L 20.65 26.24 L 23.4 19.33 Q 27 10 30.71 19.28 L 30.66 19.26 L 32.46 23.91 L 34.55 28.66 L 36.26 33.27 L 38.35 38.03 L 40.05 42.63" fill="none" stroke="#1982d2" stroke-width="6" stroke-linejoin="round" stroke-linecap="round" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 44.51 53.77 L 44.56 53.83 L 42.48 51.97 L 40.5 50.21 L 38.48 48.41 L 36.41 46.56 L 34.48 44.86 L 34.55 45.02 L 36.72 44 L 39 43.24 L 41.21 42.28 L 43.48 41.51 L 45.62 40.4 L 45.78 40.42 L 45.51 43.09 L 45.01 45.74 L 44.87 48.42 L 44.94 51.12 L 44.51 53.77 Z Z" fill="#1982d2" stroke="#1982d2" stroke-width="6" stroke-linejoin="round" stroke-linecap="round" stroke-miterlimit="10" pointer-events="all"/><path d="M 7 40 L 7.02 40.05 L 9.28 39.25 L 11.33 38 L 13.48 36.96 L 15.73 36.14 L 17.88 35.1 Q 27 31 37 31 L 37 30.79 L 39 31.11 L 41 30.85 L 43 30.78 L 45 30.89 L 47 31" fill="none" stroke="#1982d2" stroke-width="8" stroke-linejoin="round" stroke-linecap="round" stroke-miterlimit="10" pointer-events="stroke"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
142
docs/manual.md
Normal file
@ -0,0 +1,142 @@
|
||||
# Manual Mode Refactor Proposal
|
||||
|
||||
**Status:** Implemented
|
||||
**Feature:** Enhanced Manual Mode Safety & Interlock Support
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Currently, `PressCylinder::onJoystickUp` implements a simplified manual control loop that bypasses safety checks (`CFlags`) and complex logic available in `loopSingle` and `loopMulti`. It directly enables solenoids based on Joystick Position, ignoring:
|
||||
|
||||
* Load balancing (in multi-cylinder setups)
|
||||
* Stall detection
|
||||
* Interlock status (implicitly, though safety overrides exist)
|
||||
* Operation timeouts
|
||||
|
||||
The goal is to unify the logic so that "Manual" operation is simply a special case of the robust `loop*` functions, but controlled by the Joystick/PushButton rather than a fixed "Auto" process.
|
||||
|
||||
## Proposed Strategy
|
||||
|
||||
We will treat Manual operation as "Auto operation towards a Virtual Maximum Target" that only persists while the operator holds the input (Joystick or PushButton).
|
||||
|
||||
### 1. Unified Control Logic
|
||||
|
||||
Instead of direct `SOLENOID_ON/OFF` in `onJoystickUp`, we will:
|
||||
|
||||
1. Detect "Active Input" (Joystick UP *OR* PushButton Pressed).
|
||||
2. If Active:
|
||||
* Temporarily override `m_targetSP` to `100%` (or a `manual_override_sp`).
|
||||
* Set effective mode to `DEFAULT_AUTO_MODE_NORMAL` (if single/unlocked) or `DEFAULT_AUTO_MODE_INTERLOCKED` (if interlocked).
|
||||
* Call `loopSingle()` or `loopMulti()` respectively.
|
||||
3. If Inactive:
|
||||
* Reset `m_targetSP` to 0 (or restore original).
|
||||
* Enforce `SOLENOIDS_OFF`.
|
||||
|
||||
### 2. Implementation Plan
|
||||
|
||||
#### A. Define Helper: `getVirtualTargetSP()`
|
||||
|
||||
For manual moves, we want to press as long as the operator commands, up to the absolute hardware limit (`maxload_threshold` or `100%`).
|
||||
|
||||
```cpp
|
||||
uint32_t PressCylinder::getVirtualTargetSP() {
|
||||
// In Manual mode, "Target" is effectively "Go until Max or Stop"
|
||||
return 70; // 70% of maxload_threshold (Virtual SP Max)
|
||||
}
|
||||
```
|
||||
|
||||
#### B. Refactor `onJoystickUp` / Main Loop Integration
|
||||
|
||||
We will stop using `onJoystickUp` as a direct actuator. Instead, it becomes an input state detector.
|
||||
|
||||
**Modified `loop()` Logic:**
|
||||
|
||||
```cpp
|
||||
// 1. Determine "Effective Mode" and "Effective Target"
|
||||
E_Mode effective_mode = (E_Mode)m_mode.getValue();
|
||||
uint32_t effective_sp = m_targetSP.getValue();
|
||||
bool manual_input_active = false;
|
||||
|
||||
// Check Inputs (Joystick or PushButton)
|
||||
// Check Inputs (Joystick or PushButton)
|
||||
if (_pushButton->getState() == PushButton::State::PRESSED || _pushButton->getState() == PushButton::State::HELD) {
|
||||
// Condition 1: PushButton Held -> Force Manual Single Cylinder
|
||||
manual_input_active = true;
|
||||
effective_mode = MODE_MANUAL;
|
||||
effective_sp = 70;
|
||||
}
|
||||
else if (_joystick->getPosition() == Joystick::E_POSITION::UP) {
|
||||
// Condition 2: Joystick UP -> Smart Auto / Interlock Dependent
|
||||
manual_input_active = true;
|
||||
effective_sp = 70;
|
||||
|
||||
if (effective_mode == MODE_MANUAL || effective_mode == MODE_MANUAL_MULTI) {
|
||||
if (m_interlocked.getValue()) {
|
||||
effective_mode = DEFAULT_AUTO_MODE_INTERLOCKED; // e.g. AUTO_MULTI_BALANCED
|
||||
} else {
|
||||
effective_mode = DEFAULT_AUTO_MODE_NORMAL; // e.g. AUTO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Dispatch to Control Loops
|
||||
if (manual_input_active || isAutoRunning()) {
|
||||
// These functions now use 'effective_mode' and 'effective_sp'
|
||||
// instead of reading m_targetSP/m_mode directly if we pass them as args,
|
||||
// OR we temporarily override member vars (simpler but riskier state).
|
||||
|
||||
// BETTER APPROACH: Refactor loopSingle/loopMulti to take args
|
||||
if (isMultiMode(effective_mode)) {
|
||||
loopMulti(effective_mode, effective_sp);
|
||||
} else {
|
||||
loopSingle(effective_mode, effective_sp);
|
||||
}
|
||||
} else {
|
||||
// Idle / Stop
|
||||
SOLENOIDS_OFF();
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Modifications Required
|
||||
|
||||
1. **Refactor `loopSingle` / `loopMulti`**:
|
||||
* Change signature to accept `E_Mode mode` and `uint16_t target_sp` arguments.
|
||||
* Remove internal calls to `m_mode.getValue()` and `m_targetSP.getValue()` in favor of these arguments.
|
||||
|
||||
2. **Update `onJoystickUp`**:
|
||||
* Actually, we might deprecate `onJoystickUp` direct logic entirely and move it into the main `loop()` structure as shown above.
|
||||
* Retain `onJoystickDoubleUp` for the HOLD feature (capturing Load to SP).
|
||||
|
||||
3. **PushButton Integration**:
|
||||
* Map `_pushButton->getState() == PushButton::State::PRESSED` (or HELD) to `manual_input_active`.
|
||||
|
||||
### 4. Safety Considerations
|
||||
|
||||
* **Deadman Switch**: The moment the Joystick/Button is released, `manual_input_active` becomes false, and the `else` block triggers `SOLENOIDS_OFF()`.
|
||||
* **Existing Safety**: `loopSingle/Multi` already contain `CheckMinLoad`, `Stall`, `Balance`, and `MaxTime` checks. By routing manual control through them, we gain all these features automatically.
|
||||
* **Virtual SP**: Setting SP to 100% is safe because `loop*` logic checks `canPress()` which respects `maxload_threshold`.
|
||||
|
||||
## Diagram: New Flow
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Input[Inputs: Joystick / Button] --> Check{Active?}
|
||||
Check -- Yes --> MapMode[Map to Effective Auto Mode]
|
||||
MapMode --> SetSP[Set Virtual SP = 70%]
|
||||
SetSP --> Dispatch{Is Multi?}
|
||||
|
||||
Dispatch -- Single --> LoopSingle[loopSingle(SafeMode, 70%)]
|
||||
Dispatch -- Multi --> LoopMulti[loopMulti(SafeMode, 70%)]
|
||||
|
||||
LoopSingle --> Actuators
|
||||
LoopMulti --> Actuators
|
||||
|
||||
Check -- No --> Stop[SOLENOIDS_OFF]
|
||||
```
|
||||
|
||||
### References
|
||||
|
||||
### Data
|
||||
|
||||
cs-1150 - interlocked
|
||||
|
||||
min load : 50-150
|
||||
181
docs/mb-lang.md
Normal file
@ -0,0 +1,181 @@
|
||||
# Modbus Logic Language (mb-lang)
|
||||
|
||||
This document describes the design and usage of the simple logic engine configurable via Modbus TCP.
|
||||
|
||||
## Purpose
|
||||
|
||||
The Modbus Logic Engine allows users to define simple conditional automation rules directly by writing to specific Modbus holding registers. This enables basic automation sequences like "if sensor value X exceeds Y, then turn on relay Z" without modifying the core firmware code.
|
||||
|
||||
## Architecture
|
||||
|
||||
A dedicated component, `ModbusLogicEngine`, runs within the firmware.
|
||||
- It exposes a block of Modbus Holding Registers for configuration and status.
|
||||
- It periodically evaluates the enabled rules.
|
||||
- It can read the state of other Modbus registers/coils.
|
||||
- It can perform actions like writing to Modbus registers/coils or calling pre-defined methods on other firmware components.
|
||||
- It updates status registers after each rule evaluation/action attempt.
|
||||
|
||||
## Configuration and Status
|
||||
|
||||
The engine supports a fixed number of logic rules, defined by `MAX_LOGIC_RULES` (e.g., 8). Each rule is configured and monitored using a block of `REGISTERS_PER_RULE` (now 13) consecutive Holding Registers.
|
||||
|
||||
- **Base Address:** `MODBUS_LOGIC_RULES_START` (Defined in `config-modbus.h`)
|
||||
- **Rule N Address:** `MODBUS_LOGIC_RULES_START + (N * REGISTERS_PER_RULE)` where `N` is the rule index (0 to `MAX_LOGIC_RULES - 1`).
|
||||
|
||||
### Register Map per Rule (N)
|
||||
|
||||
| Offset | Register Name | Address Offset | R/W | Description | Notes |
|
||||
| :----- | :---------------------------- | :-------------------------------- | :-- | :--------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------- |
|
||||
| +0 | `Rule_N_Enabled` | `Base + (N*13) + 0` | R/W | **Enable/Disable Rule:** 0 = Disabled, 1 = Enabled | Rules are ignored if disabled. |
|
||||
| +1 | `Rule_N_Cond_Src_Type` | `Base + (N*13) + 1` | R/W | **Condition Source Type:** 0 = Holding Register, 1 = Coil | Specifies what type of Modbus item the condition reads. |
|
||||
| +2 | `Rule_N_Cond_Src_Addr` | `Base + (N*13) + 2` | R/W | **Condition Source Address:** Modbus address of the register/coil to read for the condition. | The address of the data point to check. |
|
||||
| +3 | `Rule_N_Cond_Operator` | `Base + (N*13) + 3` | R/W | **Condition Operator:** 0=`==`, 1=`!=`, 2=`<`, 3=`<=`, 4=`>`, 5=`>=` | The comparison to perform. |
|
||||
| +4 | `Rule_N_Cond_Value` | `Base + (N*13) + 4` | R/W | **Condition Value:** The value to compare the source against. | For Coils, use 0 for OFF and 1 for ON. |
|
||||
| +5 | `Rule_N_Action_Type` | `Base + (N*13) + 5` | R/W | **Action Type:** 0=None, 1=Write Holding Reg, 2=Write Coil, 3=Call Component Method | Specifies what to do if the condition is true. |
|
||||
| +6 | `Rule_N_Action_Target` | `Base + (N*13) + 6` | R/W | **Action Target:** Modbus Address (for Write Reg/Coil) or Component ID (for Call Method) | Specifies *what* to act upon (Register Address, Coil Address, Component ID). |
|
||||
| +7 | `Rule_N_Action_Param1` | `Base + (N*13) + 7` | R/W | **Action Parameter 1:** Value (for Write Reg), ON/OFF (for Write Coil, 0=OFF, 1=ON), Method ID (for Call) | Specifies *how* to act upon the target. |
|
||||
| +8 | `Rule_N_Action_Param2` | `Base + (N*13) + 8` | R/W | **Action Parameter 2:** Argument 1 for `Call Component Method` | First argument for the component method. Ignored for Write actions. |
|
||||
| +9 | `Rule_N_Action_Param3` | `Base + (N*13) + 9` | R/W | **Action Parameter 3:** Argument 2 for `Call Component Method` | Second argument for the component method. Ignored for Write actions. |
|
||||
| +10 | `Rule_N_Last_Status` | `Base + (N*13) + 10` | R | **Last Action Status:** 0=Idle/OK, 1=Err Cond Read, 2=Err Action Write/Call, 3=Invalid Action Params | Reports the outcome of the last trigger attempt. (See Status Codes below) |
|
||||
| +11 | `Rule_N_Last_Trigger_Timestamp` | `Base + (N*13) + 11` | R | **Last Trigger Timestamp:** System time (e.g., seconds since boot) when rule last triggered. | 0 if never triggered. Wraps eventually. |
|
||||
| +12 | `Rule_N_Trigger_Count` | `Base + (N*13) + 12` | R | **Trigger Count:** Number of times this rule's action has been successfully triggered. | Wraps eventually. Can be reset by writing 0 via Modbus. |
|
||||
|
||||
*Note: `Base` refers to `MODBUS_LOGIC_RULES_START`. R=Read-Only, W=Writeable (from Modbus perspective). Status registers (+10 to +12) are updated internally but the count (+12) can potentially be reset via write.*
|
||||
|
||||
### Status Codes (`Rule_N_Last_Status`)
|
||||
|
||||
| Code | Meaning |
|
||||
| :--- | :-------------------------- |
|
||||
| 0 | Idle / Action OK |
|
||||
| 1 | Error Reading Condition Src |
|
||||
| 2 | Error Performing Action |
|
||||
| 3 | Invalid Action Parameters |
|
||||
| 4 | Component Method Call Failed|
|
||||
| ... | (Other specific errors TBD) |
|
||||
|
||||
## Actions
|
||||
|
||||
### 1. Write Holding Register
|
||||
|
||||
- **Action Type:** 1
|
||||
- **Action Target:** Modbus address of the Holding Register.
|
||||
- **Action Parameter 1:** Value to write.
|
||||
- **Action Parameters 2 & 3:** Ignored.
|
||||
|
||||
### 2. Write Coil
|
||||
|
||||
- **Action Type:** 2
|
||||
- **Action Target:** Modbus address of the Coil.
|
||||
- **Action Parameter 1:** Value to write (0 for OFF, 1 for ON). Other non-zero values may also be interpreted as ON.
|
||||
- **Action Parameters 2 & 3:** Ignored.
|
||||
|
||||
### 3. Call Component Method
|
||||
|
||||
- **Action Type:** 3
|
||||
- **Action Target:** The numeric `Component ID`.
|
||||
- **Action Parameter 1:** The numeric `Method ID`.
|
||||
- **Action Parameter 2:** The first integer argument (`arg1`).
|
||||
- **Action Parameter 3:** The second integer argument (`arg2`).
|
||||
|
||||
**Important:** Only specific, pre-registered methods can be called. Available Component/Method IDs need separate documentation.
|
||||
|
||||
## Execution Flow
|
||||
|
||||
1. The `ModbusLogicEngine` loops periodically.
|
||||
2. For each rule `N` from 0 to `MAX_LOGIC_RULES - 1`:
|
||||
a. Read `Rule_N_Enabled`. If 0, skip.
|
||||
b. Read condition parameters.
|
||||
c. Attempt to read the current value from `Cond_Src_Addr`.
|
||||
d. If read fails, update `Rule_N_Last_Status` (e.g., to 1) and skip to the next rule.
|
||||
e. Evaluate the condition.
|
||||
f. If the condition is TRUE:
|
||||
i. Read action parameters.
|
||||
ii. Attempt to perform the specified action.
|
||||
iii. Update `Rule_N_Last_Status` based on action success (0) or failure (e.g., 2, 3, 4).
|
||||
iv. If action was successful, increment `Rule_N_Trigger_Count` and update `Rule_N_Last_Trigger_Timestamp`.
|
||||
g. If the condition is FALSE, potentially reset `Rule_N_Last_Status` to 0 (Idle), unless it holds an error state.
|
||||
|
||||
## Example Scenarios
|
||||
|
||||
*(Addresses updated for REGISTERS_PER_RULE = 13)*
|
||||
|
||||
### Example 1: Turn on Relay 5 if Register 200 >= 100
|
||||
|
||||
Assume:
|
||||
- `MODBUS_LOGIC_RULES_START` = 1000
|
||||
- Rule Index `N = 0` (`Base = 1000`)
|
||||
- Relay 5 is mapped to Coil address 5
|
||||
- Register 200
|
||||
|
||||
Write:
|
||||
|
||||
| Register Address | Value | Meaning |
|
||||
| :--------------- | :---- | :------------------------ |
|
||||
| 1000 | 1 | Rule 0: Enabled |
|
||||
| 1001 | 0 | Cond Src Type: Reg |
|
||||
| 1002 | 200 | Cond Src Addr: 200 |
|
||||
| 1003 | 5 | Cond Operator: >= (5) |
|
||||
| 1004 | 100 | Cond Value: 100 |
|
||||
| 1005 | 2 | Action Type: Write Coil |
|
||||
| 1006 | 5 | Action Target: Coil Addr 5|
|
||||
| 1007 | 1 | Action Param 1: Value ON |
|
||||
| 1008 | 0 | Action Param 2: (Ignored) |
|
||||
| 1009 | 0 | Action Param 3: (Ignored) |
|
||||
|
||||
Read Status (after trigger):
|
||||
|
||||
| Register Address | Value | Meaning |
|
||||
| :--------------- | :------------ | :-------------------------- |
|
||||
| 1010 | 0 | Last Status: OK |
|
||||
| 1011 | e.g., 12345 | Last Trigger: Timestamp |
|
||||
| 1012 | e.g., 1 | Trigger Count: 1 |
|
||||
|
||||
### Example 2: Call `resetCounter()` Method on Component `StatsTracker` if Coil 10 is ON
|
||||
|
||||
Assume:
|
||||
- Rule Index `N = 1` (`Base = 1000 + 13 = 1013`)
|
||||
- Coil 10
|
||||
- Component `StatsTracker` ID = 5
|
||||
- Method `resetCounter` ID = 1 (takes no args)
|
||||
|
||||
Write:
|
||||
|
||||
| Register Address | Value | Meaning |
|
||||
| :--------------- | :---- | :---------------------------- |
|
||||
| 1013 | 1 | Rule 1: Enabled |
|
||||
| 1014 | 1 | Cond Src Type: Coil |
|
||||
| 1015 | 10 | Cond Src Addr: Coil 10 |
|
||||
| 1016 | 0 | Cond Operator: == (0) |
|
||||
| 1017 | 1 | Cond Value: ON (1) |
|
||||
| 1018 | 3 | Action Type: Call Method |
|
||||
| 1019 | 5 | Action Target: Component ID 5 |
|
||||
| 1020 | 1 | Action Param 1: Method ID 1 |
|
||||
| 1021 | 0 | Action Param 2: Arg1 = 0 |
|
||||
| 1022 | 0 | Action Param 3: Arg2 = 0 |
|
||||
|
||||
Read Status (after trigger):
|
||||
|
||||
| Register Address | Value | Meaning |
|
||||
| :--------------- | :------------ | :-------------------------- |
|
||||
| 1023 | 0 | Last Status: OK |
|
||||
| 1024 | e.g., 12360 | Last Trigger: Timestamp |
|
||||
| 1025 | e.g., 1 | Trigger Count: 1 |
|
||||
|
||||
## Limitations & Considerations
|
||||
|
||||
- **Complexity:** Only simple, single conditions per rule. No `AND`/`OR`/`ELSE`.
|
||||
- **Execution Order:** Rules evaluated sequentially.
|
||||
- **Performance:** Rule evaluation takes time. Consider impact and execution frequency.
|
||||
- **Timestamp:** The `Last_Trigger_Timestamp` resolution and potential for wrapping depend on the firmware implementation (e.g., `millis()` overflow, using seconds since boot).
|
||||
- **Error Handling:** Status codes provide basic feedback. More detailed logging might be needed for complex debugging.
|
||||
- **Method Availability:** Callable methods are fixed in firmware.
|
||||
- **Concurrency:** Actions (especially method calls) might take time. The engine design needs to consider if rule evaluation should block or if actions run asynchronously (current design implies synchronous execution within the loop).
|
||||
|
||||
|
||||
## Limitations & Considerations
|
||||
|
||||
- **Complexity:** Only simple, single conditions per rule are supported. No `AND`/`OR` or `ELSE` logic.
|
||||
- **Execution Order:** Rules are evaluated sequentially. Be mindful of potential interactions if multiple rules modify the same target.
|
||||
- **Performance:** Reading Modbus values and executing actions takes time. Complex rules or a large number of rules might impact overall system performance. Rule execution frequency should be considered.
|
||||
- **Error Handling:** The current design doesn't explicitly define Modbus registers for rule execution status or errors. This could be added later.
|
||||
- **Method Availability:** The list of callable methods (`Component ID`, `Method ID`) is fixed in the firmware and needs to be documented for users.
|
||||
109
docs/mb-plc.md
Normal file
@ -0,0 +1,109 @@
|
||||
# Comparison: Modbus Logic Engine (MB_SCRIPT) vs. PLC Ladder Logic
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
This document compares the custom Modbus Logic Engine (`MB_SCRIPT`) implemented in this firmware with traditional PLC (Programmable Logic Controller) programming languages, primarily focusing on Ladder Logic (LD).
|
||||
|
||||
* **Modbus Logic Engine (MB_SCRIPT):** A feature enabling simple, rule-based automation directly within the ESP32 firmware. Rules are configured and monitored via Modbus registers. It allows defining conditions based on Modbus values and triggering actions like writing Modbus values or calling internal C++ functions.
|
||||
* **PLC Ladder Logic (LD):** A graphical programming language widely used in industrial automation. It mimics the appearance of electrical relay logic diagrams, making it intuitive for electricians and technicians. It runs on dedicated PLC hardware in a cyclic scan execution model.
|
||||
|
||||
## 2. Core Concepts Comparison
|
||||
|
||||
| Feature | PLC Ladder Logic (Typical) | Modbus Logic Engine (MB_SCRIPT) |
|
||||
| :---------------------- | :------------------------------------------------------------- | :------------------------------------------------------------------ |
|
||||
| **Programming Model** | Graphical (Relay Logic Diagram), Textual (ST, FBD often avail.) | Rule-Based (IF condition THEN action), Configuration via Registers |
|
||||
| **Representation** | Rungs, Contacts (NO/NC), Coils, Function Blocks (Timers, Cnt) | Blocks of Holding Registers defining rules |
|
||||
| **Execution Model** | Cyclic Scan (Read Inputs -> Solve Logic -> Write Outputs) | Iterative `loop()` checks rules sequentially based on interval |
|
||||
| **Data Addressing** | Standardized I/O points (%I, %Q), Memory (%M), Data Regs (%MW) | Modbus Addresses (Coils/Registers) managed by firmware components |
|
||||
| **Built-in Functions** | Timers (TON/TOF), Counters (CTU/CTD), Math, Compare, Move | Basic Comparisons (==, !=, <, >...), Write Coil/Reg, Call Method |
|
||||
| **Complex Logic** | Subroutines, Jumps, Structured Text (ST), Function Blocks | Requires registering custom C++ methods (`CallableMethod`) |
|
||||
| **Configuration Tool** | Dedicated PLC IDE (e.g., TIA Portal, RSLogix) | Standard Modbus Client/Master Software |
|
||||
| **Deployment** | Download program binary to PLC hardware | Write configuration values to Modbus registers |
|
||||
| **Debugging/Monitor** | Online monitoring in IDE, Forcing I/O, Status LEDs | Reading Status registers via Modbus, Serial Logs (Debug/Receipt Flags) |
|
||||
| **Hardware** | Dedicated Industrial PLC Hardware | Runs on the ESP32 microcontroller within the firmware |
|
||||
| **Extensibility** | Adding I/O modules, using advanced function blocks, libraries | Adding/Registering C++ methods, potentially modifying engine code |
|
||||
| **Target User** | Automation Engineers, Technicians, Electricians | Firmware developers, System integrators familiar with Modbus |
|
||||
|
||||
## 3. Detailed Comparison
|
||||
|
||||
### 3.1. Programming Model & Representation
|
||||
|
||||
* **Ladder Logic:** Visual and intuitive for those familiar with electrical schematics. Logic flows left-to-right across rungs. Standard symbols for contacts, coils, timers, etc., are well-understood in the industry.
|
||||
* **MB_SCRIPT:** Abstract. Logic is defined by setting numerical values in specific Modbus registers according to predefined offsets (`ModbusLogicEngineOffsets`). Requires understanding the specific register map and enum values (`E_RegType`, `ConditionOperator`, `CommandType`, `MB_Error`). Less visual and more data-entry focused.
|
||||
|
||||
### 3.2. Execution
|
||||
|
||||
* **Ladder Logic:** Typically deterministic cyclic scan. The PLC reads all inputs, executes the entire ladder program from top to bottom, and then updates all outputs in a predictable cycle time.
|
||||
* **MB_SCRIPT:** Executes within the ESP32's main `loop()`. Rules are checked sequentially within the `ModbusLogicEngine::loop()` call, which runs periodically based on `loopInterval`. Execution time can be influenced by other tasks running on the ESP32. It's event-driven based on Modbus value changes *observed* during rule evaluation, but the evaluation itself is interval-based.
|
||||
|
||||
### 3.3. Data Handling
|
||||
|
||||
* **Ladder Logic:** Uses well-defined memory areas for inputs, outputs, internal bits, timers, counters, and data registers, accessed via standardized addressing.
|
||||
* **MB_SCRIPT:** Relies entirely on the existing Modbus address space managed by other firmware components (`ModbusManager`). Condition sources and action targets are arbitrary Modbus addresses. Internal rule state (status, timestamp, count) is exposed via dedicated status registers within the rule's block. Complex data manipulation requires C++ functions.
|
||||
|
||||
### 3.4. Capabilities & Functionality
|
||||
|
||||
* **Ladder Logic:** Offers standard, pre-built function blocks for common automation tasks like timing delays, counting events, basic arithmetic, and data manipulation. More advanced PLCs include PID loops, communication protocols, motion control, etc.
|
||||
* **MB_SCRIPT:** Provides fundamental building blocks: compare Modbus values and trigger simple actions (write Modbus value, call internal function). It lacks built-in timers, counters, or complex math operations within the rule definition itself. Such functionality must be implemented in C++ and exposed via the `CALL_COMPONENT_METHOD` command.
|
||||
|
||||
### 3.5. Configuration & Deployment
|
||||
|
||||
* **Ladder Logic:** Requires specialized, often vendor-specific, programming software. The compiled logic is downloaded as a binary program to the PLC. Configuration is done offline in the IDE.
|
||||
* **MB_SCRIPT:** Configured "online" by writing values to Modbus registers using any standard Modbus master tool. No separate compilation or download step for the logic itself is needed (only for the firmware containing the engine). This allows dynamic reconfiguration without reflashing firmware (though registered methods are fixed in firmware).
|
||||
|
||||
### 3.6. Debugging & Monitoring
|
||||
|
||||
* **Ladder Logic:** IDEs provide powerful online monitoring tools, allowing visualization of rung state, live value tracing, and forcing of inputs/outputs for testing. PLCs often have hardware status LEDs.
|
||||
* **MB_SCRIPT:** Debugging relies on reading the `LAST_STATUS`, `LAST_TRIGGER_TS`, and `TRIGGER_COUNT` registers via Modbus. More detail requires enabling the `RULE_FLAG_DEBUG` and `RULE_FLAG_RECEIPT` flags and monitoring the device's serial output (`npm run build:monitor`). Less interactive than typical PLC debugging.
|
||||
|
||||
## 4. Strengths & Weaknesses
|
||||
|
||||
**MB_SCRIPT:**
|
||||
|
||||
* **Strengths:**
|
||||
* Simple rule structure, easy to understand if the register map is known.
|
||||
* Configuration via standard Modbus tools - no specialized IDE needed.
|
||||
* Runs directly on the ESP32, reducing need for external micro-controllers for simple logic.
|
||||
* Extensible via C++ (`CALL_COMPONENT_METHOD`) for complex custom actions.
|
||||
* Logic configuration can potentially be updated without firmware reflash.
|
||||
* **Weaknesses:**
|
||||
* Limited built-in functions (no timers, counters, complex math within rules).
|
||||
* Logic representation is abstract (register values) and not visual.
|
||||
* Debugging relies heavily on Modbus reads and serial logs.
|
||||
* Sequential rule execution might have timing implications compared to cyclic scan.
|
||||
* Scalability limited by `MAX_LOGIC_RULES` and ESP32 resources.
|
||||
* Complex logic requires C++ development and method registration.
|
||||
|
||||
**PLC Ladder Logic:**
|
||||
|
||||
* **Strengths:**
|
||||
* Visual and intuitive programming paradigm (especially LD).
|
||||
* Industry standard, widely understood by technicians and engineers.
|
||||
* Rich set of built-in standard functions (timers, counters, etc.).
|
||||
* Mature, powerful IDEs with excellent online monitoring and debugging capabilities.
|
||||
* Deterministic execution (typically).
|
||||
* Highly scalable with modular hardware.
|
||||
* **Weaknesses:**
|
||||
* Requires dedicated, often expensive, PLC hardware.
|
||||
* Requires specialized, often expensive, vendor-specific IDE software.
|
||||
* Less straightforward to integrate directly with custom C++ functions or device-specific hardware features compared to code running *on* the device.
|
||||
* Configuration/updates typically require offline editing and download.
|
||||
|
||||
## 5. Use Cases
|
||||
|
||||
* **MB_SCRIPT is suitable for:**
|
||||
* Simple, reactive logic based directly on Modbus values on the device.
|
||||
* Triggering predefined C++ functions based on Modbus conditions.
|
||||
* Implementing basic interlocks or sequences configurable via Modbus.
|
||||
* Situations where adding a full PLC is overkill or impractical.
|
||||
* Environments where configuration via standard Modbus tools is preferred.
|
||||
* **PLC Ladder Logic is suitable for:**
|
||||
* Complex industrial automation and control sequences.
|
||||
* Applications requiring standard timing, counting, and process control functions.
|
||||
* Systems needing robust, deterministic execution.
|
||||
* Environments where technicians are primarily familiar with Ladder Logic.
|
||||
* Large-scale systems with extensive I/O requirements.
|
||||
|
||||
## 6. Conclusion
|
||||
|
||||
The Modbus Logic Engine (`MB_SCRIPT`) provides a useful, lightweight mechanism for implementing simple, Modbus-driven automation rules directly on the ESP32 firmware. It leverages the existing Modbus infrastructure for configuration and interaction but relies on C++ for complex actions. It is not a replacement for a full PLC system using Ladder Logic, which offers a more comprehensive, standardized, and visually intuitive environment with richer built-in capabilities tailored for industrial control, albeit with the requirement of dedicated hardware and software. The choice depends on the complexity of the required logic, the target environment, user expertise, and integration needs.
|
||||
102
docs/mb-rtu-filters.md
Normal file
@ -0,0 +1,102 @@
|
||||
# Modbus RTU Operation Filtering
|
||||
|
||||
The Modbus RTU filter system uses a **Chain of Responsibility** pattern to process `ModbusOperation`s before they are added to the execution queue. This allows for modular validation, modification, and management of operations.
|
||||
|
||||
## Architecture
|
||||
|
||||
Each filter implements the `ModbusOperationFilter` interface. The `process(ModbusOperation &op)` method passes the operation through the current filter's `filter()` logic and, if successful, forwards it to the next filter in the chain.
|
||||
|
||||
### Filter Interface
|
||||
|
||||
```cpp
|
||||
class ModbusOperationFilter {
|
||||
// ...
|
||||
// Process the operation through this filter and subsequent filters
|
||||
bool process(ModbusOperation &op) {
|
||||
if (!filter(op)) return false; // Stop if this filter rejects
|
||||
if (nextFilter) return nextFilter->process(op); // Pass to next
|
||||
return true; // End of chain, accepted
|
||||
}
|
||||
|
||||
// Abstract method to be implemented by concrete filters
|
||||
virtual bool filter(ModbusOperation &op) = 0;
|
||||
};
|
||||
```
|
||||
|
||||
## Active Filters
|
||||
|
||||
The current filter chain is configured in `ModbusRTU` constructor as follows:
|
||||
1. **DuplicateOperationFilter**
|
||||
2. **OperationLifecycleFilter**
|
||||
3. **PriorityFilter**
|
||||
|
||||
### 1. DuplicateOperationFilter
|
||||
**Purpose**: Prevents the queue from filling up with identical requests.
|
||||
**Logic**: Checks if an operation with the same `type`, `slaveId`, `address`, and `value` (for writes) is already pending in the `ModbusRTU` queue.
|
||||
**Action**: Returns `false` (drops operation) if a duplicate is found.
|
||||
|
||||
### 2. OperationLifecycleFilter
|
||||
**Purpose**: Enforces operation validity limits.
|
||||
**Logic**: Checks if `op.retries` has exceeded `maxRetries`.
|
||||
**Action**: Returns `false` (drops operation) if limits are exceeded.
|
||||
|
||||
### 3. PriorityFilter
|
||||
**Purpose**: Dynamically assigns priority to operations based on device-specific logic.
|
||||
**Logic**:
|
||||
1. Retrieves the target `RTU_Base` device instance using `Manager::getDeviceById(slaveId)`.
|
||||
2. Calls `device->isHighPriority(op)` to ask the device if this specific operation is critical.
|
||||
3. If `true`, sets the `OP_HIGH_PRIORITY_BIT` on the operation.
|
||||
**Action**: Always returns `true` (modifies operation but doesn't drop it).
|
||||
|
||||
## Flow Diagram
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Caller
|
||||
participant ModbusRTU
|
||||
participant DupFilter as DuplicateOperationFilter
|
||||
participant LifeFilter as OperationLifecycleFilter
|
||||
participant PrioFilter as PriorityFilter
|
||||
participant Device as RTU_Base (Device)
|
||||
|
||||
Caller->>ModbusRTU: queueOperation(op)
|
||||
ModbusRTU->>DupFilter: process(op)
|
||||
|
||||
rect rgb(240, 240, 240)
|
||||
Note right of DupFilter: Check for duplicates
|
||||
DupFilter->>DupFilter: filter(op)
|
||||
end
|
||||
|
||||
alt Duplicate Found
|
||||
DupFilter-->>ModbusRTU: false (Drop)
|
||||
else Unique
|
||||
DupFilter->>LifeFilter: process(op)
|
||||
|
||||
rect rgb(240, 240, 240)
|
||||
Note right of LifeFilter: Check Lifecycle
|
||||
LifeFilter->>LifeFilter: filter(op)
|
||||
Note right of LifeFilter: Check retries < max
|
||||
end
|
||||
|
||||
alt Limit Exceeded
|
||||
LifeFilter-->>DupFilter: false
|
||||
DupFilter-->>ModbusRTU: false (Drop)
|
||||
else OK
|
||||
LifeFilter->>PrioFilter: process(op)
|
||||
|
||||
rect rgb(240, 240, 240)
|
||||
Note right of PrioFilter: Check Priority
|
||||
PrioFilter->>PrioFilter: filter(op)
|
||||
PrioFilter->>Device: isHighPriority(op)
|
||||
Device-->>PrioFilter: bool
|
||||
opt isHighPriority == true
|
||||
PrioFilter->>PrioFilter: op.setHighPriority(true)
|
||||
end
|
||||
end
|
||||
|
||||
PrioFilter-->>LifeFilter: true
|
||||
LifeFilter-->>DupFilter: true
|
||||
DupFilter-->>ModbusRTU: true (Queue)
|
||||
end
|
||||
end
|
||||
```
|
||||
240
docs/mb-script-design.md
Normal file
@ -0,0 +1,240 @@
|
||||
# Modbus Logic Engine (MB_SCRIPT) Design
|
||||
|
||||
## 1. Overview
|
||||
|
||||
The Modbus Logic Engine (`ModbusLogicEngine` class, enabled by `#define ENABLE_MB_SCRIPT`) provides a way to implement simple automation rules directly on the device, configured and monitored via Modbus. It allows users to define rules based on the state of Modbus registers or coils (conditions) and trigger actions like writing to other registers/coils or calling internal component methods.
|
||||
|
||||
This enables reactive logic without requiring an external controller polling and writing values constantly.
|
||||
|
||||
## 2. Modbus Register Layout
|
||||
|
||||
Each logic rule occupies a contiguous block of Modbus holding registers. The number of rules is defined by `MAX_LOGIC_RULES` (default 8) and the number of registers per rule by `LOGIC_ENGINE_REGISTERS_PER_RULE` (currently 13). The starting address for the first rule is defined by `MODBUS_LOGIC_RULES_START`.
|
||||
|
||||
**Register Layout per Rule:**
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Rule N
|
||||
Reg0[Offset 0: ENABLED (0/1)]
|
||||
Reg1[Offset 1: COND_SRC_TYPE (E_RegType)]
|
||||
Reg2[Offset 2: COND_SRC_ADDR]
|
||||
Reg3[Offset 3: COND_OPERATOR (ConditionOperator)]
|
||||
Reg4[Offset 4: COND_VALUE]
|
||||
Reg5[Offset 5: COMMAND_TYPE (CommandType)]
|
||||
Reg6[Offset 6: COMMAND_TARGET (Addr/CompID)]
|
||||
Reg7[Offset 7: COMMAND_PARAM1 (Value/MethodID)]
|
||||
Reg8[Offset 8: COMMAND_PARAM2 (Arg1)]
|
||||
Reg9[Offset 9: FLAGS (Debug/Receipt)]
|
||||
Reg10[Offset 10: LAST_STATUS (MB_Error)]
|
||||
Reg11[Offset 11: LAST_TRIGGER_TS (Lower 16bit)]
|
||||
Reg12[Offset 12: TRIGGER_COUNT]
|
||||
end
|
||||
|
||||
style Reg0 fill:#f9f,stroke:#333,stroke-width:2px
|
||||
style Reg1 fill:#f9f,stroke:#333,stroke-width:2px
|
||||
style Reg2 fill:#f9f,stroke:#333,stroke-width:2px
|
||||
style Reg3 fill:#f9f,stroke:#333,stroke-width:2px
|
||||
style Reg4 fill:#f9f,stroke:#333,stroke-width:2px
|
||||
style Reg5 fill:#f9f,stroke:#333,stroke-width:2px
|
||||
style Reg6 fill:#f9f,stroke:#333,stroke-width:2px
|
||||
style Reg7 fill:#f9f,stroke:#333,stroke-width:2px
|
||||
style Reg8 fill:#f9f,stroke:#333,stroke-width:2px
|
||||
style Reg9 fill:#f9f,stroke:#333,stroke-width:2px
|
||||
style Reg10 fill:#ccf,stroke:#333,stroke-width:2px
|
||||
style Reg11 fill:#ccf,stroke:#333,stroke-width:2px
|
||||
style Reg12 fill:#ccf,stroke:#333,stroke-width:2px
|
||||
|
||||
```
|
||||
|
||||
**Register Descriptions (Offsets defined in `ModbusLogicEngineOffsets`):**
|
||||
|
||||
* **Configuration Registers (Read/Write via Modbus):**
|
||||
* `ENABLED` (0): `0` = Disabled, `1` = Enabled.
|
||||
* `COND_SRC_TYPE` (1): Type of the Modbus entity to check for the condition. Uses `RegisterState::E_RegType` enum values (e.g., `REG_HOLDING = 3`, `REG_COIL = 2`).
|
||||
* `COND_SRC_ADDR` (2): Modbus address of the register/coil for the condition.
|
||||
* `COND_OPERATOR` (3): Comparison operator to use. Uses `ConditionOperator` enum values (e.g., `EQUAL = 0`, `NOT_EQUAL = 1`, ...).
|
||||
* `COND_VALUE` (4): The value to compare the source register/coil against.
|
||||
* `COMMAND_TYPE` (5): The action to perform if the condition is met. Uses `CommandType` enum values (e.g., `WRITE_COIL = 2`, `WRITE_HOLDING_REGISTER = 3`, `CALL_COMPONENT_METHOD = 100`).
|
||||
* `COMMAND_TARGET` (6): Target of the command.
|
||||
* For `WRITE_*`: The Modbus address to write to.
|
||||
* For `CALL_COMPONENT_METHOD`: The `Component::id` of the target component.
|
||||
* `COMMAND_PARAM1` (7): First parameter for the command.
|
||||
* For `WRITE_*`: The value to write.
|
||||
* For `CALL_COMPONENT_METHOD`: The `methodId` registered with `ModbusLogicEngine::registerMethod`.
|
||||
* `COMMAND_PARAM2` (8): Second parameter for the command.
|
||||
* For `WRITE_*`: Unused.
|
||||
* For `CALL_COMPONENT_METHOD`: The first argument (`arg1`) passed to the registered method.
|
||||
* `FLAGS` (9): Bit flags for rule behavior (See Section 7).
|
||||
|
||||
* **Status Registers (Read-Only via Modbus, except TRIGGER_COUNT reset):**
|
||||
* `LAST_STATUS` (10): Result of the last evaluation/action attempt for this rule. Uses `MB_Error` enum values (e.g., `Success = 0`, `IllegalDataAddress = 2`, `ServerDeviceFailure = 4`).
|
||||
* `LAST_TRIGGER_TS` (11): Timestamp (lower 16 bits of `millis()/1000`) of the last successful trigger.
|
||||
* `TRIGGER_COUNT` (12): Counter of successful triggers. Can be reset by writing `0`.
|
||||
|
||||
## 3. Data Structures
|
||||
|
||||
* **`LogicRule` Struct:**
|
||||
* Holds the internal representation of a single rule.
|
||||
* `config[9]`: An array storing the 9 configuration registers (Offset 0 to 8) read from/written to Modbus.
|
||||
* `lastStatus` (type `RuleStatus`/`MB_Error`): Internal state variable reflecting the last status.
|
||||
* `lastTriggerTimestamp` (type `uint32_t`): Internal state variable for the full timestamp.
|
||||
* `triggerCount` (type `uint16_t`): Internal state variable for the trigger count.
|
||||
* Helper methods (`isEnabled()`, `getCondSourceType()`, `getCommandType()`, `getFlags()`, etc.) provide convenient access to the values stored in the `config` array.
|
||||
* **`std::vector<LogicRule> rules`:** Member variable in `ModbusLogicEngine` holding all configured rules.
|
||||
* **`std::map<uint32_t, CallableMethod> callableMethods`:** Member variable storing methods registered via `registerMethod`, keyed by `(componentId << 16) | methodId`.
|
||||
|
||||
## 4. Core Logic Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant ModbusClient
|
||||
participant ModbusManager
|
||||
participant ModbusLogicEngine as MLE
|
||||
participant TargetComponent
|
||||
|
||||
Note over ModbusClient, MLE: Initialization
|
||||
PHApp->>MLE: constructor(app)
|
||||
PHApp->>MLE: setup()
|
||||
MLE->>MLE: rules.resize(MAX_LOGIC_RULES)
|
||||
Note over MLE: Methods registered via registerMethod()
|
||||
PHApp->>MLE: registerMethod(compID, methodID, function)
|
||||
MLE->>MLE: callableMethods.insert(...)
|
||||
|
||||
loop Application Loop
|
||||
PHApp->>MLE: loop()
|
||||
alt Not Initialized or Interval Not Met
|
||||
MLE-->>PHApp: return E_OK
|
||||
else Rule Evaluation
|
||||
MLE->>MLE: For each rule in rules vector
|
||||
alt Rule Enabled?
|
||||
MLE->>MLE: evaluateCondition(rule)
|
||||
Note over MLE: Reads condition source
|
||||
MLE->>ModbusManager: findComponentForAddress(condAddr)
|
||||
ModbusManager-->>MLE: TargetComponent*
|
||||
MLE->>TargetComponent: readNetworkValue(condAddr)
|
||||
TargetComponent-->>MLE: currentValue (or error)
|
||||
Note over MLE: Performs comparison
|
||||
alt Condition Met?
|
||||
MLE->>MLE: performAction(rule)
|
||||
opt CommandType == WRITE_*
|
||||
Note over MLE: Performs write action
|
||||
MLE->>ModbusManager: findComponentForAddress(targetAddr)
|
||||
ModbusManager-->>MLE: TargetComponent*
|
||||
MLE->>TargetComponent: writeNetworkValue(targetAddr, value)
|
||||
TargetComponent-->>MLE: E_OK (or error)
|
||||
MLE->>MLE: updateRuleStatus(rule, MB_Error::Success / ServerDeviceFailure)
|
||||
opt CommandType == CALL_COMPONENT_METHOD
|
||||
Note over MLE: Performs method call
|
||||
MLE->>MLE: callableMethods.find(key)
|
||||
alt Method Found?
|
||||
MLE->>TargetComponent: Execute registered std::function(arg1)
|
||||
TargetComponent-->>MLE: E_OK (or error)
|
||||
MLE->>MLE: updateRuleStatus(rule, MB_Error::Success / OpExecutionFailed)
|
||||
else Method Not Found
|
||||
MLE->>MLE: updateRuleStatus(rule, MB_Error::IllegalDataAddress)
|
||||
end
|
||||
end
|
||||
MLE->>MLE: update timestamp & trigger count
|
||||
else Condition Not Met
|
||||
MLE->>MLE: updateRuleStatus(rule, MB_Error::Success) if not error
|
||||
end
|
||||
else Rule Disabled
|
||||
MLE->>MLE: Skip rule
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Note over ModbusClient, MLE: Configuration/Status Access
|
||||
ModbusClient->>ModbusManager: Read/Write Request (Holding Registers)
|
||||
ModbusManager->>MLE: readNetworkValue(addr) / writeNetworkValue(addr, val)
|
||||
MLE->>MLE: getRuleInfoFromAddress(addr)
|
||||
alt Read Request
|
||||
MLE->>MLE: Access rule.config[] or internal status
|
||||
MLE-->>ModbusManager: value / status
|
||||
else Write Request
|
||||
MLE->>MLE: rule.setConfigValue(offset, val) / rule.triggerCount = 0
|
||||
MLE-->>ModbusManager: E_OK / error
|
||||
end
|
||||
ModbusManager-->>ModbusClient: Response
|
||||
|
||||
```
|
||||
|
||||
* **`setup()`:** Initializes the `rules` vector based on `MAX_LOGIC_RULES`.
|
||||
* **`loop()`:** Called periodically by the main application loop (`PHApp`).
|
||||
* Checks if initialized and if the evaluation `loopInterval` has passed.
|
||||
* Iterates through each `LogicRule` in the `rules` vector.
|
||||
* If a rule `isEnabled()`, it calls `evaluateCondition()`.
|
||||
* If `evaluateCondition()` returns `true` (condition met), it calls `performAction()`.
|
||||
* **`evaluateCondition()`:**
|
||||
* Retrieves condition parameters (source type, address, operator, value) from the rule.
|
||||
* Calls `readConditionSourceValue()` to get the current value of the source register/coil.
|
||||
* Performs the comparison based on the `condOperator`.
|
||||
* Updates `rule.lastStatus` (e.g., `MB_Error::IllegalDataAddress` on read failure, `MB_Error::IllegalDataValue` on invalid operator).
|
||||
* Returns `true` if the condition is met, `false` otherwise (including errors).
|
||||
* **`performAction()`:**
|
||||
* Retrieves command parameters (type, target, params) from the rule.
|
||||
* Based on `commandType`:
|
||||
* Calls `performWriteAction()` for `WRITE_*` commands.
|
||||
* Calls `performCallAction()` for `CALL_COMPONENT_METHOD`.
|
||||
* Updates `rule.lastStatus` based on the success/failure of the action (e.g., `MB_Error::Success`, `MB_Error::ServerDeviceFailure`, `MB_Error::OpExecutionFailed`, `MB_Error::IllegalFunction`).
|
||||
* If successful, updates `rule.lastTriggerTimestamp` and increments `rule.triggerCount`.
|
||||
* Returns `true` on success, `false` on failure.
|
||||
|
||||
## 5. Interaction with ModbusManager
|
||||
|
||||
The `ModbusLogicEngine` relies on the `ModbusManager` (accessed via the `PHApp* app` pointer) to interact with other components' Modbus values.
|
||||
|
||||
* **`readConditionSourceValue()`:**
|
||||
* Takes the source type (`RegisterState::E_RegType`) and address.
|
||||
* Uses `app->modbusManager->findComponentForAddress(address)` to find the component responsible for that address.
|
||||
* Calls the target component's `readNetworkValue(address)` method.
|
||||
* Returns the value or indicates failure.
|
||||
* **`performWriteAction()`:**
|
||||
* Takes the command type (`WRITE_COIL` or `WRITE_HOLDING_REGISTER`), target address, and value.
|
||||
* Uses `app->modbusManager->findComponentForAddress(address)` to find the target component.
|
||||
* Calls the target component's `writeNetworkValue(address, value)` method.
|
||||
* Returns `true` if the write call returns `E_OK`, `false` otherwise.
|
||||
|
||||
## 6. Method Calling (`CALL_COMPONENT_METHOD`)
|
||||
|
||||
This command type allows rules to trigger C++ methods within other components.
|
||||
|
||||
* **Registration:** Components (like `PHApp` or custom components) that want to expose methods to the Logic Engine must call `ModbusLogicEngine::registerMethod(componentId, methodId, method)`.
|
||||
* `componentId`: The unique `Component::id` of the component exposing the method.
|
||||
* `methodId`: A unique ID (within that component) for the specific method being exposed.
|
||||
* `method`: A `std::function<short(short, short)>` object wrapping the actual C++ method (often created using `std::bind`). The function should accept two `short` arguments and return `E_OK` (or another error code).
|
||||
* The engine stores this registration in the `callableMethods` map.
|
||||
* **Configuration:** A rule is configured with `COMMAND_TYPE = CALL_COMPONENT_METHOD`.
|
||||
* `COMMAND_TARGET` is set to the `componentId`.
|
||||
* `COMMAND_PARAM1` is set to the `methodId`.
|
||||
* `COMMAND_PARAM2` is set to the value to be passed as the first argument (`arg1`) to the registered C++ method.
|
||||
* **Execution (`performCallAction()`):**
|
||||
* Combines `componentId` (from Target) and `methodId` (from Param1) into a key.
|
||||
* Looks up the key in the `callableMethods` map.
|
||||
* If found, executes the stored `std::function`, passing `param2` (as `arg1`) and a dummy `0` (as `arg2`) to the bound C++ method.
|
||||
* Updates status based on the return value of the C++ method (`MB_Error::Success` if `E_OK`, `MB_Error::OpExecutionFailed` otherwise) or `MB_Error::IllegalDataAddress` if the method was not found in the map.
|
||||
|
||||
*Note: This `std::function`-based registration is internal to the `ModbusLogicEngine` and separate from the `Bridge` mechanism used for serial commands.*
|
||||
|
||||
## 7. Flags (`FLAGS` Register - Offset 9)
|
||||
|
||||
The `FLAGS` register allows modifying rule behavior using bitmasks:
|
||||
|
||||
* **`RULE_FLAG_DEBUG` (Bit 0 / Value 1):** If set, enables verbose logging (`Log.verboseln`) during the evaluation and action phases for this specific rule, showing details like addresses, values, and outcomes.
|
||||
* **`RULE_FLAG_RECEIPT` (Bit 1 / Value 2):** If set, logs an informational message (`Log.infoln`) whenever the rule's action is executed successfully.
|
||||
|
||||
These flags can be combined (e.g., value `3` enables both).
|
||||
|
||||
## 8. Modbus Interface (`read/writeNetworkValue`)
|
||||
|
||||
* The `ModbusLogicEngine` implements `readNetworkValue` and `writeNetworkValue` as required by the `Component` base class.
|
||||
* These methods are called by `ModbusManager` when a Modbus client reads/writes registers within the engine's address range (`MODBUS_LOGIC_RULES_START` onwards).
|
||||
* They calculate the `ruleIndex` and `offset` from the requested Modbus address.
|
||||
* **Read:**
|
||||
* If the offset corresponds to a configuration register (0-8), it returns the value from `rule.config[offset]`.
|
||||
* If the offset is `FLAGS` (9), it returns the flag value via `getFlags()`.
|
||||
* If the offset corresponds to a status register (10-12), it returns the value from the internal state variables (`rule.lastStatus`, `rule.lastTriggerTimestamp & 0xFFFF`, `rule.triggerCount`).
|
||||
* **Write:**
|
||||
* If the offset corresponds to a configuration register (0-9, including FLAGS), it updates `rule.config[offset]` using `rule.setConfigValue()`.
|
||||
* If the offset is `TRIGGER_COUNT` (12) and the value is `0`, it resets `rule.triggerCount`.
|
||||
* Writes to read-only status registers (10, 11) are disallowed and return an error.
|
||||
197
docs/mb-script-testing.md
Normal file
@ -0,0 +1,197 @@
|
||||
# Modbus Logic Engine (MB_SCRIPT) Functional Testing Strategy
|
||||
|
||||
## 1. Objective
|
||||
|
||||
To verify the functional correctness of the `ModbusLogicEngine` component (enabled by the `ENABLE_MB_SCRIPT` define). This includes:
|
||||
* Correct configuration of logic rules via Modbus.
|
||||
* Accurate evaluation of rule conditions based on Modbus register/coil values.
|
||||
* Proper execution of defined actions (writing registers/coils, calling component methods).
|
||||
* Correct reporting of rule status, trigger timestamps, and trigger counts via Modbus.
|
||||
* Handling of various error conditions.
|
||||
|
||||
## 2. Scope
|
||||
|
||||
This strategy focuses on black-box functional testing from the perspective of a Modbus client interacting with the device. It does not cover unit testing of individual `ModbusLogicEngine` methods or performance/stress testing (which may have separate test plans).
|
||||
|
||||
## 3. Approach
|
||||
|
||||
Testing will primarily utilize the existing `npm` scripts which wrap Python helper scripts to interact with the device over Modbus TCP.
|
||||
|
||||
* **Configuration:** Rules will be configured by writing to the specific Modbus holding registers allocated to the Logic Engine (`MODBUS_LOGIC_RULES_START` and subsequent offsets defined in `src/ModbusLogicEngine.h`).
|
||||
* **Triggering:** Rule conditions will be triggered by writing appropriate values to the source Modbus holding registers or coils specified in the rule's condition.
|
||||
* **Verification:**
|
||||
* Action outcomes will be verified by reading the target Modbus holding registers or coils.
|
||||
* Rule status, timestamps, and trigger counts will be verified by reading the corresponding status registers for the rule.
|
||||
* Internal behavior and potential errors can be monitored using device logs (`npm run build:monitor` or `npm run debug:serial`).
|
||||
* **No New Scripts:** This strategy aims to use only the pre-existing `npm` scripts for Modbus interaction.
|
||||
|
||||
## 4. Tools
|
||||
|
||||
* **Modbus Read/Write:**
|
||||
* `npm run modbus:read:holding -- --address <addr>`
|
||||
* `npm run modbus:write:holding -- --address <addr> --value <val>`
|
||||
* `npm run modbus:read:coil -- --address <addr>`
|
||||
* `npm run modbus:write:coil -- --address <addr> --value <0|1>`
|
||||
* **Logging:**
|
||||
* `npm run build:monitor` (Live serial monitoring)
|
||||
* `npm run debug:serial` (Fetch buffered logs via REST API)
|
||||
* **Reference:**
|
||||
* `src/ModbusLogicEngine.h` (for register offsets, enums)
|
||||
* `src/config-modbus.h` (for `MODBUS_LOGIC_RULES_START` address)
|
||||
|
||||
## 5. Prerequisites
|
||||
|
||||
* Firmware compiled with `#define ENABLE_MB_SCRIPT` in `config.h` and flashed to the ESP32.
|
||||
* Device connected to the network and accessible via its IP address or mDNS name (`modbus-esp32.local` by default).
|
||||
* Python environment configured correctly to run the scripts in the `scripts/` directory.
|
||||
* Knowledge of the Modbus register mapping for the Logic Engine (starting address + offsets).
|
||||
|
||||
## 6. Test Cases
|
||||
|
||||
Let `RULE_START = MODBUS_LOGIC_RULES_START`.
|
||||
Let `RULE_0_BASE = RULE_START`.
|
||||
Let `RULE_1_BASE = RULE_START + LOGIC_ENGINE_REGISTERS_PER_RULE`.
|
||||
Offsets are defined in `ModbusLogicEngineOffsets`.
|
||||
|
||||
*(Note: Choose suitable, otherwise unused Modbus addresses for source/target registers/coils for testing)*
|
||||
|
||||
**TC 1: Basic Rule - Write Holding Register**
|
||||
1. **Configure:**
|
||||
* Write `1` to `RULE_0_BASE + ENABLED`.
|
||||
* Write `3` (REG_HOLDING) to `RULE_0_BASE + COND_SRC_TYPE`.
|
||||
* Write `2000` to `RULE_0_BASE + COND_SRC_ADDR`.
|
||||
* Write `0` (EQUAL) to `RULE_0_BASE + COND_OPERATOR`.
|
||||
* Write `123` to `RULE_0_BASE + COND_VALUE`.
|
||||
* Write `3` (WRITE_HOLDING_REGISTER) to `RULE_0_BASE + COMMAND_TYPE`.
|
||||
* Write `2001` to `RULE_0_BASE + COMMAND_TARGET`.
|
||||
* Write `456` to `RULE_0_BASE + COMMAND_PARAM1` (Value to write).
|
||||
* Write `0` to `RULE_0_BASE + COMMAND_PARAM2` (Unused).
|
||||
* Write `0` to `RULE_0_BASE + FLAGS`.
|
||||
2. **Trigger:** Write `123` to Modbus address `2000`.
|
||||
3. **Verify:**
|
||||
* Read address `2001`. Expected: `456`.
|
||||
* Read `RULE_0_BASE + LAST_STATUS`. Expected: `0` (MB_Error::Success).
|
||||
* Read `RULE_0_BASE + TRIGGER_COUNT`. Expected: `1`.
|
||||
|
||||
**TC 2: Condition Operator - Not Equal**
|
||||
1. **Configure:** Similar to TC 1, but:
|
||||
* Write `1` (NOT_EQUAL) to `RULE_0_BASE + COND_OPERATOR`.
|
||||
* Write `123` to `RULE_0_BASE + COND_VALUE`.
|
||||
2. **Trigger 1:** Write `123` to address `2000`.
|
||||
3. **Verify 1:** Read address `2001`. Expected: *Unchanged* from previous state (action should *not* run).
|
||||
4. **Trigger 2:** Write `124` to address `2000`.
|
||||
5. **Verify 2:**
|
||||
* Read address `2001`. Expected: `456`.
|
||||
* Read `RULE_0_BASE + LAST_STATUS`. Expected: `0` (MB_Error::Success).
|
||||
* Read `RULE_0_BASE + TRIGGER_COUNT`. Expected: Incremented.
|
||||
* *(Repeat for other operators: `<`, `<=`, `>`, `>=`)*
|
||||
|
||||
**TC 3: Source Type - Coil**
|
||||
1. **Configure:** Similar to TC 1, but:
|
||||
* Write `2` (REG_COIL) to `RULE_0_BASE + COND_SRC_TYPE`.
|
||||
* Write `100` (Test Coil Addr) to `RULE_0_BASE + COND_SRC_ADDR`.
|
||||
* Write `1` to `RULE_0_BASE + COND_VALUE` (Condition is Coil ON).
|
||||
2. **Trigger:** Write `1` to Coil address `100`.
|
||||
3. **Verify:**
|
||||
* Read address `2001`. Expected: `456`.
|
||||
* Read `RULE_0_BASE + LAST_STATUS`. Expected: `0` (MB_Error::Success).
|
||||
|
||||
**TC 4: Action Type - Write Coil**
|
||||
1. **Configure:** Similar to TC 1, but:
|
||||
* Write `2` (WRITE_COIL) to `RULE_0_BASE + COMMAND_TYPE`.
|
||||
* Write `101` to `RULE_0_BASE + COMMAND_TARGET`.
|
||||
* Write `1` to `RULE_0_BASE + COMMAND_PARAM1` (Value: ON).
|
||||
* Write `0` to `RULE_0_BASE + COMMAND_PARAM2` (Unused).
|
||||
2. **Trigger:** Write `123` to address `2000`.
|
||||
3. **Verify:**
|
||||
* Read Coil address `101`. Expected: `1`.
|
||||
* Read `RULE_0_BASE + LAST_STATUS`. Expected: `0` (MB_Error::Success).
|
||||
|
||||
**TC 5: Action Type - Call Component Method (Requires Setup)**
|
||||
* **Prerequisite:** A method must be registered with the `ModbusLogicEngine` that has a verifiable side-effect readable via Modbus. Example: A simple method in `PHApp` that increments a counter stored in a Modbus register (`e.g., address 3000`).
|
||||
```cpp
|
||||
// In PHApp.h (or a test component)
|
||||
short testMethod(short p1, short p2) {
|
||||
testMethodCounter += p1; // Use p1 (arg1 from rule)
|
||||
Log.infoln("Test Method Called! Arg1=%d, Counter: %d", p1, testMethodCounter);
|
||||
return E_OK;
|
||||
}
|
||||
uint16_t testMethodCounter = 0;
|
||||
|
||||
// In PHApp::setup() or where ModbusLogicEngine is initialized
|
||||
logicEngine->registerMethod(this->id, 1, // Use app ID and method ID 1
|
||||
std::bind(&PHApp::testMethod, this, std::placeholders::_1, std::placeholders::_2));
|
||||
|
||||
// In PHApp::readNetworkValue()
|
||||
if (address == 3000) return testMethodCounter;
|
||||
```
|
||||
1. **Configure:** Similar to TC 1, but:
|
||||
* Write `100` (CALL_COMPONENT_METHOD) to `RULE_0_BASE + COMMAND_TYPE`.
|
||||
* Write `app->id` to `RULE_0_BASE + COMMAND_TARGET` (Component ID).
|
||||
* Write `1` to `RULE_0_BASE + COMMAND_PARAM1` (Method ID).
|
||||
* Write `5` to `RULE_0_BASE + COMMAND_PARAM2` (Argument 1).
|
||||
2. **Initial Read:** Read address `3000`. Note the value (e.g., `X`).
|
||||
3. **Trigger:** Write `123` to address `2000`.
|
||||
4. **Verify:**
|
||||
* Read address `3000`. Expected: `X + 5`.
|
||||
* Read `RULE_0_BASE + LAST_STATUS`. Expected: `0` (MB_Error::Success).
|
||||
* Check logs (`build:monitor`) for "Test Method Called! Arg1=5".
|
||||
|
||||
**TC 6: Rule Enable/Disable**
|
||||
1. **Configure:** Configure rule as in TC 1.
|
||||
2. **Disable:** Write `0` to `RULE_0_BASE + ENABLED`.
|
||||
3. **Trigger:** Write `123` to address `2000`.
|
||||
4. **Verify (Disabled):** Read address `2001`. Expected: *Unchanged*. Trigger count should *not* increment.
|
||||
5. **Enable:** Write `1` to `RULE_0_BASE + ENABLED`.
|
||||
6. **Trigger:** Write `123` to address `2000`.
|
||||
7. **Verify (Enabled):** Read address `2001`. Expected: `456`. Trigger count *should* increment.
|
||||
|
||||
**TC 7: Error - Invalid Condition Source Address**
|
||||
1. **Configure:** Similar to TC 1, but:
|
||||
* Write `9999` (Invalid/Unregistered address) to `RULE_0_BASE + COND_SRC_ADDR`.
|
||||
2. **Trigger:** Let the engine loop run.
|
||||
3. **Verify:** Read `RULE_0_BASE + LAST_STATUS`. Expected: `2` (MB_Error::IllegalDataAddress).
|
||||
|
||||
**TC 8: Error - Invalid Action Target Address (Write)**
|
||||
1. **Configure:** Similar to TC 1, but:
|
||||
* Write `9998` (Invalid/Unregistered address) to `RULE_0_BASE + COMMAND_TARGET`.
|
||||
2. **Trigger:** Write `123` to address `2000`.
|
||||
3. **Verify:** Read `RULE_0_BASE + LAST_STATUS`. Expected: `4` (MB_Error::ServerDeviceFailure).
|
||||
|
||||
**TC 9: Error - Invalid Action Target Method (Call)**
|
||||
1. **Configure:** Similar to TC 5, but:
|
||||
* Write `99` (Non-existent Method ID) to `RULE_0_BASE + COMMAND_PARAM1`.
|
||||
2. **Trigger:** Write `123` to address `2000`.
|
||||
3. **Verify:** Read `RULE_0_BASE + LAST_STATUS`. Expected: `2` (MB_Error::IllegalDataAddress).
|
||||
|
||||
**TC 10: Status/Counter Reset**
|
||||
1. **Configure & Trigger:** Perform TC 1.
|
||||
2. **Verify Count:** Read `RULE_0_BASE + TRIGGER_COUNT`. Expected: `1` (or current count).
|
||||
3. **Reset Counter:** Write `0` to `RULE_0_BASE + TRIGGER_COUNT`.
|
||||
4. **Verify Reset:** Read `RULE_0_BASE + TRIGGER_COUNT`. Expected: `0`.
|
||||
5. **Trigger Again:** Write `123` to address `2000`.
|
||||
6. **Verify Increment:** Read `RULE_0_BASE + TRIGGER_COUNT`. Expected: `1`.
|
||||
|
||||
**TC 11: Debug Flag**
|
||||
1. **Configure:** Configure as in TC 1, but:
|
||||
* Write `RULE_FLAG_DEBUG` (value 1) to `RULE_0_BASE + FLAGS`.
|
||||
2. **Trigger:** Write `123` to address `2000`.
|
||||
3. **Verify:**
|
||||
* Check logs (`build:monitor`). Expected: Verbose logs like "MLE Eval [0]: ...", "MLE Action [0]: ...".
|
||||
* Read address `2001`. Expected: `456`.
|
||||
|
||||
**TC 12: Receipt Flag**
|
||||
1. **Configure:** Configure as in TC 1, but:
|
||||
* Write `RULE_FLAG_RECEIPT` (value 2) to `RULE_0_BASE + FLAGS`.
|
||||
2. **Trigger:** Write `123` to address `2000`.
|
||||
3. **Verify:**
|
||||
* Check logs (`build:monitor`). Expected: Info log "MLE: Rule 0 action successful.".
|
||||
* Read address `2001`. Expected: `456`.
|
||||
|
||||
**TC 13: Debug + Receipt Flags**
|
||||
1. **Configure:** Configure as in TC 1, but:
|
||||
* Write `RULE_FLAG_DEBUG | RULE_FLAG_RECEIPT` (value 3) to `RULE_0_BASE + FLAGS`.
|
||||
2. **Trigger:** Write `123` to address `2000`.
|
||||
3. **Verify:**
|
||||
* Check logs (`build:monitor`). Expected: Both verbose debug logs and the receipt log.
|
||||
* Read address `2001`. Expected: `456`.
|
||||
225
docs/mb-sync.md
Normal file
@ -0,0 +1,225 @@
|
||||
# ValueWrapper and Modbus Block Synchronization
|
||||
|
||||
This document outlines a streamlined mechanism for synchronizing component state with the Modbus TCP interface using the `ValueWrapper` class and a unifying macro, `MB_REG_EX`. This pattern reduces boilerplate code, improves readability, and centralizes the logic for value-change detection and notification.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
The system is built on three key components:
|
||||
|
||||
1. **`ValueWrapper<T>`**: A template class that wraps a value of type `T`. It monitors the value for significant changes (based on a configurable threshold) and triggers a notification to its owner component when such a change occurs.
|
||||
2. **`MB_Registers`**: A structure that defines a Modbus register block, including its address, function code, and access permissions.
|
||||
3. **`MB_REG_EX` / `MB_REG`**: Macros that initialize both a `ValueWrapper` instance and its corresponding `MB_Registers` entry in a single, atomic operation within a component's constructor.
|
||||
|
||||
### Class Relationships
|
||||
|
||||
The following diagram illustrates how these components relate to a custom component that uses them.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Component {
|
||||
+owner: Component*
|
||||
+id: ushort
|
||||
+name: String
|
||||
+mb_tcp_base_address() uint16_t
|
||||
+onMessage(source, call, flags, data, sender)
|
||||
}
|
||||
|
||||
class ValueWrapper {
|
||||
-m_owner: Component*
|
||||
-m_value: T
|
||||
-m_threshold: T
|
||||
+update(T newValue)
|
||||
+get() T
|
||||
}
|
||||
|
||||
class MB_Registers {
|
||||
+startAddress: ushort
|
||||
+count: ushort
|
||||
+type: E_FN_CODE
|
||||
+access: E_ModbusAccess
|
||||
}
|
||||
|
||||
Component <|-- YourCustomComponent
|
||||
YourCustomComponent "1" *-- "N" ValueWrapper : has-a
|
||||
YourCustomComponent "1" *-- "N" MB_Registers : defines
|
||||
ValueWrapper ..> Component : notifies via onMessage
|
||||
|
||||
```
|
||||
|
||||
A custom component derives from `Component` and owns its `ValueWrapper` instances. When a `ValueWrapper` detects a change, it uses its owner pointer to send an `onMessage` notification, carrying the updated Modbus data.
|
||||
|
||||
## Initialization and Runtime Flow
|
||||
|
||||
The entire process, from initialization to a runtime update, is designed to be efficient and straightforward.
|
||||
|
||||
### Initialization Sequence
|
||||
|
||||
The `MB_REG_EX` or `MB_REG` macro is the cornerstone of the setup process. It must be called from within a component's constructor body (not the initializer list).
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as User/System
|
||||
participant C as YourCustomComponent
|
||||
participant VW as "ValueWrapper"
|
||||
participant MB as "MB_Registers[]"
|
||||
|
||||
User->>C: new YourCustomComponent()
|
||||
activate C
|
||||
Note over C: Constructor calls MB_REG_EX or MB_REG
|
||||
C->>VW: new ValueWrapper(...)
|
||||
Note right of VW: Initializes with owner, threshold, and optional notification callback.
|
||||
C->>MB: MB_Registers(...)
|
||||
Note right of MB: Defines Modbus register properties (address, access, etc.).
|
||||
deactivate C
|
||||
|
||||
```
|
||||
|
||||
### Runtime Update Sequence
|
||||
|
||||
During normal operation, the component's `loop()` method is responsible for feeding new values to the `ValueWrapper`. The wrapper handles the rest.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as YourCustomComponent
|
||||
participant VW as "ValueWrapper"
|
||||
participant Owner as "Owner Component (e.g., ModbusTCP)"
|
||||
|
||||
loop Component's loop()
|
||||
C->>C: Read or calculate new value
|
||||
C->>VW: update(newValue)
|
||||
activate VW
|
||||
VW->>VW: Check if |newValue - oldValue| >= threshold
|
||||
alt threshold is met AND notifications enabled
|
||||
VW->>Owner: onMessage(MB_UpdateData)
|
||||
activate Owner
|
||||
Owner->>Owner: Process update (e.g., queue Modbus message)
|
||||
deactivate Owner
|
||||
opt Post-notification callback exists
|
||||
VW->>C: post_notify_cb(newValue, oldValue)
|
||||
end
|
||||
end
|
||||
deactivate VW
|
||||
end
|
||||
```
|
||||
|
||||
## How to Use `MB_REG` and `MB_REG_EX`
|
||||
|
||||
To implement this pattern, follow these steps:
|
||||
|
||||
1. Declare the `ValueWrapper<T>` members in your component's header file. They will be default-initialized.
|
||||
2. In the component's constructor, call the appropriate macro (`MB_REG` or `MB_REG_EX`) for each wrapped value.
|
||||
|
||||
### Macro Signatures
|
||||
|
||||
#### `MB_REG_EX` (Extended)
|
||||
The extended macro allows for fine-grained control over all parameters, including the slave ID and whether notifications are enabled.
|
||||
```cpp
|
||||
MB_REG_EX(
|
||||
vw_member, // The ValueWrapper member variable
|
||||
mb_blocks_array, // The MB_Registers array
|
||||
vw_type, // The data type (e.g., PlotStatus, int16_t)
|
||||
mb_reg_offset_enum, // The register offset enum value
|
||||
mb_fn_code, // The Modbus function code
|
||||
mb_access, // Read/write access (MB_ACCESS_...)
|
||||
mb_slave_id, // The slave ID
|
||||
mb_desc, // A description string
|
||||
mb_group, // A group name string
|
||||
vw_initial_val, // The wrapper's initial value
|
||||
vw_threshold_val, // The threshold for notification
|
||||
vw_threshold_mode, // DIFFERENCE or INTERVAL_STEP
|
||||
vw_enable_notification,// true to enable notifications, false to disable
|
||||
vw_post_notify_cb // An optional post-notification callback (or nullptr)
|
||||
);
|
||||
```
|
||||
|
||||
#### `MB_REG` (Simple)
|
||||
The simple macro is a convenience for the most common use case. It assumes notifications are **enabled** and the **slave ID is 1**.
|
||||
```cpp
|
||||
MB_REG(
|
||||
vw_member, // The ValueWrapper member variable
|
||||
mb_blocks_array, // The MB_Registers array
|
||||
vw_type, // The data type (e.g., PlotStatus, int16_t)
|
||||
mb_reg_offset_enum, // The register offset enum value
|
||||
mb_fn_code, // The Modbus function code
|
||||
mb_access, // Read/write access (MB_ACCESS_...)
|
||||
mb_desc, // A description string
|
||||
mb_group, // A group name string
|
||||
vw_initial_val, // The wrapper's initial value
|
||||
vw_threshold_val, // The threshold for notification
|
||||
vw_threshold_mode, // DIFFERENCE or INTERVAL_STEP
|
||||
vw_post_notify_cb // An optional post-notification callback (or nullptr)
|
||||
);
|
||||
```
|
||||
|
||||
### Example Implementation
|
||||
|
||||
Here is a conceptual example based on `TemperatureProfile`. Since it needs to configure notification enablement, it uses `MB_REG_EX`.
|
||||
|
||||
```cpp
|
||||
// TemperatureProfile.h
|
||||
class TemperatureProfile : public PlotBase {
|
||||
// ...
|
||||
private:
|
||||
ValueWrapper<PlotStatus> _statusWrapper;
|
||||
ValueWrapper<int16_t> _currentTemperatureWrapper;
|
||||
// ...
|
||||
MB_Registers _modbusBlocks[TEMP_PROFILE_REGISTER_COUNT];
|
||||
};
|
||||
|
||||
// TemperatureProfile.cpp
|
||||
TemperatureProfile::TemperatureProfile(Component *owner, short slot, ushort componentId)
|
||||
: PlotBase(owner, componentId),
|
||||
// _statusWrapper is default-initialized here
|
||||
// ...
|
||||
{
|
||||
name = "TempProfile_" + String(this->id) + "_Slot_" + String(slot);
|
||||
const char* group = name.c_str();
|
||||
|
||||
// Now, initialize the wrapper and the Modbus block together
|
||||
MB_REG_EX(
|
||||
_statusWrapper,
|
||||
_modbusBlocks,
|
||||
PlotStatus,
|
||||
TemperatureProfileRegisterOffset::STATUS,
|
||||
E_FN_CODE::FN_READ_HOLD_REGISTER,
|
||||
MB_ACCESS_READ_ONLY,
|
||||
1, // slaveId
|
||||
"TProf Status",
|
||||
group,
|
||||
PlotStatus::IDLE,
|
||||
PlotStatus::RUNNING, // Threshold: notify when value becomes RUNNING
|
||||
ValueWrapper<PlotStatus>::ThresholdMode::DIFFERENCE,
|
||||
true, // Enable notifications
|
||||
nullptr // No post-notification callback
|
||||
);
|
||||
|
||||
MB_REG_EX(
|
||||
_currentTemperatureWrapper,
|
||||
_modbusBlocks,
|
||||
int16_t,
|
||||
TemperatureProfileRegisterOffset::CURRENT_TEMP,
|
||||
E_FN_CODE::FN_WRITE_HOLD_REGISTER,
|
||||
MB_ACCESS_READ_ONLY, // This block is RO from Modbus master perspective
|
||||
1, // slaveId
|
||||
"TProf Curr Temp",
|
||||
group,
|
||||
INT16_MIN,
|
||||
1, // Threshold: notify if value changes by at least 1
|
||||
ValueWrapper<int16_t>::ThresholdMode::DIFFERENCE,
|
||||
false, // Disable notifications for this wrapper
|
||||
nullptr
|
||||
);
|
||||
|
||||
// ... initialize other blocks
|
||||
}
|
||||
|
||||
// In the loop method
|
||||
void TemperatureProfile::loop() {
|
||||
// ...
|
||||
_statusWrapper.update(getCurrentStatus());
|
||||
_currentTemperatureWrapper.update(getTemperature(getElapsedMs()));
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
By setting `vw_enable_notification` to `false` in `MB_REG_EX`, the `ValueWrapper` still tracks the value and holds state, but its `update` method will not trigger an `onMessage` call, effectively decoupling it from the Modbus notification system while still allowing its use as a stateful wrapper.
|
||||
441
docs/mb-todos.md
Normal file
@ -0,0 +1,441 @@
|
||||
# Modbus Implementation Analysis & TODOs
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
The current Modbus RTU and TCP implementation is functional but suffers from several architectural issues that impact performance, scalability, and reliability. Key problems include inefficient data lookups due to linear searches, overly complex and redundant state management abstractions, and a lack of support for transactional operations.
|
||||
|
||||
This document outlines these issues and provides a roadmap for both immediate, tactical improvements and long-term strategic changes to create a faster, more robust, and transactional Modbus subsystem.
|
||||
|
||||
## 2. Identified Issues & Bottlenecks
|
||||
|
||||
### 2.1. Useless Abstractions & State Duplication
|
||||
|
||||
The separation between `ModbusRTU`'s cache (`SlaveData`, `ModbusValueEntry`) and the `RTU_Base`/`RegisterState` classes creates an unnecessary and confusing layer of abstraction.
|
||||
|
||||
- **State is duplicated:** The value of a register is stored in both a `ModbusValueEntry` inside `ModbusRTU` and a `RegisterState` object owned by a `RTU_Base` device. This increases memory usage and creates synchronization complexity.
|
||||
- **Complex Data Flow:** An update flows from the `ModbusRTU` cache, triggers a callback to the `Manager`, which then finds the `RTU_Base` device, which then updates its internal `RegisterState`. This is indirect and inefficient.
|
||||
- **High Overhead:** The `RTU_Base` and `RegisterState` classes introduce significant object-oriented overhead for what is fundamentally a simple register map. Each register becomes a heap-allocated object, which can be slow and lead to memory fragmentation on an embedded system.
|
||||
|
||||
This abstraction is "useless" because it doesn't provide significant benefits to outweigh its complexity and performance costs. A flatter, more direct data model is needed.
|
||||
|
||||
### 2.2. Inefficient Data Lookups (Performance Bottleneck)
|
||||
|
||||
Multiple critical code paths rely on linear iteration through arrays to find data. This is a major performance bottleneck, especially as the number of slaves and registers grows.
|
||||
|
||||
- `ModbusRTU::findCoilEntry`/`findRegisterEntry`: Iterates through `MAX_ADDRESSES_PER_SLAVE` entries to find a cached value.
|
||||
- `ModbusTCP::findMappingForAddress`: Iterates through all registered `MB_Registers` blocks to map a TCP address.
|
||||
- `Manager::getDeviceById`: Iterates through an array to find a device pointer.
|
||||
|
||||
These O(n) operations are called frequently and will slow down the entire system under load.
|
||||
|
||||
### 2.3. Non-Transactional Operations
|
||||
|
||||
The user explicitly requested "transactional" capabilities. The current system is not transactional. Each read/write is a discrete operation queued and executed independently. There is no mechanism to:
|
||||
|
||||
- Group multiple operations (e.g., a read-modify-write sequence) to ensure they execute without interruption.
|
||||
- Roll back a series of operations if one fails.
|
||||
- Ensure a consistent view of data across multiple registers at a single point in time.
|
||||
|
||||
This makes it difficult to implement complex control logic that relies on atomic updates to multiple registers.
|
||||
|
||||
### 2.4. Sequential Operation Queue (Performance Bottleneck)
|
||||
|
||||
The `ModbusRTU::operationQueue` is processed sequentially by the main `loop()`. The `handlePendingOperations` function finds and executes only *one* operation per call. This can lead to high latency for operations if the queue is long, as each operation must wait for the `minOperationInterval` plus the processing time of all preceding operations.
|
||||
|
||||
## 3. Short-Term Solutions (Tactical Fixes)
|
||||
|
||||
These solutions can be implemented quickly to get immediate performance gains.
|
||||
|
||||
### 3.1. Solution: Optimize Data Lookups
|
||||
|
||||
Replace the linear search for cached register values in `ModbusRTU` with a more efficient data structure. Since this is an embedded system, we should avoid dynamic memory allocation found in `std::map`. A statically-allocated hash map is a good compromise.
|
||||
|
||||
**Code Example: A simple hash map for `SlaveData`**
|
||||
|
||||
This requires modifying `ModbusTypes.h` to change the `SlaveData` structure.
|
||||
|
||||
```cpp:lib/polymech-base/src/modbus/ModbusTypes.h
|
||||
// ... existing code ...
|
||||
#include "constants.h"
|
||||
#include "config-modbus.h"
|
||||
|
||||
// --- HASH MAP CONFIG FOR SLAVE DATA ---
|
||||
#define SLAVE_DATA_MAP_SIZE (MAX_ADDRESSES_PER_SLAVE * 2) // Simple hash map size
|
||||
|
||||
// Structure to represent a register or coil value entry
|
||||
struct ModbusValueEntry
|
||||
{
|
||||
// ... existing code ...
|
||||
uint16_t address; // 2 bytes
|
||||
uint16_t value; // 2 bytes
|
||||
uint8_t flags; // 1 byte for flags (used, synchronized)
|
||||
ModbusValueEntry* next; // Pointer for separate chaining in case of collision
|
||||
// ... existing code ...
|
||||
ModbusValueEntry()
|
||||
: lastUpdate(0), address(0), value(0), flags(0), next(nullptr) {}
|
||||
|
||||
ModbusValueEntry(uint16_t addr, uint16_t val, bool sync = true)
|
||||
: lastUpdate(millis()), address(addr), value(val), flags(0), next(nullptr)
|
||||
// ... existing code ...
|
||||
};
|
||||
|
||||
// Structure to represent a Modbus slave's data
|
||||
struct SlaveData
|
||||
{
|
||||
// --- NEW HASH MAP IMPLEMENTATION ---
|
||||
ModbusValueEntry entryPool[MAX_ADDRESSES_PER_SLAVE * 2]; // Pool of entries for coils and registers
|
||||
ModbusValueEntry* coilMap[SLAVE_DATA_MAP_SIZE];
|
||||
ModbusValueEntry* registerMap[SLAVE_DATA_MAP_SIZE];
|
||||
uint16_t entryPoolIndex;
|
||||
// --- END NEW HASH MAP ---
|
||||
|
||||
uint8_t coilCount;
|
||||
uint8_t registerCount;
|
||||
uint8_t slaveId;
|
||||
|
||||
SlaveData() : entryPoolIndex(0), coilCount(0), registerCount(0), slaveId(0)
|
||||
{
|
||||
clear();
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
entryPoolIndex = 0;
|
||||
coilCount = 0;
|
||||
registerCount = 0;
|
||||
slaveId = 0;
|
||||
// Initialize hash maps to nullptr
|
||||
for (int i = 0; i < SLAVE_DATA_MAP_SIZE; ++i) {
|
||||
coilMap[i] = nullptr;
|
||||
registerMap[i] = nullptr;
|
||||
}
|
||||
// Invalidate all entries in the pool
|
||||
for (int i = 0; i < MAX_ADDRESSES_PER_SLAVE * 2; ++i) {
|
||||
CBI(entryPool[i].flags, OP_USED_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
// Simple hash function
|
||||
static uint16_t hash(uint16_t address) {
|
||||
return address % SLAVE_DATA_MAP_SIZE;
|
||||
}
|
||||
};
|
||||
|
||||
// ... existing code ...
|
||||
```
|
||||
|
||||
You would then update `ModbusRTU::findRegisterEntry`, `createRegisterEntry` and their coil counterparts to use this hash map, which would change the lookup from O(n) to O(1) on average.
|
||||
|
||||
### 3.2. Solution: Flatten the Abstraction
|
||||
|
||||
Deprecate the `RTU_Base` and `RegisterState` classes for state management. Interact with the `ModbusRTU` cache directly from your main application logic or components. This eliminates state duplication and simplifies the data flow significantly.
|
||||
|
||||
**Code Example: Direct interaction**
|
||||
|
||||
```cpp
|
||||
// In your main application logic or a component that needs to write a value
|
||||
void setHeaterTemperature(ModbusRTU& modbus, uint8_t slaveId, uint16_t temp)
|
||||
{
|
||||
uint16_t address = 0x100A; // Address for heater setpoint
|
||||
|
||||
// The component logic now directly queues the write operation.
|
||||
// The ModbusRTU class handles caching, retries, etc.
|
||||
modbus.writeRegister(slaveId, address, temp);
|
||||
}
|
||||
|
||||
// In your main loop, to read a value
|
||||
void checkHeaterStatus(ModbusRTU& modbus, uint8_t slaveId)
|
||||
{
|
||||
uint16_t address = 0x200B; // Address for heater status
|
||||
uint16_t status;
|
||||
|
||||
// Directly get the value from the ModbusRTU cache.
|
||||
// The return value indicates if the value is available in the cache.
|
||||
if (modbus.getRegisterValue(slaveId, address, status)) {
|
||||
if (status == 1) {
|
||||
// Heater is on
|
||||
}
|
||||
} else {
|
||||
// Value not yet synchronized, maybe queue a read
|
||||
modbus.readRegister(slaveId, address);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This change simplifies the architecture by removing an entire layer of objects and indirect calls.
|
||||
|
||||
## 4. Long-Term Solutions (Strategic Changes)
|
||||
|
||||
### 4.1. Solution: Implement Transactional Operations
|
||||
|
||||
Modify the queuing mechanism to support transactions. A transaction would be an atomic group of operations.
|
||||
|
||||
1. **Define a `ModbusTransaction` struct:** This struct would contain an array of `ModbusOperation`s.
|
||||
2. **Change the Queue:** The `operationQueue` in `ModbusRTU` would become a queue of `ModbusTransaction` pointers.
|
||||
3. **Update `handlePendingOperations`:** The loop would grab the next transaction from the queue and execute all operations within it sequentially, without allowing other operations to interrupt. If any operation in the transaction fails, the entire transaction is marked as failed.
|
||||
|
||||
**Conceptual Code:**
|
||||
|
||||
```cpp
|
||||
// In ModbusTypes.h
|
||||
struct ModbusTransaction {
|
||||
ModbusOperation operations[MAX_OPS_PER_TRANSACTION];
|
||||
uint8_t opCount;
|
||||
// ... other metadata like transaction ID, status, etc.
|
||||
};
|
||||
|
||||
// In ModbusRTU.h
|
||||
class ModbusRTU {
|
||||
// ...
|
||||
private:
|
||||
ModbusTransaction transactionQueue[MAX_PENDING_TRANSACTIONS];
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
This architectural change is significant but is the only robust way to meet the "transactional" requirement.
|
||||
|
||||
### 4.2. Solution: Overhaul State Management
|
||||
|
||||
As a long-term goal, create a single, unified data model for all Modbus state (both RTU and TCP). This could be a "global register map" implemented as a single contiguous block of memory or a more sophisticated data structure.
|
||||
|
||||
- **Single Source of Truth:** All components read from and write to this central map.
|
||||
- **Efficient Access:** The map would be indexed directly by address, providing O(1) access.
|
||||
- **Gateway Logic:** The `ModbusTCP` and `ModbusRTU` classes become stateless gateways that simply translate requests into reads/writes on this central map.
|
||||
- **Synchronization:** A separate mechanism (like a dirty flag system) would trigger RTU writes when values in the map are changed by the application logic.
|
||||
|
||||
This would represent a fundamental shift from the current object-oriented, distributed state model to a centralized, data-centric one, leading to massive gains in performance and simplicity.
|
||||
|
||||
## 5. Bidirectional Data Flow Diagrams
|
||||
|
||||
Based on the refined architecture (`App` -> `NetworkComponent` -> `NetworkValue`), these diagrams model the two primary data flow directions.
|
||||
|
||||
### 5.1. Data Flow: From Device to Controller (RTU -> TCP)
|
||||
|
||||
This diagram shows how a value read from a physical RTU device gets updated internally, making it available for a Modbus TCP client to read.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Physical Device
|
||||
participant ModbusRTU
|
||||
participant PHApp
|
||||
participant NetworkComponent as OmronE5
|
||||
participant NetworkValue
|
||||
|
||||
Note over Physical Device, NetworkValue: Goal: Update NetworkValue with new sensor reading from device.
|
||||
|
||||
ModbusRTU->>Physical Device: Queues and sends read request (e.g., Read Holding Registers)
|
||||
Physical Device-->>ModbusRTU: Responds with data
|
||||
|
||||
activate ModbusRTU
|
||||
ModbusRTU->>ModbusRTU: onDataReceived(message, token)
|
||||
ModbusRTU->>ModbusRTU: _processReadResponse(op, message)
|
||||
ModbusRTU->>ModbusRTU: updateRegisterValue(slaveId, addr, newValue)
|
||||
Note right of ModbusRTU: Cache is updated and change is detected.
|
||||
ModbusRTU->>PHApp: onRegisterChangeCallback(op, oldValue, newValue)
|
||||
deactivate ModbusRTU
|
||||
|
||||
activate PHApp
|
||||
PHApp->>PHApp: Find component by op.slaveId (e.g., OmronE5 instance)
|
||||
PHApp->>NetworkComponent: onRegisterUpdate(op.address, newValue)
|
||||
deactivate PHApp
|
||||
|
||||
activate NetworkComponent
|
||||
NetworkComponent->>NetworkComponent: Map RTU address to correct NetworkValue
|
||||
NetworkComponent->>NetworkValue: update(newValue)
|
||||
deactivate NetworkComponent
|
||||
|
||||
activate NetworkValue
|
||||
NetworkValue->>NetworkValue: Check against threshold, update internal state
|
||||
Note right of NetworkValue: The value is now updated and ready for TCP clients.
|
||||
deactivate NetworkValue
|
||||
|
||||
```
|
||||
|
||||
### 5.2. Data Flow: From Controller to Device (TCP -> RTU)
|
||||
|
||||
This diagram shows how a write command from a Modbus TCP client gets propagated down to the physical RTU device.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant TCP Client
|
||||
participant ModbusTCP
|
||||
participant PHApp
|
||||
participant NetworkComponent as OmronE5
|
||||
participant ModbusRTU
|
||||
|
||||
Note over TCP Client, ModbusRTU: Goal: Write a new setpoint from a client to the physical device.
|
||||
|
||||
TCP Client->>ModbusTCP: Write request (e.g., Write Single Register at TCP Address)
|
||||
|
||||
activate ModbusTCP
|
||||
ModbusTCP->>ModbusTCP: writeSingleHregWorker(request)
|
||||
ModbusTCP->>ModbusTCP: findMappingForAddress(tcpAddress)
|
||||
Note right of ModbusTCP: Mapping contains the target componentId.
|
||||
ModbusTCP->>PHApp: byId(componentId)
|
||||
deactivate ModbusTCP
|
||||
|
||||
activate PHApp
|
||||
PHApp->>NetworkComponent: mb_tcp_write(reg, value)
|
||||
deactivate PHApp
|
||||
|
||||
activate NetworkComponent
|
||||
NetworkComponent->>NetworkComponent: Map TCP address to internal action (e.g., setSP)
|
||||
NetworkComponent->>ModbusRTU: writeRegister(slaveId, rtuAddress, value)
|
||||
Note right of NetworkComponent: Component logic translates TCP write to an RTU write request.
|
||||
deactivate NetworkComponent
|
||||
|
||||
activate ModbusRTU
|
||||
ModbusRTU->>ModbusRTU: queueOperation(op)
|
||||
Note right of ModbusRTU: The write operation is now in the queue.
|
||||
deactivate ModbusRTU
|
||||
|
||||
loop RTU Processing Loop
|
||||
ModbusRTU->>ModbusRTU: process()
|
||||
ModbusRTU->>ModbusRTU: handlePendingOperations()
|
||||
ModbusRTU->>Physical Device: Sends the write command
|
||||
end
|
||||
```
|
||||
|
||||
## 6. Verifying a Transaction (`setSP` Example)
|
||||
|
||||
Verifying that a write operation is truly complete requires more than just sending a command. A robust verification process confirms that the device has not only acknowledged the command but has also correctly updated its internal state. The most reliable method for this is a **Read-After-Write** sequence.
|
||||
|
||||
This process involves two phases:
|
||||
1. **Write Acknowledgment**: Confirming the device received the write command.
|
||||
2. **Value Confirmation**: Reading the value back from the device to ensure it matches the value that was written.
|
||||
|
||||
The following diagram illustrates this closed-loop verification flow for setting an Omron SP.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant AppLogic as "App Logic (e.g., OmronE5)"
|
||||
participant ModbusRTU
|
||||
participant PhysicalDevice as "Omron Device"
|
||||
|
||||
Note over AppLogic, PhysicalDevice: Goal: Set SP to 150 and verify it was written correctly.
|
||||
|
||||
AppLogic->>ModbusRTU: writeRegister(slaveId, SP_Address, 150)
|
||||
AppLogic->>AppLogic: state = WAITING_WRITE_ACK; pendingSP = 150
|
||||
|
||||
ModbusRTU->>PhysicalDevice: Sends write command
|
||||
PhysicalDevice-->>ModbusRTU: Write Acknowledged
|
||||
|
||||
activate ModbusRTU
|
||||
ModbusRTU->>ModbusRTU: onDataReceived() for write
|
||||
ModbusRTU->>AppLogic: onWriteCallback(op for SP write)
|
||||
deactivate ModbusRTU
|
||||
|
||||
activate AppLogic
|
||||
AppLogic->>AppLogic: if (op.address == SP_Address and state == WAITING_WRITE_ACK)
|
||||
AppLogic->>ModbusRTU: readRegister(slaveId, SP_Address)
|
||||
AppLogic->>AppLogic: state = WAITING_READ_VERIFY
|
||||
deactivate AppLogic
|
||||
|
||||
ModbusRTU->>PhysicalDevice: Sends read command
|
||||
PhysicalDevice-->>ModbusRTU: Responds with SP value
|
||||
|
||||
activate ModbusRTU
|
||||
ModbusRTU->>ModbusRTU: onDataReceived() for read
|
||||
ModbusRTU->>AppLogic: onRegisterChangeCallback(op, oldValue, newValue)
|
||||
deactivate ModbusRTU
|
||||
|
||||
activate AppLogic
|
||||
AppLogic->>AppLogic: if (op.address == SP_Address and state == WAITING_READ_VERIFY)
|
||||
AppLogic->>AppLogic: if (newValue == pendingSP) -> SUCCESS!
|
||||
AppLogic->>AppLogic: else -> FAILED!
|
||||
AppLogic->>AppLogic: state = IDLE
|
||||
deactivate AppLogic
|
||||
```
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
- **State Management**: The component initiating the write (`OmronE5` in this case) needs a simple state machine to track the verification process (e.g., `IDLE`, `WAITING_WRITE_ACK`, `WAITING_READ_VERIFY`).
|
||||
- **Callback Logic**: The core logic is implemented in the `onWriteCallback` and `onRegisterChangeCallback` handlers. These handlers must inspect the incoming `ModbusOperation` to ensure they are acting on the correct address for the correct slave.
|
||||
- **Flattened Abstraction**: This verification model works best with the "Flatten the Abstraction" approach, where the `OmronE5` component interacts directly with `ModbusRTU` and its callbacks, rather than going through the `RTU_Base` layer.
|
||||
|
||||
## 7. Proposed New Class Design
|
||||
|
||||
Based on the goal of flattening the architecture, this new class design eliminates the `RTU_Base`, `RegisterState`, and `Manager` abstractions. `PHApp` becomes the central orchestrator connecting the Modbus services (`ModbusRTU`, `ModbusTCP`) to the various `NetworkComponent`s that represent devices.
|
||||
|
||||
### 7.1. Class Diagram
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class PHApp {
|
||||
+ModbusRTU* modbusRTU
|
||||
+ModbusTCP* modbusTCP
|
||||
+OmronE5* omron_devices[]
|
||||
+byId(ushort id) Component*
|
||||
+setup()
|
||||
+loop()
|
||||
#onRegisterChange(op, oldValue, newValue)
|
||||
}
|
||||
|
||||
class ModbusRTU {
|
||||
-SlaveData _slaveData[]
|
||||
+writeRegister(slaveId, address, value)
|
||||
+readRegister(slaveId, address)
|
||||
+getRegisterValue(slaveId, address, &value)
|
||||
+setOnRegisterChangeCallback(callback)
|
||||
+setOnWriteCallback(callback)
|
||||
}
|
||||
|
||||
class ModbusTCP {
|
||||
+registerModbus(component, info)
|
||||
}
|
||||
|
||||
class NetworkComponent {
|
||||
<<Abstract>>
|
||||
#_networkValues: vector<NetworkValueBase*>
|
||||
+virtual onRegisterUpdate(address, newValue)
|
||||
+mb_tcp_write(reg, value)
|
||||
+mb_tcp_read(reg)
|
||||
}
|
||||
|
||||
class OmronE5 {
|
||||
-NetworkValue m_pv
|
||||
-NetworkValue m_sp
|
||||
+setSP(uint16_t value)
|
||||
+onRegisterUpdate(address, newValue) override
|
||||
}
|
||||
|
||||
class NetworkValue {
|
||||
-T m_value
|
||||
+update(newValue)
|
||||
+getValue() T
|
||||
}
|
||||
|
||||
class SlaveData {
|
||||
#coilMap[]
|
||||
#registerMap[]
|
||||
#entryPool[]
|
||||
}
|
||||
|
||||
PHApp o-- "1" ModbusRTU : owns
|
||||
PHApp o-- "1" ModbusTCP : owns
|
||||
PHApp o-- "*" OmronE5 : owns device components
|
||||
|
||||
ModbusTCP ..> PHApp : Finds target component via byId() for writes
|
||||
ModbusRTU ..> PHApp : Fires callbacks (e.g. onRegisterChange)
|
||||
|
||||
Component <|-- NetworkComponent
|
||||
NetworkComponent <|-- OmronE5
|
||||
|
||||
NetworkComponent o-- "*" NetworkValue : manages
|
||||
|
||||
OmronE5 ..> ModbusRTU : Directly queues write operations
|
||||
PHApp ..> OmronE5 : Routes callbacks via onRegisterUpdate()
|
||||
|
||||
ModbusRTU o-- "*" SlaveData : Manages RTU device cache
|
||||
```
|
||||
|
||||
### 7.2. Key Changes in this Design
|
||||
|
||||
1. **`RTU_Base` is Eliminated**: `OmronE5` now inherits directly from `NetworkComponent` (which inherits from `Component`), removing the complex and RTU-specific `RTU_Base` class.
|
||||
2. **`RegisterState` is Eliminated**: The state of a register is held in two places only: the `ModbusRTU` cache (`SlaveData`) as the ground truth from the bus, and the `NetworkValue` within the specific component (`OmronE5`) for application logic. There is no third, intermediate `RegisterState` object.
|
||||
3. **`Manager` is Eliminated**: The `Manager` class, which was responsible for routing callbacks to `RTU_Base` devices, is no longer needed.
|
||||
4. **`PHApp` as the Router**: `PHApp` takes on the role of the central router. It subscribes to `ModbusRTU`'s callbacks. When a register changes, `PHApp` receives the callback, identifies which device component is affected (based on slave ID), and calls a method like `onRegisterUpdate()` on that specific component instance.
|
||||
5. **Direct Command Path**: When `OmronE5` needs to write a value (e.g., in its `setSP` method), it now calls `modbusRTU->writeRegister()` directly, rather than manipulating an internal `RegisterState` and waiting for a sync loop.
|
||||
|
||||
This new architecture is much cleaner, reduces memory overhead, and aligns with the data flow diagrams. It makes the system more performant and easier to reason about.
|
||||
301
docs/modbus-rtu.md
Normal file
@ -0,0 +1,301 @@
|
||||
# Modbus RTU Client Subsystem Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the classes, structures, and enums involved in the Modbus RTU Client functionality within the firmware. This subsystem is primarily managed by the `ModbusRTU` class and is responsible for communicating with downstream Modbus RTU slave devices over a serial (RS485) connection. It often works in conjunction with the Modbus TCP server (`ModbusManager`) to act as a TCP-to-RTU gateway.
|
||||
|
||||
The core tasks include queuing read/write operations, managing communication timing, handling retries and errors, caching slave data, and providing an interface to interact with the RTU devices.
|
||||
|
||||
## Key Classes
|
||||
|
||||
### `ModbusRTU`
|
||||
|
||||
* **Header:** `src/ModbusRTU.h`
|
||||
* **Source:** `src/ModbusRTU.cpp`
|
||||
* **Purpose:** The central class managing Modbus RTU client communication. It uses the `eModbus` library's `ModbusClientRTU` internally.
|
||||
* **Responsibilities:**
|
||||
* Initializes the RTU client and serial port.
|
||||
* Manages a queue (`operationQueue`) of pending `ModbusOperation` requests.
|
||||
* Handles the execution of operations, respecting timing intervals (`minOperationInterval`).
|
||||
* Processes responses and errors from the `eModbus` library via callbacks (`onDataReceived`, `onErrorReceived`).
|
||||
* Maintains a cache (`slaveData`) of the last known values for registers and coils on each slave device using `SlaveData` and `ModbusValueEntry`.
|
||||
* Provides public methods (`readCoil`, `writeRegister`, etc.) to queue operations.
|
||||
* Manages an optional filter chain (`ModbusOperationFilter`) to preprocess outgoing operations (e.g., prevent duplicates, rate limit).
|
||||
* Provides status information (error counts, success rates).
|
||||
* Handles adaptive timing adjustments if `ENABLE_ADAPTIVE_TIMEOUT` is defined.
|
||||
|
||||
### `BaseModbusDevice`
|
||||
|
||||
* **Header:** `src/ModbusTypes.h`
|
||||
* **Source:** `src/ModbusTypes.cpp`
|
||||
* **Purpose:** Represents the state and basic interaction logic for a single external Modbus RTU slave device being managed by `ModbusRTU`.
|
||||
* **Responsibilities:**
|
||||
* Stores the slave ID (`deviceId`).
|
||||
* Tracks the device's communication state (`E_DeviceState`: IDLE, RUNNING, ERROR).
|
||||
* Manages communication timing (timeout, sync interval) and error counts for the specific device.
|
||||
* Holds collections (`inputRegisters`, `outputRegisters`) of `RegisterState` objects representing the data points on the slave.
|
||||
* Provides methods to add/manage `RegisterState` objects (`addInputRegister`, `addOutputRegister`).
|
||||
* Provides methods to interact with the device's registers via the `ModbusRTU` manager (`initialize`, `syncFromDevice`, `writeOutputs`).
|
||||
* Provides local accessors for register values (`getInputRegisterValue`, `getOutputRegisterValue`, `setOutputRegisterValue`).
|
||||
* Includes helper methods for state management (`printState`, `reset`, `runTestSequence`).
|
||||
|
||||
### `ModbusDeviceState`
|
||||
|
||||
* **Header:** `src/ModbusDeviceState.h`
|
||||
* **Source:** `src/ModbusDeviceState.cpp`
|
||||
* **Purpose:** A derived class of `BaseModbusDevice`. Intended for application-specific implementations or specializations of Modbus RTU slave devices.
|
||||
* **Responsibilities:**
|
||||
* Currently, mainly overrides `setState` to add specific logging.
|
||||
* Its constructor can be used to add specific default registers required by the application logic using the `addInputRegister`/`addOutputRegister` methods inherited from `BaseModbusDevice`.
|
||||
|
||||
### `RegisterState`
|
||||
|
||||
* **Header:** `src/ModbusTypes.h`
|
||||
* **Source:** `src/ModbusTypes.cpp`
|
||||
* **Purpose:** Represents a single data point (Input Register, Holding Register, Coil, or Discrete Input) on an external Modbus RTU slave device.
|
||||
* **Responsibilities:**
|
||||
* Stores the register type (`RegType`), Modbus address, current value, min/max values (for validation), and priority.
|
||||
* Provides methods (`readFromDevice`, `writeToDevice`) to queue corresponding read/write operations via the `ModbusRTU` manager.
|
||||
* Includes helper methods for boolean conversion (`getBoolValue`, `setBoolValue`) and printing state (`printState`).
|
||||
|
||||
## Supporting Structures
|
||||
|
||||
### `ModbusOperation`
|
||||
|
||||
* **Header:** `src/ModbusTypes.h`
|
||||
* **Purpose:** Represents a single Modbus read or write request waiting to be executed or currently in progress. Stored in the `ModbusRTU`'s `operationQueue`.
|
||||
* **Key Members:**
|
||||
* `timestamp`: When the operation was created.
|
||||
* `token`: Unique identifier used by `eModbus` library for asynchronous handling.
|
||||
* `address`, `value`, `quantity`: Modbus request parameters.
|
||||
* `slaveId`: Target slave device.
|
||||
* `type`: The type of operation (`E_MB_OpType`).
|
||||
* `status`: Current status (`E_MB_OpStatus`: PENDING, SUCCESS, FAILED, RETRYING).
|
||||
* `retries`: Number of times execution has been attempted.
|
||||
* `flags`: Bit flags indicating state (USED, HIGH_PRIORITY, IN_PROGRESS, BROADCAST, SYNCHRONIZED).
|
||||
|
||||
### `ModbusValueEntry`
|
||||
|
||||
* **Header:** `src/ModbusTypes.h`
|
||||
* **Purpose:** Represents a cached value for a single register or coil address on a specific slave. Used within the `SlaveData` structure.
|
||||
* **Key Members:**
|
||||
* `address`: The Modbus address.
|
||||
* `value`: The last known value.
|
||||
* `lastUpdate`: Timestamp of the last update.
|
||||
* `synchronized`: Flag indicating if the cached value is believed to be in sync with the device.
|
||||
* `used`: Flag indicating if this entry is actively used.
|
||||
|
||||
### `SlaveData`
|
||||
|
||||
* **Header:** `src/ModbusTypes.h`
|
||||
* **Purpose:** Holds the cached data for a single Modbus RTU slave device managed by `ModbusRTU`. An array of `SlaveData` is stored in `ModbusRTU`.
|
||||
* **Key Members:**
|
||||
* `coils`: Fixed-size array of `ModbusValueEntry` for coils.
|
||||
* `registers`: Fixed-size array of `ModbusValueEntry` for registers.
|
||||
* `coilCount`, `registerCount`: Number of used entries in the arrays.
|
||||
|
||||
## Operation Filtering
|
||||
|
||||
### `ModbusOperationFilter` (Base Class)
|
||||
|
||||
* **Header:** `src/ModbusTypes.h`
|
||||
* **Purpose:** Abstract base class for filters that can be chained together to process `ModbusOperation` requests before they are queued for execution by `ModbusRTU`.
|
||||
* **Key Methods:**
|
||||
* `filter(op)`: Pure virtual method; derived classes implement logic here. Returns `true` to allow the operation, `false` to drop it.
|
||||
* `process(op)`: Processes the operation through the current filter and the rest of the chain.
|
||||
* `setNext(filter*)`, `getNext()`: Manage the filter chain links.
|
||||
* `notifyOperationExecuted(op)`, `notifyOperationCompleted(op)`: Optional methods for stateful filters.
|
||||
* `getType()`: Returns the filter type (`E_FilterType`).
|
||||
|
||||
### Derived Filter Classes
|
||||
|
||||
* **Header:** `src/ModbusTypes.h`
|
||||
* **Source:** `src/ModbusTypes.cpp`
|
||||
* **Classes:**
|
||||
* `DuplicateOperationFilter`: Prevents queuing an operation if an identical one (same slave, type, address) is already pending in the `ModbusRTU` queue. Requires a pointer to the `ModbusRTU` instance.
|
||||
* `RateLimitFilter`: Enforces a minimum time interval between consecutive operations passing through it.
|
||||
* `PriorityFilter`: Currently a placeholder; intended to potentially adjust operation priority flags (though filtering itself always returns true).
|
||||
* `OperationLifecycleFilter`: Drops operations that have exceeded `MAX_RETRIES` or have timed out (`OPERATION_TIMEOUT`).
|
||||
|
||||
## Key Enums
|
||||
|
||||
* **`E_MB_OpType`**: (`src/ModbusTypes.h`) Defines the type of Modbus operation (READ_COIL, WRITE_REGISTER, etc.).
|
||||
* **`E_MB_OpStatus`**: (`src/ModbusTypes.h`) Defines the status of a queued `ModbusOperation` (PENDING, SUCCESS, FAILED, RETRYING).
|
||||
* **`MB_Error`**: (`src/ModbusTypes.h`) Comprehensive error codes, combining standard Modbus exceptions, internal queue/operation errors, and `eModbus` communication errors.
|
||||
* **`E_FilterType`**: (`src/ModbusTypes.h`) Identifies the type of a `ModbusOperationFilter`.
|
||||
* **`E_DeviceState`**: (`src/ModbusTypes.h`) State of a `BaseModbusDevice` (IDLE, RUNNING, ERROR).
|
||||
* **`RegType`**: (`src/ModbusTypes.h`) Type of a `RegisterState` (INPUT, HOLDING, COIL, DISCRETE_INPUT).
|
||||
* **`E_InitState`**: (`src/ModbusRTU.h`) Internal state for the `ModbusRTU` class initialization process.
|
||||
|
||||
## Key Constants
|
||||
|
||||
* **`MAX_MODBUS_SLAVES`**: (`src/ModbusTypes.h`) Max number of RTU slaves the client can manage.
|
||||
* **`MAX_ADDRESSES_PER_SLAVE`**: (`src/ModbusTypes.h`) Size of the `ModbusValueEntry` arrays within `SlaveData`.
|
||||
* **`MAX_PENDING_OPERATIONS`**: (`src/ModbusTypes.h`) Size of the `ModbusRTU` operation queue.
|
||||
* **`MAX_RETRIES`**: (`src/ModbusTypes.h`) Default max retries for a failing operation.
|
||||
* **`OPERATION_TIMEOUT`**: (`src/ModbusTypes.h`) Default timeout for an operation before being considered failed/expired by `OperationLifecycleFilter`.
|
||||
* **`BAUDRATE`**: (`src/ModbusTypes.h`) Default serial baud rate for RS485.
|
||||
* **`PRIORITY_*` Defines**: (`src/ModbusTypes.h`) Constants used for setting `RegisterState` priorities.
|
||||
* **`OP_FLAG_*` Defines**: (`src/ModbusTypes.h`) Bit flags used within `ModbusOperation.flags`.
|
||||
|
||||
## Callback Functions
|
||||
|
||||
* **`ResponseCallback`**: (`src/ModbusTypes.h`) typedef `void (*ResponseCallback)(uint8_t slaveId)`. Can be set on `ModbusRTU` to trigger custom logic when *any* response (success or error) is received for a slave.
|
||||
* **`OnRegisterChangeCallback`**: (`src/ModbusTypes.h`) typedef `void (*OnRegisterChangeCallback)(const ModbusOperation& op, uint16_t oldValue, uint16_t newValue)`. Set on `ModbusRTU` to be notified when a successful read operation results in a changed value in the cache.
|
||||
* **`OnWriteCallback`**: (`src/ModbusTypes.h`) typedef `void (*OnWriteCallback)(const ModbusOperation& op)`. Set on `ModbusRTU` to be notified when a write operation completes successfully.
|
||||
* **`OnErrorCallback`**: (`src/ModbusTypes.h`) typedef `void (*OnErrorCallback)(const ModbusOperation& op, int errorCode, const char* errorMessage)`. Set on `ModbusRTU` to be notified when an operation fails (either timeout, Modbus exception, or internal error).
|
||||
|
||||
## Mermaid Diagrams
|
||||
|
||||
### Core Class Relationships
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class ModbusRTU {
|
||||
+MB_Error begin(HardwareSerial&, uint32_t)
|
||||
+MB_Error process()
|
||||
+MB_Error readCoil(uint8_t, uint16_t)
|
||||
+MB_Error writeRegister(uint8_t, uint16_t, uint16_t)
|
||||
+bool getCoilValue(uint8_t, uint16_t, bool&)
|
||||
+bool getRegisterValue(uint8_t, uint16_t, uint16_t&)
|
||||
+bool isCoilSynchronized(uint8_t, uint16_t)
|
||||
+bool isRegisterSynchronized(uint8_t, uint16_t)
|
||||
+bool hasPendingOperations(uint8_t)
|
||||
+void addFilter(ModbusOperationFilter*)
|
||||
-SlaveData slaveData[MAX_MODBUS_SLAVES]
|
||||
-ModbusOperation operationQueue[MAX_PENDING_OPERATIONS]
|
||||
-ModbusOperationFilter* firstFilter
|
||||
-MB_Error queueOperation(ModbusOperation, bool)
|
||||
-void onDataReceived(ModbusMessage, uint32_t)
|
||||
-void onErrorReceived(Error, uint32_t)
|
||||
-ModbusValueEntry* findCoilEntry(...)
|
||||
-ModbusValueEntry* createCoilEntry(...)
|
||||
-ModbusValueEntry* findRegisterEntry(...)
|
||||
-ModbusValueEntry* createRegisterEntry(...)
|
||||
}
|
||||
|
||||
class BaseModbusDevice {
|
||||
+uint8_t deviceId
|
||||
+E_DeviceState state
|
||||
+unsigned long syncInterval
|
||||
+bool addInputRegister(uint16_t, uint8_t)
|
||||
+bool addOutputRegister(uint16_t, uint16_t, uint8_t)
|
||||
+void updateState(ModbusRTU&)
|
||||
+bool initialize(ModbusRTU&)
|
||||
+void syncFromDevice(ModbusRTU&)
|
||||
+void writeOutputs(ModbusRTU&)
|
||||
+void printState(ModbusRTU&)
|
||||
+void reset()
|
||||
+uint16_t getInputRegisterValue(uint16_t)
|
||||
+uint16_t getOutputRegisterValue(uint16_t)
|
||||
+void setOutputRegisterValue(uint16_t, uint16_t)
|
||||
#RegisterState* inputRegisters[]
|
||||
#RegisterState* outputRegisters[]
|
||||
#int inputRegCount
|
||||
#int outputRegCount
|
||||
}
|
||||
|
||||
class ModbusDeviceState {
|
||||
+ModbusDeviceState(uint8_t)
|
||||
+void setState(E_DeviceState) override
|
||||
}
|
||||
|
||||
class RegisterState {
|
||||
+RegType type
|
||||
+uint16_t address
|
||||
+uint16_t value
|
||||
+uint8_t priority
|
||||
+MB_Error readFromDevice(ModbusRTU&, uint8_t)
|
||||
+MB_Error writeToDevice(ModbusRTU&, uint8_t)
|
||||
+void printState(ModbusRTU&, uint8_t)
|
||||
+bool getBoolValue()
|
||||
}
|
||||
|
||||
class ModbusOperation {
|
||||
+unsigned long timestamp
|
||||
+uint32_t token
|
||||
+uint16_t address
|
||||
+uint16_t value
|
||||
+uint16_t quantity
|
||||
+uint8_t slaveId
|
||||
+E_MB_OpType type
|
||||
+E_MB_OpStatus status
|
||||
+uint8_t flags
|
||||
}
|
||||
|
||||
class SlaveData {
|
||||
#ModbusValueEntry coils[]
|
||||
#ModbusValueEntry registers[]
|
||||
#uint8_t coilCount
|
||||
#uint8_t registerCount
|
||||
}
|
||||
|
||||
class ModbusValueEntry {
|
||||
+unsigned long lastUpdate
|
||||
+uint16_t address
|
||||
+uint16_t value
|
||||
+bool synchronized
|
||||
+bool used
|
||||
}
|
||||
|
||||
ModbusRTU o-- "0..MAX_MODBUS_SLAVES" SlaveData : contains cache
|
||||
ModbusRTU o-- "0..MAX_PENDING_OPERATIONS" ModbusOperation : queues
|
||||
ModbusRTU ..> ModbusOperationFilter : uses chain
|
||||
BaseModbusDevice o-- "0..MAX_INPUT_REGISTERS" RegisterState : manages inputs
|
||||
BaseModbusDevice o-- "0..MAX_OUTPUT_REGISTERS" RegisterState : manages outputs
|
||||
BaseModbusDevice ..> ModbusRTU : interacts via
|
||||
ModbusDeviceState --|> BaseModbusDevice : inherits from
|
||||
SlaveData o-- "0..MAX_ADDRESSES_PER_SLAVE" ModbusValueEntry : caches coil values
|
||||
SlaveData o-- "0..MAX_ADDRESSES_PER_SLAVE" ModbusValueEntry : caches register values
|
||||
RegisterState ..> ModbusRTU : interacts via
|
||||
|
||||
```
|
||||
|
||||
### Filter Chain Relationships
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class ModbusOperationFilter {
|
||||
<<Abstract>>
|
||||
+process(ModbusOperation) bool
|
||||
+setNext(ModbusOperationFilter*)
|
||||
+getType() E_FilterType
|
||||
#ModbusOperationFilter* nextFilter
|
||||
+$ filter(ModbusOperation) bool
|
||||
+$ notifyOperationExecuted(ModbusOperation) void
|
||||
+$ notifyOperationCompleted(ModbusOperation) void
|
||||
}
|
||||
|
||||
class DuplicateOperationFilter {
|
||||
+DuplicateOperationFilter(ModbusRTU*)
|
||||
+filter(ModbusOperation) bool
|
||||
-ModbusRTU* modbusRTU
|
||||
}
|
||||
|
||||
class RateLimitFilter {
|
||||
+RateLimitFilter(ulong)
|
||||
+filter(ModbusOperation) bool
|
||||
-ulong minInterval
|
||||
-ulong lastOperationTime
|
||||
}
|
||||
|
||||
class PriorityFilter {
|
||||
+filter(ModbusOperation) bool
|
||||
+adjustPriority(ModbusOperation&) bool
|
||||
}
|
||||
|
||||
class OperationLifecycleFilter {
|
||||
+OperationLifecycleFilter(ulong, uint8_t)
|
||||
+filter(ModbusOperation) bool
|
||||
-ulong timeout
|
||||
-uint8_t maxRetries
|
||||
}
|
||||
|
||||
DuplicateOperationFilter --|> ModbusOperationFilter
|
||||
RateLimitFilter --|> ModbusOperationFilter
|
||||
PriorityFilter --|> ModbusOperationFilter
|
||||
OperationLifecycleFilter --|> ModbusOperationFilter
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
87
docs/mqtt-pid.md
Normal file
@ -0,0 +1,87 @@
|
||||
# PID Controller Logging via MQTT
|
||||
|
||||
This document outlines the setup for logging PID controller data to an InfluxDB instance via MQTT and how to visualize this data in Grafana.
|
||||
|
||||
## Firmware MQTT Setup
|
||||
|
||||
The firmware is configured to send PID controller data (Setpoint, Process Value, and heating state) to an MQTT broker. This data is then picked up by a Telegraf instance, which forwards it to InfluxDB.
|
||||
|
||||
### Enabling the Feature
|
||||
|
||||
To enable MQTT logging, ensure the following line is present and not commented out in `cassandra-rc2/src/config.h`:
|
||||
|
||||
```c++
|
||||
#define ENABLE_MQTT_LOGGING
|
||||
```
|
||||
|
||||
### MQTT Configuration
|
||||
|
||||
The MQTT broker details are configured in `cassandra-rc2/src/PHApp-Mqtt.cpp`. You may need to change the `MQTT_BROKER` IP address to match your environment.
|
||||
|
||||
```c++
|
||||
#define MQTT_BROKER "192.168.1.100" // Replace with your MQTT broker's IP address
|
||||
#define MQTT_PORT 1883
|
||||
```
|
||||
|
||||
### Data Format
|
||||
|
||||
The firmware sends data using the InfluxDB Line Protocol. The format is as follows:
|
||||
|
||||
- **Measurement:** `pid_controller`
|
||||
- **Tags:**
|
||||
- `controller_id`: (integer) The ID of the PID controller (e.g., 1, 2, ... 8).
|
||||
- **Fields:**
|
||||
- `sp`: (integer) The Setpoint value.
|
||||
- `pv`: (integer) The Process Value.
|
||||
- `heating`: (boolean) The heating state (`true` or `false`).
|
||||
|
||||
**Example Payload:**
|
||||
|
||||
```
|
||||
pid_controller,controller_id=1 sp=150,pv=148,heating=true
|
||||
```
|
||||
|
||||
This message is published to the topic `pid/controller/<id>`.
|
||||
|
||||
---
|
||||
|
||||
## Grafana: Adding InfluxDB Data Source
|
||||
|
||||
Follow these steps to connect your Grafana instance to the InfluxDB container.
|
||||
|
||||
1. **Navigate to Grafana:** Open your web browser and go to `http://localhost:3000`.
|
||||
|
||||
2. **Login:** Use the default credentials `admin` for both username and password, unless you have changed them. You will be prompted to change the password on first login.
|
||||
|
||||
3. **Go to Data Sources:**
|
||||
* In the left-hand menu, hover over the **Configuration** (cog) icon.
|
||||
* Click on **Data Sources**.
|
||||
|
||||
4. **Add Data Source:**
|
||||
* Click the **"Add new data source"** button.
|
||||
* Select **"InfluxDB"** from the list of time series databases.
|
||||
|
||||
5. **Configure the Connection:**
|
||||
|
||||
* **Name:** Give your data source a name, for example, `InfluxDB_PID`.
|
||||
* **Query Language:** Select **Flux**.
|
||||
* **URL:** Set the URL to `http://influxdb:8086`. Grafana and InfluxDB are in the same Docker network, so they can communicate using their service names.
|
||||
* **Authentication:** Leave the "Basic auth" and "With credentials" toggles off.
|
||||
* **Organization:** Enter `my-org`.
|
||||
* **Token:** Enter `my-super-token`.
|
||||
* **Default Bucket:** Enter `my-bucket`.
|
||||
* **Min time interval:** `10s` (optional, but good practice to match the Telegraf flush interval).
|
||||
|
||||
The credentials and settings are derived from your `docker-compose.yaml` file:
|
||||
|
||||
- `DOCKER_INFLUXDB_INIT_USERNAME`: **admin**
|
||||
- `DOCKER_INFLUXDB_INIT_PASSWORD`: **admin123**
|
||||
- `DOCKER_INFLUXDB_INIT_ORG`: **my-org**
|
||||
- `DOCKER_INFLUXDB_INIT_BUCKET`: **my-bucket**
|
||||
- `DOCKER_INFLUXDB_INIT_ADMIN_TOKEN`: **my-super-token**
|
||||
|
||||
6. **Save & Test:**
|
||||
* Click the **"Save & test"** button at the bottom of the page.
|
||||
* You should see a green notification saying "Data source is working".
|
||||
|
||||
You are now ready to create dashboards and query your PID controller data from InfluxDB in Grafana.
|
||||
254
docs/nc-mb.md
Normal file
@ -0,0 +1,254 @@
|
||||
# `ModbusTCP` as a Feature for `NetworkComponent`
|
||||
|
||||
This document outlines a design to refactor `NetworkComponent` to make network protocols like Modbus TCP modular, opt-in features. This approach increases flexibility, reduces the memory footprint for components that don't need network capabilities, and provides a clear path for adding other protocols (e.g., Serial, CAN) in the future.
|
||||
|
||||
## 1. Motivation
|
||||
|
||||
Currently, `NetworkComponent` is tightly coupled with Modbus TCP. Its constructor requires a Modbus `baseAddress`, and it contains member variables and methods specific to Modbus. This design has a few drawbacks:
|
||||
|
||||
- **Lack of Modularity**: Every component inheriting from `NetworkComponent` carries the overhead of Modbus, even if unused.
|
||||
- **Limited Extensibility**: Adding new network protocols is difficult without modifying the base `NetworkComponent` and `Component` classes.
|
||||
- **Inconsistency**: `NetworkValue` already uses a flexible feature system (for logging, notifications, etc.), which has proven effective. Aligning `NetworkComponent` with a similar pattern would create a more consistent and robust architecture.
|
||||
|
||||
The goal is to evolve `NetworkComponent` into a lean, protocol-agnostic base class and provide network capabilities via composable mixins, starting with Modbus TCP.
|
||||
|
||||
## 2. Proposed Design
|
||||
|
||||
The proposed solution is inspired by the `Persistent` mixin and involves two main steps:
|
||||
1. Refactor `NetworkComponent` to remove hard-coded Modbus TCP logic.
|
||||
2. Create a `WithModbus` mixin class that encapsulates the extracted Modbus TCP functionality.
|
||||
|
||||
This approach ensures that existing component implementations remain source-compatible.
|
||||
|
||||
### 2.1. Refactored `NetworkComponent` (Core)
|
||||
|
||||
The core `NetworkComponent` will be simplified to a generic manager for `NetworkValue`s, without any knowledge of specific network protocols.
|
||||
|
||||
**`lib/polymech-base/src/modbus/NetworkComponent.h` (Proposed Changes)**
|
||||
```cpp
|
||||
#ifndef NETWORK_COMPONENT_H
|
||||
#define NETWORK_COMPONENT_H
|
||||
|
||||
#include "Component.h"
|
||||
#include <vector>
|
||||
#include "NetworkValue.h"
|
||||
|
||||
// The template parameter N (number of NetworkValues) is kept for consistency.
|
||||
template <size_t N = 20>
|
||||
class NetworkComponent : public Component {
|
||||
protected:
|
||||
std::vector<NetworkValueBase *> _networkValues;
|
||||
NetworkValue<bool> m_enabled;
|
||||
|
||||
public:
|
||||
// The constructor no longer takes a Modbus-specific 'baseAddress'.
|
||||
template <typename... Args>
|
||||
NetworkComponent(Args &&... args)
|
||||
: Component(std::forward<Args>(args)...),
|
||||
m_enabled(this, this->id, "Enabled", /*...initial values...*/),
|
||||
// No modbus blocks are initialized here.
|
||||
{
|
||||
_networkValues.reserve(N);
|
||||
// Default capability is removed. It will be added by the feature mixin.
|
||||
// setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS);
|
||||
addNetworkValue(&m_enabled);
|
||||
}
|
||||
|
||||
virtual ~NetworkComponent() = default; // Destructor simplifies.
|
||||
|
||||
// Generic setup remains, but Modbus-specific calls are gone.
|
||||
short setup() override {
|
||||
Component::setup();
|
||||
// The m_enabled.initModbus() call is removed.
|
||||
return E_OK;
|
||||
}
|
||||
|
||||
// Generic loop logic.
|
||||
short loop() override {
|
||||
Component::loop();
|
||||
if (!this->enabled()) {
|
||||
return E_OK;
|
||||
}
|
||||
return loopNetwork();
|
||||
}
|
||||
|
||||
virtual short loopNetwork() { return E_OK; }
|
||||
|
||||
void addNetworkValue(NetworkValueBase *nv) {
|
||||
if (nv) {
|
||||
_networkValues.push_back(nv);
|
||||
}
|
||||
}
|
||||
|
||||
// onMessage can be simplified if it only contained protocol-specific logic.
|
||||
short onMessage(int id, E_CALLS verb, E_MessageFlags flags, void* user, Component *src) override {
|
||||
// If the Protobuf logic is also moved to a feature, this can be cleaner.
|
||||
if (verb == E_CALLS::EC_PROTOBUF_UPDATE && user != nullptr) {
|
||||
return this->owner->onMessage(id, verb, flags, user, src);
|
||||
}
|
||||
return Component::onMessage(id, verb, flags, user, src);
|
||||
}
|
||||
};
|
||||
|
||||
// Convenience macros for NetworkValues can be moved to the Modbus mixin
|
||||
// if they are only used in that context.
|
||||
// #define INIT_NETWORK_VALUE(...)
|
||||
// #define SETUP_NETWORK_VALUE(...)
|
||||
|
||||
#endif // NETWORK_COMPONENT_H
|
||||
```
|
||||
The virtual functions for Modbus (`mb_tcp_write`, `mb_tcp_read`, etc.) remain in the base `Component` class with default implementations. The refactored `NetworkComponent` simply inherits them without providing an implementation.
|
||||
|
||||
### 2.2. The `WithModbus` Mixin
|
||||
|
||||
A new header will define the `WithModbus` mixin. This class inherits from its template parameter `TBase` (which will be a `NetworkComponent` instantiation) and re-introduces all the Modbus TCP logic.
|
||||
|
||||
**`lib/polymech-base/src/mixins/WithModbus.h` (New File)**
|
||||
```cpp
|
||||
#ifndef MIXIN_WITH_MODBUS_H
|
||||
#define MIXIN_WITH_MODBUS_H
|
||||
|
||||
#include "modbus/NetworkComponent.h"
|
||||
#include "modbus/ModbusTCP.h"
|
||||
|
||||
template <class TBase, size_t N>
|
||||
class WithModbus : public TBase {
|
||||
protected:
|
||||
uint16_t _baseAddress;
|
||||
MB_Registers* _modbusBlocks;
|
||||
mutable ModbusBlockView _modbusBlockView;
|
||||
size_t _nextIndex;
|
||||
ModbusTCP* modbusTCP;
|
||||
|
||||
public:
|
||||
template <typename... Args>
|
||||
WithModbus(uint16_t baseAddress, Args&&... args)
|
||||
: TBase(std::forward<Args>(args)...),
|
||||
_baseAddress(baseAddress),
|
||||
_modbusBlocks(new MB_Registers[N]()),
|
||||
_modbusBlockView{_modbusBlocks, static_cast<int>(N)},
|
||||
_nextIndex(0),
|
||||
modbusTCP(nullptr)
|
||||
{
|
||||
// Add the Modbus capability flag.
|
||||
this->setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS);
|
||||
}
|
||||
|
||||
virtual ~WithModbus() {
|
||||
delete[] _modbusBlocks;
|
||||
}
|
||||
|
||||
// The Modbus-specific parts of setup() are now here.
|
||||
short setup() override {
|
||||
TBase::setup();
|
||||
this->m_enabled.initModbus(_baseAddress + E_NVC_ENABLED, 1, this->id, this->slaveId, FN_WRITE_COIL, "Enabled", this->name.c_str());
|
||||
this->m_enabled.initNotify(true, true, NetworkValue_ThresholdMode::DIFFERENCE);
|
||||
registerBlock(this->m_enabled.getRegisterInfo());
|
||||
return E_OK;
|
||||
}
|
||||
|
||||
// All Modbus-related methods are implemented here.
|
||||
MB_Registers* registerBlock(const MB_Registers& reg) {
|
||||
if (_nextIndex >= N) { /* ... error handling ... */ return nullptr; }
|
||||
_modbusBlocks[_nextIndex] = reg;
|
||||
return &_modbusBlocks[_nextIndex++];
|
||||
}
|
||||
|
||||
// --- Component virtual overrides ---
|
||||
ModbusBlockView* mb_tcp_blocks() const override {
|
||||
_modbusBlockView.count = _nextIndex;
|
||||
return const_cast<ModbusBlockView*>(&_modbusBlockView);
|
||||
}
|
||||
|
||||
void mb_tcp_register(ModbusTCP* manager) override {
|
||||
if (!this->hasNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS)) return;
|
||||
this->modbusTCP = manager;
|
||||
// ... (rest of the original implementation)
|
||||
}
|
||||
|
||||
uint16_t mb_tcp_base_address() const override { return _baseAddress; }
|
||||
|
||||
short mb_tcp_read(MB_Registers *reg) override {
|
||||
if (reg->startAddress == (_baseAddress + E_NVC_ENABLED)) {
|
||||
return this->enabled() ? 1 : 0;
|
||||
}
|
||||
// Chain up to allow base class to handle other reads if needed.
|
||||
return TBase::mb_tcp_read(reg);
|
||||
}
|
||||
|
||||
short mb_tcp_write(MB_Registers *reg, short value) override {
|
||||
if (reg->startAddress == (_baseAddress + E_NVC_ENABLED)) {
|
||||
this->enable(value != 0);
|
||||
this->m_enabled.update(value != 0);
|
||||
return E_OK;
|
||||
}
|
||||
return TBase::mb_tcp_write(reg, value);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper macros can be defined here as they are specific to the Modbus context
|
||||
#define SETUP_NETWORK_VALUE(nv_member, reg_offset_enum, fn_code, desc, ...) \
|
||||
do { \
|
||||
(nv_member).initNotify(__VA_ARGS__); \
|
||||
(nv_member).initModbus( \
|
||||
_baseAddress + static_cast<uint16_t>(reg_offset_enum), \
|
||||
1, \
|
||||
this->id, \
|
||||
this->slaveId, \
|
||||
fn_code, \
|
||||
desc, \
|
||||
this->name.c_str() \
|
||||
); \
|
||||
registerBlock((nv_member).getRegisterInfo()); \
|
||||
} while(0)
|
||||
|
||||
#endif // MIXIN_WITH_MODBUS_H
|
||||
```
|
||||
|
||||
## 3. Usage and Compatibility
|
||||
|
||||
To maintain source compatibility, a component that previously inherited from `NetworkComponent` will now inherit from the `WithModbus` mixin, which wraps the core `NetworkComponent`.
|
||||
|
||||
### Example: `Relay` Component
|
||||
|
||||
**`Relay.h` (Before)**
|
||||
```cpp
|
||||
#include "modbus/NetworkComponent.h"
|
||||
//...
|
||||
class Relay : public NetworkComponent<RELAY_MB_COUNT> { /*...*/ };
|
||||
```
|
||||
|
||||
**`Relay.h` (After)**
|
||||
```cpp
|
||||
#include "modbus/NetworkComponent.h" // The new lean version
|
||||
#include "mixins/WithModbus.h" // The new mixin
|
||||
//...
|
||||
|
||||
// Define a type that represents the classic NetworkComponent behavior.
|
||||
template<size_t N>
|
||||
using ModbusNetworkComponent = WithModbus<NetworkComponent<N>, N>;
|
||||
|
||||
// Relay's inheritance changes, but the rest of its interface and implementation
|
||||
// remains IDENTICAL. The constructor signature doesn't change.
|
||||
class Relay : public ModbusNetworkComponent<RELAY_MB_COUNT> {
|
||||
public:
|
||||
// ... same as before
|
||||
Relay(
|
||||
Component *owner,
|
||||
short _pin,
|
||||
short _id,
|
||||
short _modbusAddress); // Signature is compatible!
|
||||
// ...
|
||||
};
|
||||
```
|
||||
The constructor in `Relay.cpp` will call the `WithModbus` constructor, which has the exact same signature as the old `NetworkComponent` constructor, ensuring full compatibility.
|
||||
|
||||
## 4. Future Extensibility
|
||||
|
||||
This design provides a clear template for adding other network protocols. For instance, to add a "Serial" feature:
|
||||
|
||||
1. Create a `WithSerial` mixin class similar to `WithModbus`.
|
||||
2. Implement the serial communication logic and necessary `Component` virtual overrides within it.
|
||||
3. A component could then use `WithSerial<NetworkComponent<...>>` or even compose features: `WithModbus<WithSerial<NetworkComponent<...>>>`.
|
||||
|
||||
This modular approach ensures that `NetworkComponent` remains a lightweight and stable base, while features can be developed and composed independently.
|
||||
274
docs/nc-persistence.md
Normal file
@ -0,0 +1,274 @@
|
||||
# `Persistence` Mixin for `NetworkComponent`
|
||||
|
||||
This document proposes a `Persistence` mixin for `NetworkComponent` to enable automatic saving and loading of `NetworkValue` states to/from disk.
|
||||
|
||||
## 1. Overview
|
||||
|
||||
The `Persistence` mixin adds functionality to any `NetworkComponent` derivative to persist its `NetworkValue`s. This is useful for retaining state across reboots, such as configuration, calibration data, or last known operational parameters.
|
||||
|
||||
The functionality is modeled after the `Settings` component but is designed to be generic and reusable.
|
||||
|
||||
## 2. Design
|
||||
|
||||
The persistence logic will be encapsulated in a template class `Persistent`, which inherits from the `NetworkComponent` it extends. This allows for compile-time addition of the persistence feature without modifying the core `NetworkComponent` for components that do not require it.
|
||||
|
||||
To support serialization, we propose adding virtual methods to `NetworkValueBase` for JSON serialization.
|
||||
|
||||
### 2.1. Changes to `NetworkValue.h`
|
||||
|
||||
`NetworkValueBase` will be augmented with virtual methods for type identification and JSON handling.
|
||||
|
||||
```cpp
|
||||
// In NetworkValueBase
|
||||
enum class NetworkValueType {
|
||||
NV_NONE,
|
||||
NV_BOOL,
|
||||
NV_UINT32,
|
||||
NV_INT32,
|
||||
NV_FLOAT,
|
||||
NV_STRING
|
||||
// ... other types
|
||||
};
|
||||
|
||||
class NetworkValueBase {
|
||||
public:
|
||||
// ... existing members
|
||||
virtual void toJSON(JsonObject& obj) const = 0;
|
||||
virtual void fromJSON(JsonObjectConst& obj) = 0;
|
||||
virtual NetworkValueType getType() const = 0;
|
||||
virtual const char* getName() const = 0;
|
||||
};
|
||||
```
|
||||
|
||||
The templated `NetworkValue<T>` class will implement these virtual methods.
|
||||
|
||||
```cpp
|
||||
// In NetworkValue<T>
|
||||
|
||||
template<typename T>
|
||||
class NetworkValue : public NetworkValueBase {
|
||||
public:
|
||||
// ... existing members
|
||||
|
||||
void toJSON(JsonObject& obj) const override {
|
||||
obj["name"] = this->name.c_str();
|
||||
obj["value"] = m_value;
|
||||
obj["type"] = static_cast<int>(getType());
|
||||
}
|
||||
|
||||
void fromJSON(JsonObjectConst& obj) override {
|
||||
if (obj["value"].is<T>()) {
|
||||
update(obj["value"].as<T>());
|
||||
}
|
||||
}
|
||||
|
||||
const char* getName() const override {
|
||||
return name.c_str();
|
||||
}
|
||||
|
||||
NetworkValueType getType() const override {
|
||||
// Specialize this for each supported type
|
||||
if (std::is_same<T, bool>::value) return NetworkValueType::NV_BOOL;
|
||||
if (std::is_same<T, uint32_t>::value) return NetworkValueType::NV_UINT32;
|
||||
// ...
|
||||
return NetworkValueType::NV_NONE;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 2.2. The `Persistent` Mixin
|
||||
|
||||
The mixin will manage a filename and provide `load()` and `save()` methods.
|
||||
|
||||
```cpp
|
||||
#include "NetworkComponent.h"
|
||||
#include <LittleFS.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
template <class TBase>
|
||||
class Persistent : public TBase {
|
||||
protected:
|
||||
String _persistenceFile;
|
||||
|
||||
public:
|
||||
template <typename... Args>
|
||||
Persistent(const char* persistenceFile, Args&&... args)
|
||||
: TBase(std::forward<Args>(args)...),
|
||||
_persistenceFile(persistenceFile) {}
|
||||
|
||||
short setup() override {
|
||||
TBase::setup();
|
||||
load(); // Automatically load on setup
|
||||
return E_OK;
|
||||
}
|
||||
|
||||
bool save() {
|
||||
if (!LittleFS.begin()) {
|
||||
Log.errorln(F("Failed to mount LittleFS for saving %s"), this->name.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
JsonArray nvArray = doc.to<JsonArray>();
|
||||
|
||||
for (const auto* nv : this->_networkValues) {
|
||||
if (nv) {
|
||||
nv->toJSON(nvArray.add<JsonObject>());
|
||||
}
|
||||
}
|
||||
|
||||
File file = LittleFS.open(_persistenceFile, "w");
|
||||
if (!file) {
|
||||
Log.errorln(F("Failed to open file for writing: %s"), _persistenceFile.c_str());
|
||||
LittleFS.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t bytesWritten = serializeJson(doc, file);
|
||||
file.close();
|
||||
LittleFS.end();
|
||||
|
||||
return bytesWritten > 0;
|
||||
}
|
||||
|
||||
bool load() {
|
||||
if (!LittleFS.begin(true)) {
|
||||
Log.errorln(F("Failed to mount LittleFS for loading %s"), this->name.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!LittleFS.exists(_persistenceFile)) {
|
||||
Log.infoln(F("Persistence file not found: %s. Using defaults."), _persistenceFile.c_str());
|
||||
LittleFS.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
File file = LittleFS.open(_persistenceFile, "r");
|
||||
if (!file) {
|
||||
Log.errorln(F("Failed to open file for reading: %s"), _persistenceFile.c_str());
|
||||
LittleFS.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
|
||||
if (error) {
|
||||
Log.errorln(F("Failed to parse JSON from %s: %s"), _persistenceFile.c_str(), error.c_str());
|
||||
LittleFS.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonArrayConst nvArray = doc.as<JsonArrayConst>();
|
||||
for (JsonObjectConst nvJson : nvArray) {
|
||||
const char* name = nvJson["name"];
|
||||
if (!name) continue;
|
||||
|
||||
for (auto* nv : this->_networkValues) {
|
||||
if (nv && strcmp(nv->getName(), name) == 0) {
|
||||
nv->fromJSON(nvJson);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
LittleFS.end();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 3. Usage Example
|
||||
|
||||
Here's how a component `MyComponent` would use the `Persistent` mixin.
|
||||
|
||||
### 3.1. Component Definition
|
||||
|
||||
Instead of inheriting from `NetworkComponent` directly, it inherits from `Persistent<NetworkComponent<...>>`.
|
||||
|
||||
```cpp
|
||||
// MyComponent.h
|
||||
|
||||
#include "modbus/NetworkComponent.h"
|
||||
#include "mixins/Persistent.h"
|
||||
|
||||
const size_t MYCOMPONENT_BLOCK_COUNT = 10;
|
||||
|
||||
class MyComponent : public Persistent<NetworkComponent<MYCOMPONENT_BLOCK_COUNT>> {
|
||||
private:
|
||||
NetworkValue<float> _calibrationFactor;
|
||||
NetworkValue<int> _threshold;
|
||||
|
||||
public:
|
||||
MyComponent(Component* owner);
|
||||
short setup() override;
|
||||
};
|
||||
```
|
||||
|
||||
### 3.2. Component Implementation
|
||||
|
||||
The constructor passes the persistence filename and other `NetworkComponent` arguments up to the `Persistent` mixin constructor.
|
||||
|
||||
```cpp
|
||||
// MyComponent.cpp
|
||||
|
||||
#include "MyComponent.h"
|
||||
|
||||
MyComponent::MyComponent(Component* owner)
|
||||
: Persistent(
|
||||
"/mycomponent.json",
|
||||
MB_ADDR_MYCOMPONENT_BASE,
|
||||
"MyComponent",
|
||||
"my_comp_key",
|
||||
owner),
|
||||
_calibrationFactor(this, this->id, "CalibrationFactor", 1.0f),
|
||||
_threshold(this, this->id, "Threshold", 100)
|
||||
{
|
||||
addNetworkValue(&_calibrationFactor);
|
||||
addNetworkValue(&_threshold);
|
||||
}
|
||||
|
||||
short MyComponent::setup() {
|
||||
Persistent::setup();
|
||||
|
||||
_calibrationFactor.initModbus(...);
|
||||
registerBlock(_calibrationFactor.getRegisterInfo());
|
||||
_threshold.initModbus(...);
|
||||
registerBlock(_threshold.getRegisterInfo());
|
||||
|
||||
return E_OK;
|
||||
}
|
||||
|
||||
void MyComponent::updateAndSaveChanges() {
|
||||
_calibrationFactor.update(1.23f);
|
||||
if (!save()) {
|
||||
Log.errorln(F("Failed to save MyComponent settings!"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. JSON File Structure
|
||||
|
||||
The persisted file (e.g., `/mycomponent.json`) would look like this:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "Enabled",
|
||||
"value": true,
|
||||
"type": 1
|
||||
},
|
||||
{
|
||||
"name": "CalibrationFactor",
|
||||
"value": 1.23,
|
||||
"type": 4
|
||||
},
|
||||
{
|
||||
"name": "Threshold",
|
||||
"value": 100,
|
||||
"type": 3
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
This structure is a flat array of all `NetworkValue` objects managed by the component. Each object contains its name, value, and a type identifier.
|
||||
132
docs/network-values.md
Normal file
@ -0,0 +1,132 @@
|
||||
# NetworkValue and NetworkComponent Enhancements
|
||||
|
||||
## 1. Overview
|
||||
|
||||
This document outlines a plan to enhance the `NetworkComponent` and `NetworkValue` classes to provide more granular, runtime control over individual `NetworkValue` instances and their specific features (Modbus, Protobuf, Logging).
|
||||
|
||||
## 2. Motivation
|
||||
|
||||
Currently, the `NetworkComponent` provides a single `m_enabled` coil that toggles the entire component's `loopNetwork()` method on or off. While useful, this is an all-or-nothing approach.
|
||||
|
||||
There is a need for more fine-grained control, allowing a user to selectively enable or disable not just the `NetworkValue` itself, but also its specific features. This would be useful for:
|
||||
|
||||
- **Debugging**: Selectively enabling logging for a single problematic `NetworkValue`.
|
||||
- **Performance**: Disabling high-frequency Protobuf or Modbus updates for values that are not currently being monitored by any client, without disabling the value's core logic.
|
||||
- **Bandwidth Management**: Remotely toggling which values are broadcast over the network to reduce traffic.
|
||||
|
||||
## 3. Proposed Feature: Feature Control Masks
|
||||
|
||||
I propose adding a series of new, default Modbus registers to the `NetworkComponent`.
|
||||
|
||||
- **Location**: These registers will start at `mb_tcp_base_address() + 1`.
|
||||
- **Type**: They will be `uint16_t` holding registers.
|
||||
- **Functionality**: Each register will act as a bitmask for a specific feature. Each bit in a mask will correspond to a `NetworkValue` instance within the `_networkValues` vector.
|
||||
|
||||
### a. New Registers
|
||||
|
||||
| Offset | Name | Description | Default |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| +0 | `ENABLED` | (Coil) Master switch for the component's `loopNetwork()`. | `1` (On) |
|
||||
| +1 | `ENABLED_MASK` | (Holding) Bitmask to enable/disable entire `NetworkValue` instances. | `0xFFFF` |
|
||||
| +2 | `MODBUS_MASK` | (Holding) Bitmask to enable/disable the Modbus feature for each `NetworkValue`. | `0xFFFF` |
|
||||
| +3 | `PROTOBUF_MASK`| (Holding) Bitmask to enable/disable the Protobuf feature for each `NetworkValue`. | `0xFFFF` |
|
||||
| +4 | `LOGGING_MASK` | (Holding) Bitmask to enable/disable the Logging feature for each `NetworkValue`. | `0xFFFF` |
|
||||
|
||||
### b. `NetworkComponent` Implementation
|
||||
|
||||
1. **New `NetworkValue` Members**: New `NetworkValue<uint16_t>` members will be added for `m_enabled_mask`, `m_modbus_mask`, `m_protobuf_mask`, and `m_logging_mask`.
|
||||
2. **Initialization**: In `setup()`, these new `NetworkValue`s will be initialized and registered as individual holding registers at their respective offsets. Their default value will be `0xFFFF`.
|
||||
3. **Update Logic**: An `onUpdate` callback will be implemented for each mask. When a mask is written to, its callback will iterate through the `_networkValues` vector and apply the corresponding feature toggle.
|
||||
|
||||
### c. `NetworkValue` Implementation
|
||||
|
||||
The `NetworkValue::update()` method will be modified to respect the component's top-level enabled flag before proceeding with any change detection or notifications.
|
||||
|
||||
## 4. Implementation Steps
|
||||
|
||||
1. **Create Documentation**: Write this plan. (Done)
|
||||
2. **Modify `NetworkComponent.h`**:
|
||||
- Add the new `NetworkValue<uint16_t>` members for each mask.
|
||||
- Update `setup()` to register the new Modbus holding registers individually.
|
||||
- Implement the `onUpdate` callbacks for each mask.
|
||||
3. **Modify `NetworkValue.h`**:
|
||||
- Add the `if (!this->enabled()) return false;` guard at the beginning of the `update()` method.
|
||||
4. **Verify & Test**: Run the build and update the `NetworkValueTestPB` component to verify the new functionality.
|
||||
|
||||
## 5. Edge Cases and Limitations
|
||||
|
||||
- **Maximum NetworkValues**: The use of a `uint16_t` for the bitmasks limits a single `NetworkComponent` to a maximum of 16 `NetworkValue` instances.
|
||||
- **Vector Order**: The masks rely on the fixed order of `NetworkValue` instances in the `_networkValues` vector. This order must not change after initialization.
|
||||
- **Component vs. NetworkValue State**: Bit 0 of the `ENABLED_MASK` controls the entire component's `loopNetwork()` method. Other bits in `ENABLED_MASK` control individual `NetworkValue` instances. The feature-specific masks only control their respective features and do not stop the `NetworkValue` from updating its internal state.
|
||||
## 6. Performance Considerations (ESP32)
|
||||
|
||||
Based on the provided device metrics (Free Heap: ~46 KB, Max Free Block: ~34 KB), memory is a significant constraint. Let's analyze the impact of this new feature.
|
||||
|
||||
### a. Memory (RAM) Impact
|
||||
|
||||
- **Per `NetworkComponent`**:
|
||||
- Each new feature mask adds one `NetworkValue<uint16_t>` instance. A `NetworkValue` object has a baseline size of around 40-50 bytes.
|
||||
- Adding 4 new masks per `NetworkComponent` will consume approximately **160-200 bytes** of RAM.
|
||||
- **System-Wide**:
|
||||
- With **30 components**, the total RAM increase would be `30 * 200 bytes = 6000 bytes`, or approximately **5.9 KB**.
|
||||
- This represents a **~13%** increase relative to the 46 KB of free heap.
|
||||
|
||||
#### Detailed Memory Breakdown
|
||||
|
||||
The total memory footprint is a sum of the `NetworkValue` objects themselves and the storage for their `MB_Registers` structs within the parent `NetworkComponent`.
|
||||
|
||||
| Item | Size per Item (Bytes) | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| `MB_Registers` struct | ~32 | Includes `ushort` fields, pointers, and compiler padding. |
|
||||
| `NetworkValue` overhead | ~24 | Includes v-table pointer, owner, flags, callbacks, etc. |
|
||||
| **Total per `NetworkValue`** | **~56** | |
|
||||
|
||||
With this baseline, we can calculate a more precise per-component and system-wide impact. This example assumes a component with 10 user-defined `NetworkValue`s, in addition to the new feature masks.
|
||||
|
||||
| Item | Count (per Component) | Total Size (Bytes) |
|
||||
| :--- | :--- | :--- |
|
||||
| **`NetworkValue` Objects** | | |
|
||||
| `m_enabled_mask` | 1 | 56 |
|
||||
| `m_modbus_mask` | 1 | 56 |
|
||||
| `m_protobuf_mask` | 1 | 56 |
|
||||
| `m_logging_mask` | 1 | 56 |
|
||||
| **`_modbusBlocks` Array** | | |
|
||||
| For user `NetworkValue`s | 10 | 320 |
|
||||
| For mask `NetworkValue`s | 4 | 128 |
|
||||
| **Total Per Component** | | **~672** |
|
||||
| | | |
|
||||
| **Total System-Wide** | **30 Components** | **~20,160 (~19.7 KB)** |
|
||||
|
||||
- A **~20 KB** increase represents a **~43%** reduction in the available 46 KB of free heap. This is a very significant impact and must be carefully considered.
|
||||
|
||||
### b. CPU Impact
|
||||
|
||||
- **Idle State**: When no masks are being changed, the CPU impact is negligible. It amounts to a few extra `if` checks in the `mb_tcp_write` handler per incoming write request.
|
||||
- **On Mask Update**: When a mask register is written to, the `onUpdate` callback will execute. This involves a loop of up to 16 iterations (the number of `NetworkValue` instances). Inside the loop, it performs a bitwise check and calls `enable()` or `enableFeature()`. This is a very fast operation, likely taking only a few microseconds to complete.
|
||||
|
||||
### c. Conclusion
|
||||
|
||||
The primary impact of this feature is on RAM. A ~16 KB increase is a major architectural cost. While the feature offers significant flexibility, the memory overhead may be too high for the target device. The "Consolidated Mask Register" optimization should be considered the primary implementation path to mitigate this.
|
||||
|
||||
## 7. Optimizations and Future Work
|
||||
|
||||
- **Consolidated Mask Register**: **(HIGHLY RECOMMENDED)** To significantly reduce the memory footprint, the four separate `uint16_t` masks should be consolidated into a single `std::array<uint16_t, 4>`. This would require only one `NetworkValue` instance and one Modbus block (`FN_WRITE_MULT_REGISTERS`) to update all masks at once, reducing the per-component RAM overhead from over 500 bytes to around 120 bytes.
|
||||
- **Dynamic Instantiation**: For very large or dynamic systems, the static `NetworkComponent<N>` template could be replaced with a version that uses a dynamically allocated `Vector` for `_modbusBlocks`. This would increase memory management complexity but allow for a variable number of `NetworkValue` instances.
|
||||
- **Read-Only Masks**: To provide a read-only view of the current feature states, additional read-only registers could be exposed. These would be updated internally whenever a mask is changed, providing a clear "live" view of the configuration for clients without allowing them to be written to directly.
|
||||
### Radical Optimizations (Advanced)
|
||||
|
||||
For systems requiring extreme memory optimization, the `NetworkComponent` and `NetworkValue` architecture could be fundamentally redesigned.
|
||||
|
||||
- **Centralized `NetworkValueRegistry`**: Instead of each `NetworkComponent` managing its own list of `NetworkValue` instances and Modbus blocks, a single, static `NetworkValueRegistry` could be implemented.
|
||||
- **Memory Savings**: This would eliminate the `std::vector _networkValues` and `MB_Registers* _modbusBlocks` from every `NetworkComponent` instance, saving hundreds of bytes per component.
|
||||
- **Implementation**: Components would register their `NetworkValue`s with this central registry during their `setup()` phase. The `ModbusTCP` manager would then interact directly with the registry instead of individual components.
|
||||
|
||||
- **Flyweight Pattern for Modbus Data**: The `MB_Registers` struct could be replaced with a more memory-efficient flyweight object.
|
||||
- **How it Works**: The central registry would store a single, canonical copy of the static Modbus metadata (name, group, function code, etc.) for each registered address. The `NetworkValue` would only need to store a pointer or an ID to this shared data, rather than a full copy.
|
||||
- **Memory Savings**: This would further reduce the per-`NetworkValue` overhead from ~56 bytes to closer to ~24-32 bytes.
|
||||
|
||||
- **Callback-Based Read/Write**: The virtual `mb_tcp_read` and `mb_tcp_write` methods could be replaced with a callback system.
|
||||
- **How it Works**: When registering a `NetworkValue`, the component would provide lambdas or function pointers for getting and setting its value. The central registry would store these pointers. When a Modbus request arrives, the registry would directly invoke the appropriate callback.
|
||||
- **Performance**: This would eliminate the overhead and indirection of virtual function calls through the component hierarchy, resulting in faster request handling.
|
||||
|
||||
- **Combined Approach**: A combination of all three radical optimizations would yield the most significant memory and performance gains, but would require a substantial refactoring of the current component architecture.
|
||||
44
docs/network_component.md
Normal file
@ -0,0 +1,44 @@
|
||||
# NetworkComponent Architecture
|
||||
|
||||
## 1. Overview
|
||||
|
||||
The `NetworkComponent` class serves as a generic, network-aware base class for components that need to expose values over Modbus and other network protocols. It is designed to reduce boilerplate by providing default implementations for common network-related tasks, including Modbus registration, message handling, and a standardized enable/disable mechanism.
|
||||
|
||||
## 2. Core Features
|
||||
|
||||
### a. Default "Enabled" Coil
|
||||
|
||||
By default, every `NetworkComponent` automatically registers a `NetworkValue<bool>` named `m_enabled`.
|
||||
|
||||
- **Modbus Address**: This coil is always located at `mb_tcp_base_address() + 0`.
|
||||
- **Functionality**: Writing to this coil via Modbus will enable or disable the component's main loop logic by setting the component's internal `E_OF_DISABLED` flag. This provides a standardized way to remotely control component activity.
|
||||
|
||||
### b. `loopNetwork()` Method
|
||||
|
||||
Derived components should place their primary operational logic inside the `loopNetwork()` method, not the standard `loop()` method.
|
||||
|
||||
The base `NetworkComponent::loop()` method will first check the state of the `m_enabled` coil. If the component is disabled, the `loop()` method will exit early, preventing `loopNetwork()` from being called. This ensures that the enabled state is always respected.
|
||||
|
||||
### c. Centralized Network Methods
|
||||
|
||||
`NetworkComponent` provides default implementations for several key virtual methods from the `Component` base class:
|
||||
|
||||
- `mb_tcp_register()`: Automatically iterates through the component's registered Modbus blocks and registers them with the `ModbusTCP` manager.
|
||||
- `mb_tcp_blocks()`: Returns the view of the component's Modbus blocks.
|
||||
- `mb_tcp_read()` / `mb_tcp_write()`: Automatically handle read/write requests for the default `m_enabled` coil.
|
||||
- `onMessage()`: Provides a default handler that forwards Protobuf messages (`EC_PROTOBUF_UPDATE`) to the component's owner, which is typically the `RestServer` for broadcasting.
|
||||
|
||||
Derived classes should not need to override these methods unless they need to add new behavior.
|
||||
|
||||
## 3. Required Implementation by Subclasses
|
||||
|
||||
To use `NetworkComponent`, a derived class **must** implement the following:
|
||||
|
||||
- A constructor that calls the `NetworkComponent` base constructor.
|
||||
- `uint16_t mb_tcp_base_address() const override`: Must return the unique starting Modbus address for this component instance.
|
||||
- `setup()`: Must call `NetworkComponent::setup()` and then register its own `NetworkValue` instances and Modbus blocks.
|
||||
- `loopNetwork()`: Contains the component's primary operational logic.
|
||||
- `mb_tcp_read(MB_Registers* reg) override`: Must first call `on_mb_tcp_read(reg, &value)`. If this returns `true`, the value has been handled by the base class. Otherwise, the method should handle its own registers.
|
||||
- `mb_tcp_write(MB_Registers* reg, short value) override`: Must first call `on_mb_tcp_write(reg, value)`. If this returns `true`, the request has been handled. Otherwise, the method should handle its own registers.
|
||||
|
||||
This architecture ensures that components are lean and focused on their specific logic, while the `NetworkComponent` base class handles the common, error-prone networking boilerplate.
|
||||
5
docs/notes.md
Normal file
@ -0,0 +1,5 @@
|
||||
## ESP32 - LittleFS Size
|
||||
|
||||
https://community.platformio.org/t/how-to-define-littlefs-partition-build-image-and-flash-it-on-esp32/11333
|
||||
|
||||
https://github.com/earlephilhower/mklittlefs/releases
|
||||
77
docs/omron-cooling.md
Normal file
@ -0,0 +1,77 @@
|
||||
# Task List: Omron E5 Cooling Integration
|
||||
|
||||
This document outlines the steps required to add cooling monitoring and control capabilities to the `OmronE5` C++ class (`src/components/OmronE5.h` and `src/components/OmronE5.cpp`), referencing registers defined in `docs/omron/omron.h`.
|
||||
|
||||
## 1. Verify/Implement Cooling Status Check (`isCooling`)
|
||||
|
||||
* **Goal:** Provide a boolean method to check if the cooling output is currently active.
|
||||
* **Changes:**
|
||||
* **`OmronE5.h`:** Ensure the `bool isCooling() const;` method declaration exists (it likely does).
|
||||
* **`OmronE5.cpp`:**
|
||||
* Verify the implementation of `isCooling()`.
|
||||
* It should check the appropriate bit within the status registers (`_currentStatusLow`, `_currentStatusHigh`) that indicates cooling output status.
|
||||
* **Reference Register:** The specific bit needs confirmation from the **H175 Manual Section 5-2 (Status Monitor Table)**. The placeholder `OR_E5_S1_Control_OutputCloseOutput` is used in the current code; verify its correctness for indicating cooling output state. The status data comes from reading `E_STATUS_1_REGISTER` (0x2001) and potentially higher words like `E_STATUS_3_REGISTER` (0x2407), which are read as part of the base status block.
|
||||
* **Registers Involved:** Primarily Status Registers (e.g., `0x2001`, `0x2406`, `0x2407`, `0x2408`, `0x2409`) read during the main data poll.
|
||||
|
||||
## 2. Add Cooling Manipulated Value (MV) Monitoring (Optional)
|
||||
|
||||
* **Goal:** Allow the application to read the current cooling output level (percentage).
|
||||
* **Considerations:** This requires reading a register likely *not* in the default read block (0x0000-0x0005). Decide if raw data or scaled float is needed. Reading extra registers increases bus traffic.
|
||||
* **Changes (If implemented):**
|
||||
* **`OmronE5.h`:**
|
||||
* Add a getter method declaration, e.g.:
|
||||
```c++
|
||||
// Option A: Scaled Float
|
||||
// bool getCoolingMV(float& value) const;
|
||||
|
||||
// Option B: Raw 32-bit Value (reading two 16-bit registers)
|
||||
bool getCoolingMVRaw(uint32_t& value) const;
|
||||
```
|
||||
* If using Option B, add private members:
|
||||
```c++
|
||||
// uint16_t _currentCoolingMVLow = 0;
|
||||
// uint16_t _currentCoolingMVHigh = 0;
|
||||
// bool _coolingMvValid = false;
|
||||
```
|
||||
* **`OmronE5.cpp`:**
|
||||
* **`setup()`:** Modify `addMandatoryReadBlock` or add a new one to include the Cooling MV registers. **Note:** `E_MV_MONITOR_COOL_REGISTER` (0x2005) is a 4-byte value, requiring reading two consecutive 16-bit addresses (0x2005 and 0x2006). Adjust the read block accordingly.
|
||||
* **`onRegisterUpdate()`:** Add `else if` conditions to update the internal state (`_currentCoolingMVLow`, `_currentCoolingMVHigh`, `_coolingMvValid`) when data for the new registers (e.g., 0x2005, 0x2006) arrives.
|
||||
* **Implement Getter:** Write the logic for `getCoolingMV` or `getCoolingMVRaw`, combining the low/high words if necessary and potentially scaling the value.
|
||||
* **Registers Involved:**
|
||||
* `E_MV_MONITOR_COOL_REGISTER` (0x2005) - Requires reading **0x2005** (Low Word) and **0x2006** (High Word).
|
||||
* Alternatively: `E_OPERATION_MV_MONITOR_COOL_REGISTER` (0x2606) - Requires reading **0x2606** (Low Word) and **0x2607** (High Word).
|
||||
|
||||
## 3. Add Cooling Configuration Setters (Optional)
|
||||
|
||||
* **Goal:** Allow runtime configuration of cooling parameters (if not pre-configured on the device).
|
||||
* **Considerations:** Increases complexity. Requires adding Modbus write capabilities for these specific registers.
|
||||
* **Changes (If implemented):**
|
||||
* **`OmronE5.h`:** Add public method declarations for desired setters (see examples below).
|
||||
* **`OmronE5.cpp`:**
|
||||
* **`setup()`:** For each setter added, call `addOutputRegister()` targeting the specific register address and function code (likely `FN_WRITE_HOLD_REGISTER`). Note that 4-byte registers often require writing to the low-word address.
|
||||
* **Implement Setter Methods:** Implement the logic for each setter, likely calling `this->setOutputRegisterValue()` with the correct address and value (potentially requiring scaling).
|
||||
* **Example Setters & Registers Involved:**
|
||||
* `setControlType(E_CONTROL_HEAT_AND_COOL)` -> Writes to `E_CONTROL_TYPE_REGISTER` (**0x2D11**)
|
||||
* `setOperationMode(E_OPERATION_DIRECT)` -> Writes to `E_DIRECT_REVERSE_OP_REGISTER` (**0x2D12**)
|
||||
* `assignOutputToCooling(outputNumber)` -> Writes `E_OUTPUT_ASSIGN_CONTROL_COOL` (2) to `E_CONTROL_OUTPUT_1_ASSIGN_REGISTER` (**0x2E06**) or `E_CONTROL_OUTPUT_2_ASSIGN_REGISTER` (**0x2E07**)
|
||||
* `setCoolingPID(p, i, d)` -> Writes scaled values to `E_PROP_BAND_COOL_REGISTER` (**0x2701**), `E_INTEGRAL_TIME_COOL_REGISTER` (**0x2702**), `E_DERIVATIVE_TIME_COOL_REGISTER` (**0x2703**)
|
||||
* `setDeadBand(value)` -> Writes scaled value to `E_DEAD_BAND_REGISTER` (**0x2704**)
|
||||
|
||||
## 4. Modbus TCP Exposure (Optional)
|
||||
|
||||
* **Goal:** Expose new cooling status/values/settings via the Modbus TCP interface.
|
||||
* **Changes (If implemented):**
|
||||
* **`OmronE5.cpp`:**
|
||||
* Add new enum members to `E_OmronTcpOffset` for each value to be exposed (e.g., `COOLING_MV`, `COOLING_P_PARAM`).
|
||||
* Increment `OMRON_TCP_BLOCK_COUNT` accordingly (in `.h` and `.cpp` constructor).
|
||||
* Add `INIT_MODBUS_BLOCK` calls in the constructor for the new offsets.
|
||||
* Add `case` statements in `mb_tcp_read()` to handle reads for the new cooling values/parameters (retrieving data from internal state or getters).
|
||||
* Add `case` statements in `mb_tcp_write()` to handle writes for the new cooling settings (calling the corresponding setter methods).
|
||||
|
||||
## Review and Testing
|
||||
|
||||
* Thoroughly test all added functionality, paying close attention to:
|
||||
* Correct register addresses and function codes.
|
||||
* Correct handling of 4-byte values (reading/writing low/high words).
|
||||
* Data scaling and unit conversions.
|
||||
* Impact on Modbus bus timing and traffic.
|
||||
BIN
docs/omron/E5DC-B_H175-E1-08.pdf
Normal file
21
docs/omron/convert.md
Normal file
@ -0,0 +1,21 @@
|
||||
Create or merge enums for found Modbus registers, for C++. Always prefix enum values with 'E_', all in capital, comment if possible (ranges, defaults)
|
||||
|
||||
example:
|
||||
|
||||
enum OR_E5_STATUS_1
|
||||
{
|
||||
// Lower Word
|
||||
|
||||
OR_E5_S1_Heater_OverCurrent = 0,
|
||||
OR_E5_S1_Heater_CurrentHold = 1,
|
||||
OR_E5_S1_AD_ConverterError = 2,
|
||||
}
|
||||
|
||||
enum OR_E5_SWR
|
||||
{
|
||||
//Temperature: Use the specified range for each sensor.
|
||||
// Analog: Scaling lower limit ‚àí 5% FS to Scaling upper limit + 5% FS
|
||||
OR_E5_SWR_PV = 0x2000,
|
||||
}
|
||||
|
||||
A file been provided with the existing enums.
|
||||
7
docs/omron/convert.sh
Normal file
@ -0,0 +1,7 @@
|
||||
kbot-d --router2=openai --model=google/gemini-2.5-pro-preview-03-25 \
|
||||
--prompt=./tests/pdf/omron/convert.md \
|
||||
--each=./tests/pdf/omron/*.jpg \
|
||||
--include=./tests/pdf/omron/omron.h \
|
||||
--mode=completion --preferences=none \
|
||||
--dst=./tests/pdf/omron/omron.h \
|
||||
--filters=code
|
||||
BIN
docs/omron/e5dc_8_100.jpg
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
docs/omron/e5dc_8_101.jpg
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
docs/omron/e5dc_8_102.jpg
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
docs/omron/e5dc_8_103.jpg
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
docs/omron/e5dc_8_104.jpg
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
docs/omron/e5dc_8_105.jpg
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
docs/omron/e5dc_8_106.jpg
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
docs/omron/e5dc_8_87.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |