diff --git a/adapters/CHANGELOG.md b/adapters/CHANGELOG.md new file mode 100644 index 00000000..6f921005 --- /dev/null +++ b/adapters/CHANGELOG.md @@ -0,0 +1,3 @@ +## v0.0.1 - (25/04/2026) + +Initial version of the adapter for the Modbus protocol. diff --git a/adapters/describe.json b/adapters/describe.json index 364e2538..7204f6cd 100644 --- a/adapters/describe.json +++ b/adapters/describe.json @@ -258,5 +258,5 @@ }, "type": "object" }, - "version": "0.0.1-master+a972c33" + "version": "0.0.1" } diff --git a/adapters/dummymodbusadapter b/adapters/dummymodbusadapter index be34ce7a..83b47faa 100755 Binary files a/adapters/dummymodbusadapter and b/adapters/dummymodbusadapter differ diff --git a/adapters/dummymodbusadapter.exe b/adapters/dummymodbusadapter.exe index 8fea09bb..3487d1de 100644 Binary files a/adapters/dummymodbusadapter.exe and b/adapters/dummymodbusadapter.exe differ diff --git a/adapters/json-rpc-spec.md b/adapters/json-rpc-spec.md deleted file mode 100644 index 2af9f85f..00000000 --- a/adapters/json-rpc-spec.md +++ /dev/null @@ -1,692 +0,0 @@ -# ModbusAdapter JSON-RPC 2.0 Specification - -## Overview - -The ModbusAdapter process communicates with a host application (client) via JSON-RPC 2.0 over its standard input and output streams. - -The adapter is launched as a child process by the client. The client writes JSON-RPC requests to the adapter's **stdin** and reads responses and notifications from its **stdout**. - ---- - -## Transport - -### Message Framing - -All messages are framed using a Content-Length header: - -```text -Content-Length: \r\n -\r\n - -``` - -- The header and body are separated by `\r\n\r\n`. -- `` is the exact byte length of the JSON body. -- No other headers are defined. - -### Message Types - -**Request** (client → adapter): -```json -{ - "jsonrpc": "2.0", - "id": 1, - "method": "adapter.initialize", - "params": {} -} -``` - -**Response** (adapter → client): -```json -{ - "jsonrpc": "2.0", - "id": 1, - "result": {} -} -``` - -**Error response** (adapter → client): -```json -{ - "jsonrpc": "2.0", - "id": 1, - "error": { - "code": -32602, - "message": "Invalid params: ..." - } -} -``` - -**Notification** (adapter → client, no `id`): -```json -{ - "jsonrpc": "2.0", - "method": "adapter.diagnostic", - "params": {} -} -``` - ---- - -## Methods - -### `adapter.initialize` - -Lifecycle signal sent by the client to indicate a session is starting. No configuration is applied. - -**Params:** `{}` (none required) - -**Result:** -```json -{ "status": "ok" } -``` - ---- - -### `adapter.describe` - -Returns the adapter's static metadata, configuration schema, default values, and capabilities. Call this after `adapter.initialize` to discover what `adapter.configure` expects. - -**Params:** `{}` (none required) - -**Result (Release build):** -```json -{ - "name": "modbusAdapter", - "version": "0.0.1", - "configVersion": 1, - "schema": { ... }, - "defaults": { ... }, - "capabilities": { - "supportsHotReload": false, - "requiresRestartOn": ["connections", "devices"] - } -} -``` - -**Result (Debug build):** the `version` field appends `-+`, e.g. `"0.0.1-dev-diagnostics+c471210"`. Branch names with slashes are converted to hyphens (e.g. `dev/diagnostics` → `dev-diagnostics`). Consumers must not treat `version` as a fixed literal; parse or compare it accordingly. - -> **Note:** Update this spec whenever the JSON-RPC implementation changes. - -| Field | Description | -| --- | --- | -| `name` | Adapter identifier | -| `version` | Adapter software version. Release: `""`. Debug: `"-+"` (slashes in branch name replaced with hyphens). | -| `configVersion` | Current config schema version | -| `schema` | JSON Schema–compatible object describing the `config` object accepted by `adapter.configure` | -| `defaults` | Default config values. The `connections` array contains a single TCP entry that also includes serial-specific fields (`portName`, `baudrate`, `parity`, `databits`, `stopbits`) so callers can see serial defaults without needing a second example. | -| `capabilities` | Feature flags | - -Each property in `schema` includes the following additional fields (standard JSON Schema annotations and custom extensions): - -| Field | Description | -| --- | --- | -| `title` | Standard JSON Schema annotation. UI-friendly label for the field, suitable for use in form inputs and dialog labels | -| `x-enumLabels` | Custom extension. Present on enum properties only. A string array, parallel to `enum`, giving a UI-friendly display name for each allowed value | - -The connection schema uses JSON Schema Draft 7 `if`/`then`/`else` to express type-dependent fields. When `type` equals `"tcp"`, the fields in `then.properties` apply (TCP-specific). Otherwise, when `type` is not `"tcp"`, the fields in `else.properties` apply (e.g., serial-specific or other non-TCP types). A UI can use this to enable or disable the relevant fields based on the selected connection type. - ---- - -### `adapter.configure` - -Applies connection and device configuration to the adapter. Must be called before `adapter.start`. - -The config object top-level structure will be used for the configuration GUI dialog generation. The GUI uses JSON Schema types to determine layout: `"type": "object"` renders as a single-form dialog, `"type": "array"` renders as a tabbed dialog (one tab per item). - -**Params:** -```json -{ - "config": { - "version": 1, - "general": {}, - "connections": [ - { - "id": 0, - "type": "tcp", - "ip": "192.168.1.100", - "port": 502, - "timeout": 1000, - "persistent": false - }, - { - "id": 1, - "type": "serial", - "portName": "/dev/ttyUSB0", - "baudrate": 115200, - "parity": "N", - "databits": 8, - "stopbits": 1, - "timeout": 1000, - "persistent": false - } - ], - "devices": [ - { - "id": 1, - "connectionId": 0, - "slaveId": 1, - "consecutiveMax": 64, - "int32LittleEndian": false - } - ] - } -} -``` - -**General fields:** - -Adapter-wide settings. Currently empty; reserved for future use. - -| Field | Type | Description | -| --- | --- | --- | -| *(none yet)* | | | - -**Connection fields:** - -| Field | Type | Required | Description | -| --- | --- | --- | --- | -| `id` | integer | yes | Connection index: `0`, `1`, or `2` | -| `name` | string | no | Human-readable label (default: `"Connection "`) | -| `type` | string | yes | `"tcp"` or `"serial"` | -| `timeout` | integer | no | Timeout in milliseconds (default: `1000`) | -| `persistent` | boolean | no | Keep connection open between reads (default: `true`) | - -TCP-specific fields: - -| Field | Type | Default | Description | -| --- | --- | --- | --- | -| `ip` | string | `"127.0.0.1"` | IP address | -| `port` | integer | `502` | TCP port | - -Serial-specific fields: - -| Field | Type | Default | Description | -| --- | --- | --- | --- | -| `portName` | string | | Serial port name (e.g. `/dev/ttyUSB0`, `COM1`) | -| `baudrate` | integer | `9600` | Baud rate | -| `parity` | string | `"N"` | `"N"` (none), `"E"` (even), `"O"` (odd) | -| `databits` | integer | `8` | Data bits: `5`, `6`, `7`, or `8` | -| `stopbits` | integer | `1` | Stop bits: `1` (one), `2` (two), or `3` (one-and-a-half) | - -**Device fields:** - -| Field | Type | Required | Description | -| --- | --- | --- | --- | -| `id` | integer | yes | Device identifier (unique within the adapter) | -| `connectionId` | integer | yes | Which connection this device is on (`0`–`2`) | -| `slaveId` | integer | yes | Modbus slave ID (`1`–`247`) | -| `consecutiveMax` | integer | no | Max registers per single read request (default: `125`) | -| `int32LittleEndian` | boolean | no | Byte order for 32-bit values (default: `true` = little endian) | - -**Result:** -```json -{ "status": "ok" } -``` - -**Errors:** -- `-32602` — Missing `config` key; missing or invalid `general`/`connections`/`devices`; invalid field values - ---- - -### `adapter.start` - -Starts Modbus polling with the configuration applied by `adapter.configure`. Must be called after `adapter.configure`. - -**Params:** - -```json -{ - "registers": ["${40001: 16b}", "${h0 @ 2: f32b}"] -} -``` - -Each element is a register subexpression string with the syntax: -`${address [@ deviceId] [: type]}` - -**Address format:** - -| Prefix | Object type | Example | -| --- | --- | --- | -| `00001`–`09999` or `c` | Coils | `c50`, `00001` | -| `10001`–`19999` or `d` | Discrete inputs | `d100`, `10001` | -| `30001`–`39999` or `i` | Input registers | `i0`, `30001` | -| `40001`–`49999` or `h` | Holding registers | `h0`, `40001` | - -**Optional fields:** - -| Field | Syntax | Default | Description | -| --- | --- | --- | --- | -| `deviceId` | `@ N` | `1` | Device ID from `adapter.configure` | -| `type` | `: type` | `16b` | Data type | - -**Type values:** - -| Value | Description | -| --- | --- | -| `"16b"` | Unsigned 16-bit integer (default) | -| `"s16b"` | Signed 16-bit integer | -| `"32b"` | Unsigned 32-bit integer (two consecutive registers) | -| `"s32b"` | Signed 32-bit integer | -| `"f32b"` | 32-bit IEEE 754 float | - -An empty `registers` array is valid and starts polling with no registers configured. - -**Result:** -```json -{ "status": "ok" } -``` - -**Errors:** -- `-32602` — invalid subexpression syntax; unknown type - ---- - -### `adapter.dataPointSchema` - -Returns the schema for data point expressions — what fields make up a data point address, how they should be rendered in the UI, and available data types. Call this after `adapter.describe` to discover how to build the data point input UI. - -**Params:** `{}` (none required) - -**Result:** -```json -{ - "addressSchema": { - "type": "object", - "properties": { - "objectType": { - "type": "string", - "title": "Object type", - "enum": ["coil", "discrete input", "input register", "holding register"], - "x-enumLabels": ["Coil", "Discrete Input", "Input Register", "Holding Register"] - }, - "address": { - "type": "integer", - "title": "Address", - "minimum": 0, - "maximum": 65535 - }, - "deviceId": { - "type": "integer", - "title": "Device ID", - "minimum": 1 - }, - "dataType": { - "type": "string", - "title": "Data type" - } - }, - "required": ["objectType", "address"] - }, - "dataTypes": [ - { "id": "16b", "label": "unsigned 16-bit" }, - { "id": "s16b", "label": "signed 16-bit" }, - { "id": "32b", "label": "unsigned 32-bit" }, - { "id": "s32b", "label": "signed 32-bit" }, - { "id": "f32b", "label": "32-bit float" } - ], - "defaultDataType": "16b" -} -``` - -| Field | Description | -| --- | --- | -| `addressSchema` | JSON Schema describing the address input fields. The core renders this with `SchemaFormWidget` | -| `dataTypes` | Array of available data types. Each entry has `id` (used in expression strings) and `label` (UI display) | -| `defaultDataType` | The `id` of the type to pre-select in the UI | - -The `addressSchema` follows standard JSON Schema conventions. The core application uses it to dynamically generate the address input portion of the data point dialog, so it must accurately describe all required fields and their constraints. The `dataType` property within `addressSchema` has no `enum` constraint; the available values are supplied by the top-level `dataTypes` array, and `defaultDataType` (`"16b"`) indicates which value to pre-select. - ---- - -### `adapter.describeDataPoint` - -Parses a data point expression into structured fields and returns a human-readable description. Used by the core to display data point details in tables and tooltips without understanding protocol-specific address formats. - -**Params:** -```json -{ - "expression": "${40001: 16b}" -} -``` - -**Result (valid):** -```json -{ - "valid": true, - "fields": { - "objectType": "holding register", - "address": 0, - "deviceId": 1, - "dataType": "16b" - }, - "description": "holding register, 0, unsigned 16-bit, device id 1" -} -``` - -**Result (invalid):** -```json -{ - "valid": false, - "error": "Unknown type 'xyz'" -} -``` - -| Field | Description | -| --- | --- | -| `valid` | Whether the expression is syntactically and semantically valid | -| `fields` | Structured parsed fields — protocol-specific, but the core treats them as opaque display data | -| `description` | Human-readable description for display in tables, tooltips, and logs | -| `error` | Human-readable error message when `valid` is false | - -**Errors:** -- `-32602` — Missing `expression` field - ---- - -### `adapter.validateDataPoint` - -Validates a single data point expression string without starting polling. Used for real-time validation feedback in the data point input dialog. - -**Params:** -```json -{ - "expression": "${40001: 16b}" -} -``` - -**Result (valid):** -```json -{ "valid": true } -``` - -**Result (invalid):** -```json -{ - "valid": false, - "error": "Unknown type 'xyz'" -} -``` - -| Field | Description | -| --- | --- | -| `valid` | Whether the expression is valid | -| `error` | Human-readable error message when `valid` is false | - -**Errors:** -- `-32602` — Missing `expression` field - ---- - -### `adapter.buildExpression` - -Constructs a register expression string from its component parts. The core calls this after the user fills in the register address form and selects a data type and device, so expression syntax stays entirely within the adapter. - -**Params:** - -```json -{ - "fields": { - "objectType": "holding register", - "address": 0 - }, - "dataType": "f32b", - "deviceId": 2 -} -``` - -| Field | Type | Required | Description | -| --- | --- | --- | --- | -| `fields` | object | yes | Address field values as returned by the data point schema form (structure matches `addressSchema` from `adapter.dataPointSchema`) | -| `dataType` | string | no | Data type identifier (e.g. `"16b"`). Omit to use the adapter default | -| `deviceId` | integer | no | Device identifier from `adapter.configure`. Omit to use the adapter default | - -**Result:** - -```json -{ "expression": "${h0@2:f32b}" } -``` - -**Errors:** - -- `-32602` — Missing or invalid `fields`; unknown `dataType` - ---- - -### `adapter.expressionHelp` - -Returns static HTML help text describing the register expression syntax. The core displays this in the expression editor info panel so the explanation stays co-located with the adapter that owns the syntax. - -**Params:** `{}` (none required) - -**Result:** - -```json -{ "helpText": "..." } -``` - -| Field | Type | Description | -| --- | --- | --- | -| `helpText` | string | HTML string suitable for display in a rich-text label | - ---- - -### `adapter.getStatus` - -Returns the current poll activity state. - -**Params:** `{}` (none required) - -**Result:** -```json -{ "active": true } -``` - ---- - -### `adapter.readData` - -Reads a list of Modbus registers. The adapter starts the read cycle and returns the result when all registers have been sampled. - -Only one `readData` request can be in flight at a time. A second request while the first is pending returns an error immediately. - -`adapter.start` must have been called before invoking this method. - -**Params:** `{}` (none required) - -**Result:** -```json -{ - "registers": [ - { "value": 42.0, "valid": true }, - { "value": 0.0, "valid": false } - ] -} -``` - -A register with `"valid": false` could not be read (communication error, timeout, or no device configured). Its `"value"` is `0.0`. - -**Errors:** -- `-32000` — Read already in progress (a previous `readData` has not yet completed) -- `-32000` — Read timed out (Modbus I/O did not complete within the timeout window) - ---- - -### `adapter.shutdown` - -Stops all Modbus activity and terminates the adapter process. - -**Params:** `{}` (none required) - -**Result:** -```json -{ "status": "ok" } -``` - -The adapter process exits after sending this response. - ---- - -## Notifications - -Notifications are sent by the adapter to the client without a corresponding request. They have no `id` field and expect no response. - -### `adapter.diagnostic` - -Carries a log or diagnostic message from the adapter. Emitted for every Qt log message (`qDebug`, `qInfo`, `qWarning`, `qCritical`, `qFatal`) produced during adapter operation. - -```json -{ - "jsonrpc": "2.0", - "method": "adapter.diagnostic", - "params": { - "level": "warning", - "message": "Connection timeout on connection 0" - } -} -``` - -| `level` | Meaning | -| --- | --- | -| `"debug"` | Verbose internal trace | -| `"info"` | Informational | -| `"warning"` | Non-fatal issue | -| `"error"` | Critical or fatal error | - ---- - -## Error Codes - -| Code | Name | Description | -| --- | --- | --- | -| `-32700` | Parse error | The message body is not valid JSON | -| `-32600` | Invalid request | Missing `jsonrpc`/`method` fields | -| `-32601` | Method not found | Unknown method name | -| `-32602` | Invalid params | Required fields missing or out of range | -| `-32603` | Internal error | Unexpected server-side error | -| `-32000` | Server error | `adapter.readData` called while a previous read has not yet completed, or the read timed out | - ---- - -## Session Lifecycle - -A typical session follows this sequence: - -```text -Client Adapter - | | - |-- adapter.initialize ------------> | - |<- { "status": "ok" } ------------ | - | | - |-- adapter.describe -------------> | - |<- { "name": ..., "schema": ... } - | - | | - |-- adapter.configure ------------> | - |<- { "status": "ok" } ------------ | - | | - |-- adapter.start ----------------> | - |<- { "status": "ok" } ------------ | - | | - |-- adapter.getStatus ------------> | - |<- { "active": true } ------------ | - | | - |-- adapter.readData -------------> | - | [Modbus I/O ...] | - |<- { "registers": [...] } -------- | - | | - |-- adapter.readData -------------> | - | [Modbus I/O ...] | - |<- { "registers": [...] } -------- | - | | - |-- adapter.shutdown -------------> | - |<- { "status": "ok" } ------------ | - | [exits] | -``` - -If stdin is closed (EOF), the adapter shuts down automatically. - ---- - -## Example - -Start a session with a TCP connection on connection 0 and one device, then read a register. - -Initialize: - -```text -Content-Length: 66\r\n -\r\n -{"jsonrpc":"2.0","id":1,"method":"adapter.initialize","params":{}} -``` - -Response: - -```text -Content-Length: 49\r\n -\r\n -{"id":1,"jsonrpc":"2.0","result":{"status":"ok"}} -``` - -Describe adapter: - -```text -Content-Length: 64\r\n -\r\n -{"jsonrpc":"2.0","id":2,"method":"adapter.describe","params":{}} -``` - -Response: - -```text -Content-Length: \r\n -\r\n -{"id":2,"jsonrpc":"2.0","result":{"name":"modbusAdapter","version":"...","configVersion":1,"schema":{...},"defaults":{...},"capabilities":{...}}} -``` - -Configure connection and device: - -```text -Content-Length: 255\r\n -\r\n -{"jsonrpc":"2.0","id":3,"method":"adapter.configure","params":{"config":{"version":1,"general":{},"connections":[{"id":0,"type":"tcp","ip":"192.168.1.100","port":502,"timeout":1000}],"devices":[{"id":1,"connectionId":0,"slaveId":1,"consecutiveMax":64}]}}} -``` - -Response: - -```text -Content-Length: 49\r\n -\r\n -{"id":3,"jsonrpc":"2.0","result":{"status":"ok"}} -``` - -Start polling: - -```text -Content-Length: 90\r\n -\r\n -{"jsonrpc":"2.0","id":4,"method":"adapter.start","params":{"registers":["${40001: 16b}"]}} -``` - -Response: - -```text -Content-Length: 49\r\n -\r\n -{"id":4,"jsonrpc":"2.0","result":{"status":"ok"}} -``` - -Read request: - -```text -Content-Length: 64\r\n -\r\n -{"jsonrpc":"2.0","id":5,"method":"adapter.readData","params":{}} -``` - -Response (when Modbus read completes): - -```text -Content-Length: 77\r\n -\r\n -{"id":5,"jsonrpc":"2.0","result":{"registers":[{"valid":true,"value":1234}]}} -``` diff --git a/adapters/modbus-adapter-spec.md b/adapters/modbus-adapter-spec.md new file mode 100644 index 00000000..1be5d3c5 --- /dev/null +++ b/adapters/modbus-adapter-spec.md @@ -0,0 +1,398 @@ +# Modbus Adapter Implementation Specification + +This document defines the Modbus adapter's concrete payloads for the generic methods defined in adapter. Refer to the protocol spec for transport framing, method signatures, the notification and error-code contracts, and the session lifecycle. + +Only methods whose request or response shape is Modbus-specific are documented here. Methods not listed (`adapter.initialize`, `adapter.getStatus`, `adapter.readData`, `adapter.shutdown`) use the generic payloads defined in the protocol spec without further specialization. + +--- + +## `adapter.describe` + +**Result (Release build):** +```json +{ + "name": "modbusAdapter", + "version": "0.0.1", + "configVersion": 1, + "schema": { ... }, + "defaults": { ... }, + "capabilities": { + "supportsHotReload": false, + "requiresRestartOn": ["connections", "devices"] + } +} +``` + +**Result (Debug build):** the `version` field appends `-+`, e.g. `"0.0.1-dev-diagnostics+c471210"`. Branch names with slashes are converted to hyphens (e.g. `dev/diagnostics` → `dev-diagnostics`). Consumers must not treat `version` as a fixed literal; parse or compare it accordingly. + +> **Note:** Update this spec whenever the Modbus JSON-RPC implementation changes. + +| Field | Description | +| --- | --- | +| `name` | Adapter identifier (`"modbusAdapter"`) | +| `version` | Adapter software version. Release: `""`. Debug: `"-+"` (slashes in branch name replaced with hyphens). | +| `configVersion` | Current config schema version | +| `schema` | JSON Schema–compatible object describing the `config` object accepted by `adapter.configure` | +| `defaults` | Default config values. The `connections` array contains a single TCP entry that also includes serial-specific fields (`portName`, `baudrate`, `parity`, `databits`, `stopbits`) so callers can see serial defaults without needing a second example. | +| `capabilities` | Feature flags | + +The connection schema uses JSON Schema Draft 7 `if`/`then`/`else` to express type-dependent fields. When `type` equals `"tcp"`, the fields in `then.properties` apply (TCP-specific). Otherwise, when `type` is not `"tcp"`, the fields in `else.properties` apply (serial-specific). A UI can use this to enable or disable the relevant fields based on the selected connection type. + +--- + +## `adapter.configure` + +Applies Modbus connection and device configuration to the adapter. Must be called before `adapter.start`. + +**Params:** +```json +{ + "config": { + "version": 1, + "general": {}, + "connections": [ + { + "id": 0, + "type": "tcp", + "ip": "192.168.1.100", + "port": 502, + "timeout": 1000, + "persistent": false + }, + { + "id": 1, + "type": "serial", + "portName": "/dev/ttyUSB0", + "baudrate": 115200, + "parity": "N", + "databits": 8, + "stopbits": 1, + "timeout": 1000, + "persistent": false + } + ], + "devices": [ + { + "id": 1, + "connectionId": 0, + "slaveId": 1, + "consecutiveMax": 64, + "int32LittleEndian": false + } + ] + } +} +``` + +**General fields:** + +Adapter-wide settings. Currently empty; reserved for future use. + +| Field | Type | Description | +| --- | --- | --- | +| *(none yet)* | | | + +**Connection fields:** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `id` | integer | yes | Connection index: `0`, `1`, or `2` | +| `name` | string | no | Human-readable label (default: `"Connection "`) | +| `type` | string | yes | `"tcp"` or `"serial"` | +| `timeout` | integer | no | Timeout in milliseconds (default: `1000`) | +| `persistent` | boolean | no | Keep connection open between reads (default: `true`) | + +TCP-specific fields: + +| Field | Type | Default | Description | +| --- | --- | --- | --- | +| `ip` | string | `"127.0.0.1"` | IP address | +| `port` | integer | `502` | TCP port | + +Serial-specific fields: + +| Field | Type | Default | Description | +| --- | --- | --- | --- | +| `portName` | string | | Serial port name (e.g. `/dev/ttyUSB0`, `COM1`) | +| `baudrate` | integer | `9600` | Baud rate | +| `parity` | string | `"N"` | `"N"` (none), `"E"` (even), `"O"` (odd) | +| `databits` | integer | `8` | Data bits: `5`, `6`, `7`, or `8` | +| `stopbits` | integer | `1` | Stop bits: `1` (one), `2` (two), or `3` (one-and-a-half) | + +**Device fields:** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `id` | integer | yes | Device identifier (unique within the adapter) | +| `connectionId` | integer | yes | Which connection this device is on (`0`–`2`) | +| `slaveId` | integer | yes | Modbus slave ID (`1`–`247`) | +| `consecutiveMax` | integer | no | Max registers per single read request (default: `125`) | +| `int32LittleEndian` | boolean | no | Byte order for 32-bit values (default: `true` = little endian) | + +--- + +## `adapter.start` + +Starts Modbus polling with the configuration applied by `adapter.configure`. + +**Params:** + +```json +{ + "registers": ["${40001: 16b}", "${h0 @ 2: f32b}"] +} +``` + +Each element is a register subexpression string with the syntax: +`${address [@ deviceId] [: type]}` + +**Address format:** + +| Prefix | Object type | Example | +| --- | --- | --- | +| `00001`–`09999` or `c` | Coils | `c50`, `00001` | +| `10001`–`19999` or `d` | Discrete inputs | `d100`, `10001` | +| `30001`–`39999` or `i` | Input registers | `i0`, `30001` | +| `40001`–`49999` or `h` | Holding registers | `h0`, `40001` | + +**Optional fields:** + +| Field | Syntax | Default | Description | +| --- | --- | --- | --- | +| `deviceId` | `@ N` | `1` | Device ID from `adapter.configure` | +| `type` | `: type` | `16b` | Data type | + +**Type values:** + +| Value | Description | +| --- | --- | +| `"16b"` | Unsigned 16-bit integer (default) | +| `"s16b"` | Signed 16-bit integer | +| `"32b"` | Unsigned 32-bit integer (two consecutive registers) | +| `"s32b"` | Signed 32-bit integer | +| `"f32b"` | 32-bit IEEE 754 float | + +--- + +## `adapter.dataPointSchema` + +**Result:** +```json +{ + "addressSchema": { + "type": "object", + "properties": { + "objectType": { + "type": "string", + "title": "Object type", + "enum": ["coil", "discrete input", "input register", "holding register"], + "x-enumLabels": ["Coil", "Discrete Input", "Input Register", "Holding Register"] + }, + "address": { + "type": "integer", + "title": "Address", + "minimum": 0, + "maximum": 65535 + }, + "deviceId": { + "type": "integer", + "title": "Device ID", + "minimum": 1 + }, + "dataType": { + "type": "string", + "title": "Data type" + } + }, + "required": ["objectType", "address"] + }, + "dataTypes": [ + { "id": "16b", "label": "unsigned 16-bit" }, + { "id": "s16b", "label": "signed 16-bit" }, + { "id": "32b", "label": "unsigned 32-bit" }, + { "id": "s32b", "label": "signed 32-bit" }, + { "id": "f32b", "label": "32-bit float" } + ], + "defaultDataType": "16b" +} +``` + +The `dataType` property within `addressSchema` has no `enum` constraint; the available values are supplied by the top-level `dataTypes` array, and `defaultDataType` (`"16b"`) indicates which value to pre-select. + +--- + +## `adapter.describeDataPoint` + +**Params:** +```json +{ + "expression": "${40001: 16b}" +} +``` + +**Result (valid):** +```json +{ + "valid": true, + "fields": { + "objectType": "holding register", + "address": 0, + "deviceId": 1, + "dataType": "16b" + }, + "description": "holding register, 0, unsigned 16-bit, device id 1" +} +``` + +**Result (invalid):** +```json +{ + "valid": false, + "error": "Unknown type 'xyz'" +} +``` + +--- + +## `adapter.validateDataPoint` + +**Params:** +```json +{ + "expression": "${40001: 16b}" +} +``` + +**Result (valid):** +```json +{ "valid": true } +``` + +**Result (invalid):** +```json +{ + "valid": false, + "error": "Unknown type 'xyz'" +} +``` + +--- + +## `adapter.buildExpression` + +**Params:** + +```json +{ + "fields": { + "objectType": "holding register", + "address": 0 + }, + "dataType": "f32b", + "deviceId": 2 +} +``` + +**Result:** + +```json +{ "expression": "${h0@2:f32b}" } +``` + +--- + +## `adapter.expressionHelp` + +**Result:** + +```json +{ "helpText": "..." } +``` + +The returned HTML documents the Modbus register expression syntax defined under [`adapter.start`](#adapterstart) (address prefixes, optional `deviceId` and `type`, and the list of type values). + +--- + +## Example + +Start a session with a TCP connection on connection 0 and one device, then read a register. + +Initialize: + +```text +Content-Length: 66\r\n +\r\n +{"jsonrpc":"2.0","id":1,"method":"adapter.initialize","params":{}} +``` + +Response: + +```text +Content-Length: 49\r\n +\r\n +{"id":1,"jsonrpc":"2.0","result":{"status":"ok"}} +``` + +Describe adapter: + +```text +Content-Length: 64\r\n +\r\n +{"jsonrpc":"2.0","id":2,"method":"adapter.describe","params":{}} +``` + +Response: + +```text +Content-Length: \r\n +\r\n +{"id":2,"jsonrpc":"2.0","result":{"name":"modbusAdapter","version":"...","configVersion":1,"schema":{...},"defaults":{...},"capabilities":{...}}} +``` + +Configure connection and device: + +```text +Content-Length: 255\r\n +\r\n +{"jsonrpc":"2.0","id":3,"method":"adapter.configure","params":{"config":{"version":1,"general":{},"connections":[{"id":0,"type":"tcp","ip":"192.168.1.100","port":502,"timeout":1000}],"devices":[{"id":1,"connectionId":0,"slaveId":1,"consecutiveMax":64}]}}} +``` + +Response: + +```text +Content-Length: 49\r\n +\r\n +{"id":3,"jsonrpc":"2.0","result":{"status":"ok"}} +``` + +Start polling: + +```text +Content-Length: 90\r\n +\r\n +{"jsonrpc":"2.0","id":4,"method":"adapter.start","params":{"registers":["${40001: 16b}"]}} +``` + +Response: + +```text +Content-Length: 49\r\n +\r\n +{"id":4,"jsonrpc":"2.0","result":{"status":"ok"}} +``` + +Read request: + +```text +Content-Length: 64\r\n +\r\n +{"jsonrpc":"2.0","id":5,"method":"adapter.readData","params":{}} +``` + +Response (when Modbus read completes): + +```text +Content-Length: 77\r\n +\r\n +{"id":5,"jsonrpc":"2.0","result":{"registers":[{"valid":true,"value":1234}]}} +``` diff --git a/adapters/modbusadapter b/adapters/modbusadapter index b618b936..13e481a0 100755 Binary files a/adapters/modbusadapter and b/adapters/modbusadapter differ diff --git a/adapters/modbusadapter.exe b/adapters/modbusadapter.exe index 0ad7182d..455b339e 100644 Binary files a/adapters/modbusadapter.exe and b/adapters/modbusadapter.exe differ