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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions adapters/json-rpc-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,133 @@ An empty `registers` array is valid and starts polling with no registers configu

---

### `adapter.registerSchema`

Returns the schema for register expressions — what fields make up a register address, how they should be rendered in the UI, and available data types. Call this after `adapter.describe` to discover how to build the register 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
}
},
"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 register dialog, so it must accurately describe all required fields and their constraints.

---

### `adapter.describeRegister`

Parses a register expression into structured fields and returns a human-readable description. Used by the core to display register 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, device 1, unsigned 16-bit"
}
```

**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.validateRegister`

Validates a single register expression string without starting polling. Used for real-time validation feedback in the register 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.getStatus`

Returns the current poll activity state.
Expand Down
52 changes: 52 additions & 0 deletions src/ProtocolAdapter/adapterclient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,46 @@ void AdapterClient::requestStatus()
_pProcess->sendRequest("adapter.getStatus", QJsonObject());
}

void AdapterClient::requestRegisterSchema()
{
if (_state != State::AWAITING_CONFIG)
{
qCWarning(scopeComm) << "AdapterClient: requestRegisterSchema called in unexpected state"
<< static_cast<int>(_state);
return;
}

_pProcess->sendRequest("adapter.registerSchema", QJsonObject());
}

void AdapterClient::describeRegister(const QString& expression)
{
if (_state != State::AWAITING_CONFIG && _state != State::ACTIVE)
{
qCWarning(scopeComm) << "AdapterClient: describeRegister called in unexpected state"
<< static_cast<int>(_state);
return;
}

QJsonObject params;
params["expression"] = expression;
_pProcess->sendRequest("adapter.describeRegister", params);
}

void AdapterClient::validateRegister(const QString& expression)
{
if (_state != State::AWAITING_CONFIG && _state != State::ACTIVE)
{
qCWarning(scopeComm) << "AdapterClient: validateRegister called in unexpected state"
<< static_cast<int>(_state);
return;
}

QJsonObject params;
params["expression"] = expression;
_pProcess->sendRequest("adapter.validateRegister", params);
}

void AdapterClient::stopSession()
{
if (_state == State::IDLE || _state == State::STOPPING)
Expand Down Expand Up @@ -258,6 +298,18 @@ void AdapterClient::handleLifecycleResponse(const QString& method, const QJsonOb
_pProcess->stop();
/* sessionStopped is emitted from onProcessFinished once the process exits */
}
else if (method == "adapter.registerSchema" && _state == State::AWAITING_CONFIG)
{
emit registerSchemaResult(result);
}
else if (method == "adapter.describeRegister" && (_state == State::AWAITING_CONFIG || _state == State::ACTIVE))
{
emit describeRegisterResult(result);
}
else if (method == "adapter.validateRegister" && (_state == State::AWAITING_CONFIG || _state == State::ACTIVE))
{
emit validateRegisterResult(result["valid"].toBool(), result["error"].toString());
}
else
{
qCWarning(scopeComm) << "AdapterClient: unexpected response for" << method << "in state"
Expand Down
47 changes: 47 additions & 0 deletions src/ProtocolAdapter/adapterclient.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,34 @@ class AdapterClient : public QObject
*/
void stopSession();

/*!
* \brief Send an adapter.registerSchema request to discover the register UI schema.
*
* Must only be called after describeResult() has been emitted (i.e., in the
* AWAITING_CONFIG state). Emits registerSchemaResult() when the adapter responds.
*/
void requestRegisterSchema();

/*!
* \brief Send an adapter.describeRegister request to parse a register expression.
*
* Can be called in AWAITING_CONFIG or ACTIVE state.
* Emits describeRegisterResult() when the adapter responds.
*
* \param expression The register expression string to describe.
*/
void describeRegister(const QString& expression);

/*!
* \brief Send an adapter.validateRegister request to validate a register expression.
*
* Can be called in AWAITING_CONFIG or ACTIVE state.
* Emits validateRegisterResult() when the adapter responds.
*
* \param expression The register expression string to validate.
*/
void validateRegister(const QString& expression);

signals:
/*!
* \brief Emitted when the adapter has been initialized, described, configured, and started.
Expand Down Expand Up @@ -124,6 +152,25 @@ class AdapterClient : public QObject
*/
void diagnosticReceived(QString level, QString message);

/*!
* \brief Emitted when an adapter.registerSchema response has been received.
* \param schema The full register schema object (addressSchema, dataTypes, defaultDataType).
*/
void registerSchemaResult(QJsonObject schema);

/*!
* \brief Emitted when an adapter.describeRegister response has been received.
* \param result The full result object (valid, fields, description or error).
*/
void describeRegisterResult(QJsonObject result);

/*!
* \brief Emitted when an adapter.validateRegister response has been received.
* \param valid Whether the expression is valid.
* \param error Human-readable error message when valid is false; empty otherwise.
*/
void validateRegisterResult(bool valid, QString error);

protected:
enum class State
{
Expand Down
9 changes: 7 additions & 2 deletions src/communication/modbuspoll.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@

#include "communication/modbuspoll.h"

#include "models/device.h"
#include "models/settingsmodel.h"
#include "util/formatdatetime.h"
#include "util/modbusdatatype.h"
#include "util/scopelogging.h"

#include <QCoreApplication>
Expand All @@ -25,6 +23,7 @@ ModbusPoll::ModbusPoll(SettingsModel* pSettingsModel, QObject* parent) : QObject
connect(_pAdapterClient, &AdapterClient::sessionStarted, this, &ModbusPoll::triggerRegisterRead);
connect(_pAdapterClient, &AdapterClient::readDataResult, this, &ModbusPoll::onReadDataResult);
connect(_pAdapterClient, &AdapterClient::describeResult, this, &ModbusPoll::onDescribeResult);
connect(_pAdapterClient, &AdapterClient::registerSchemaResult, this, &ModbusPoll::onRegisterSchemaResult);
connect(_pAdapterClient, &AdapterClient::sessionError, this, [this](QString message) {
qCWarning(scopeComm) << "AdapterClient error:" << message;
_bPollActive = false;
Expand Down Expand Up @@ -123,6 +122,12 @@ void ModbusPoll::onReadDataResult(ResultDoubleList results)
void ModbusPoll::onDescribeResult(const QJsonObject& description)
{
_pSettingsModel->updateAdapterFromDescribe("modbus", description);
_pAdapterClient->requestRegisterSchema();
}

void ModbusPoll::onRegisterSchemaResult(const QJsonObject& schema)
{
_pSettingsModel->setAdapterRegisterSchema("modbus", schema);
}

/*! \brief Route an adapter.diagnostic notification to the diagnostics log.
Expand Down
1 change: 1 addition & 0 deletions src/communication/modbuspoll.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ private slots:
void triggerRegisterRead();
void onReadDataResult(ResultDoubleList results);
void onDescribeResult(const QJsonObject& description);
void onRegisterSchemaResult(const QJsonObject& schema);

private:
QStringList buildRegisterExpressions(const QList<DataPoint>& registerList);
Expand Down
Loading