Skip to content

Commit 0b3d910

Browse files
authored
Merge pull request #26 from ModbusScope/dev/subexpressions
Subexpressions info from adapter
2 parents a6172ee + e8063cc commit 0b3d910

35 files changed

Lines changed: 1370 additions & 304 deletions

adapters/dummymodbusadapter

105 KB
Binary file not shown.

adapters/dummymodbusadapter.exe

156 KB
Binary file not shown.

adapters/json-rpc-spec.md

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,177 @@ An empty `registers` array is valid and starts polling with no registers configu
277277

278278
---
279279

280+
### `adapter.dataPointSchema`
281+
282+
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.
283+
284+
**Params:** `{}` (none required)
285+
286+
**Result:**
287+
```json
288+
{
289+
"addressSchema": {
290+
"type": "object",
291+
"properties": {
292+
"objectType": {
293+
"type": "string",
294+
"title": "Object type",
295+
"enum": ["coil", "discrete input", "input register", "holding register"],
296+
"x-enumLabels": ["Coil", "Discrete Input", "Input Register", "Holding Register"]
297+
},
298+
"address": {
299+
"type": "integer",
300+
"title": "Address",
301+
"minimum": 0,
302+
"maximum": 65535
303+
},
304+
"deviceId": {
305+
"type": "integer",
306+
"title": "Device ID",
307+
"minimum": 1
308+
},
309+
"dataType": {
310+
"type": "string",
311+
"title": "Data type"
312+
}
313+
},
314+
"required": ["objectType", "address"]
315+
},
316+
"dataTypes": [
317+
{ "id": "16b", "label": "unsigned 16-bit" },
318+
{ "id": "s16b", "label": "signed 16-bit" },
319+
{ "id": "32b", "label": "unsigned 32-bit" },
320+
{ "id": "s32b", "label": "signed 32-bit" },
321+
{ "id": "f32b", "label": "32-bit float" }
322+
],
323+
"defaultDataType": "16b"
324+
}
325+
```
326+
327+
| Field | Description |
328+
| --- | --- |
329+
| `addressSchema` | JSON Schema describing the address input fields. The core renders this with `SchemaFormWidget` |
330+
| `dataTypes` | Array of available data types. Each entry has `id` (used in expression strings) and `label` (UI display) |
331+
| `defaultDataType` | The `id` of the type to pre-select in the UI |
332+
333+
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.
334+
335+
---
336+
337+
### `adapter.describeDataPoint`
338+
339+
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.
340+
341+
**Params:**
342+
```json
343+
{
344+
"expression": "${40001: 16b}"
345+
}
346+
```
347+
348+
**Result (valid):**
349+
```json
350+
{
351+
"valid": true,
352+
"fields": {
353+
"objectType": "holding register",
354+
"address": 0,
355+
"deviceId": 1,
356+
"dataType": "16b"
357+
},
358+
"description": "holding register, 0, unsigned 16-bit, device id 1"
359+
}
360+
```
361+
362+
**Result (invalid):**
363+
```json
364+
{
365+
"valid": false,
366+
"error": "Unknown type 'xyz'"
367+
}
368+
```
369+
370+
| Field | Description |
371+
| --- | --- |
372+
| `valid` | Whether the expression is syntactically and semantically valid |
373+
| `fields` | Structured parsed fields — protocol-specific, but the core treats them as opaque display data |
374+
| `description` | Human-readable description for display in tables, tooltips, and logs |
375+
| `error` | Human-readable error message when `valid` is false |
376+
377+
**Errors:**
378+
- `-32602` — Missing `expression` field
379+
380+
---
381+
382+
### `adapter.validateDataPoint`
383+
384+
Validates a single data point expression string without starting polling. Used for real-time validation feedback in the data point input dialog.
385+
386+
**Params:**
387+
```json
388+
{
389+
"expression": "${40001: 16b}"
390+
}
391+
```
392+
393+
**Result (valid):**
394+
```json
395+
{ "valid": true }
396+
```
397+
398+
**Result (invalid):**
399+
```json
400+
{
401+
"valid": false,
402+
"error": "Unknown type 'xyz'"
403+
}
404+
```
405+
406+
| Field | Description |
407+
| --- | --- |
408+
| `valid` | Whether the expression is valid |
409+
| `error` | Human-readable error message when `valid` is false |
410+
411+
**Errors:**
412+
- `-32602` — Missing `expression` field
413+
414+
---
415+
416+
### `adapter.buildExpression`
417+
418+
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.
419+
420+
**Params:**
421+
422+
```json
423+
{
424+
"fields": {
425+
"objectType": "holding register",
426+
"address": 0
427+
},
428+
"dataType": "f32b",
429+
"deviceId": 2
430+
}
431+
```
432+
433+
| Field | Type | Required | Description |
434+
| --- | --- | --- | --- |
435+
| `fields` | object | yes | Address field values as returned by the data point schema form (structure matches `addressSchema` from `adapter.dataPointSchema`) |
436+
| `dataType` | string | no | Data type identifier (e.g. `"16b"`). Omit to use the adapter default |
437+
| `deviceId` | integer | no | Device identifier from `adapter.configure`. Omit to use the adapter default |
438+
439+
**Result:**
440+
441+
```json
442+
{ "expression": "${h0@2:f32b}" }
443+
```
444+
445+
**Errors:**
446+
447+
- `-32602` — Missing or invalid `fields`; unknown `dataType`
448+
449+
---
450+
280451
### `adapter.getStatus`
281452

282453
Returns the current poll activity state.

adapters/modbusadapter

109 KB
Binary file not shown.

adapters/modbusadapter.exe

156 KB
Binary file not shown.

src/ProtocolAdapter/adapterclient.cpp

Lines changed: 128 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ void AdapterClient::provideConfig(QJsonObject config, QStringList registerExpres
5151

5252
_pendingExpressions = registerExpressions;
5353
_pendingConfig = config;
54+
_pendingAuxRequests.clear();
5455
_state = State::CONFIGURING;
5556
_handshakeTimer.start(_handshakeTimeoutMs);
5657
QJsonObject params;
@@ -80,6 +81,85 @@ void AdapterClient::requestStatus()
8081
_pProcess->sendRequest("adapter.getStatus", QJsonObject());
8182
}
8283

84+
/*!
85+
* \brief Request the adapter's data point schema while awaiting configuration.
86+
*/
87+
void AdapterClient::requestDataPointSchema()
88+
{
89+
if (_state != State::AWAITING_CONFIG)
90+
{
91+
qCWarning(scopeComm) << "AdapterClient: requestDataPointSchema called in unexpected state"
92+
<< static_cast<int>(_state);
93+
return;
94+
}
95+
96+
_pendingAuxRequests["adapter.dataPointSchema"] = _pProcess->sendRequest("adapter.dataPointSchema", QJsonObject());
97+
}
98+
99+
/*!
100+
* \brief Request a human-readable description of a data point expression.
101+
* \param expression The data point expression string to describe.
102+
*/
103+
void AdapterClient::describeDataPoint(const QString& expression)
104+
{
105+
if (_state != State::AWAITING_CONFIG && _state != State::ACTIVE)
106+
{
107+
qCWarning(scopeComm) << "AdapterClient: describeDataPoint called in unexpected state"
108+
<< static_cast<int>(_state);
109+
return;
110+
}
111+
112+
QJsonObject params;
113+
params["expression"] = expression;
114+
_pendingAuxRequests["adapter.describeDataPoint"] = _pProcess->sendRequest("adapter.describeDataPoint", params);
115+
}
116+
117+
/*!
118+
* \brief Validate a data point expression string via the adapter.
119+
* \param expression The data point expression string to validate.
120+
*/
121+
void AdapterClient::validateDataPoint(const QString& expression)
122+
{
123+
if (_state != State::AWAITING_CONFIG && _state != State::ACTIVE)
124+
{
125+
qCWarning(scopeComm) << "AdapterClient: validateDataPoint called in unexpected state"
126+
<< static_cast<int>(_state);
127+
return;
128+
}
129+
130+
QJsonObject params;
131+
params["expression"] = expression;
132+
_pendingAuxRequests["adapter.validateDataPoint"] = _pProcess->sendRequest("adapter.validateDataPoint", params);
133+
}
134+
135+
/*!
136+
* \brief Send an adapter.buildExpression request to construct a data point expression string.
137+
* \param addressFields Address field values as returned by the data point schema form.
138+
* \param dataType Data type identifier; omitted from params when empty.
139+
* \param deviceId Device identifier; omitted from params when zero.
140+
*/
141+
void AdapterClient::buildExpression(const QJsonObject& addressFields, const QString& dataType, deviceId_t deviceId)
142+
{
143+
if (_state != State::AWAITING_CONFIG && _state != State::ACTIVE)
144+
{
145+
qCWarning(scopeComm) << "AdapterClient: buildExpression called in unexpected state" << static_cast<int>(_state);
146+
return;
147+
}
148+
149+
QJsonObject params;
150+
params["fields"] = addressFields;
151+
const QString trimmedDataType = dataType.trimmed();
152+
if (!trimmedDataType.isEmpty())
153+
{
154+
params["dataType"] = trimmedDataType;
155+
}
156+
if (deviceId != 0)
157+
{
158+
params["deviceId"] = static_cast<qint64>(deviceId);
159+
}
160+
_pendingAuxRequests["adapter.buildExpression"] = _pProcess->sendRequest("adapter.buildExpression", params);
161+
}
162+
83163
void AdapterClient::stopSession()
84164
{
85165
if (_state == State::IDLE || _state == State::STOPPING)
@@ -88,6 +168,7 @@ void AdapterClient::stopSession()
88168
}
89169

90170
_handshakeTimer.stop();
171+
_pendingAuxRequests.clear();
91172

92173
if (_state == State::ACTIVE || _state == State::STARTING)
93174
{
@@ -105,17 +186,17 @@ void AdapterClient::stopSession()
105186

106187
void AdapterClient::onResponseReceived(int id, const QString& method, const QJsonValue& result)
107188
{
108-
Q_UNUSED(id)
109189
if (result.isObject())
110190
{
111-
handleLifecycleResponse(method, result.toObject());
191+
handleLifecycleResponse(id, method, result.toObject());
112192
}
113193
else
114194
{
115195
qCWarning(scopeComm) << "AdapterClient: unexpected non-object result for" << method;
116196
_handshakeTimer.stop();
117197
/* Set IDLE before stop() so onProcessFinished's IDLE guard suppresses any
118198
duplicate sessionError emission when the process exits asynchronously. */
199+
_pendingAuxRequests.clear();
119200
_state = State::IDLE;
120201
_pProcess->stop();
121202
emit sessionError(QString("Unexpected non-object result for %1").arg(method));
@@ -132,6 +213,7 @@ void AdapterClient::onErrorReceived(int id, const QString& method, const QJsonOb
132213
State previousState = _state;
133214
/* Set IDLE before stop() so onProcessFinished's IDLE guard suppresses any
134215
duplicate sessionError emission when the process exits asynchronously. */
216+
_pendingAuxRequests.clear();
135217
_state = State::IDLE;
136218
_pProcess->stop();
137219

@@ -146,6 +228,7 @@ void AdapterClient::onProcessError(const QString& message)
146228
_handshakeTimer.stop();
147229
if (_state != State::STOPPING)
148230
{
231+
_pendingAuxRequests.clear();
149232
_state = State::IDLE;
150233
emit sessionError(message);
151234
}
@@ -154,6 +237,7 @@ void AdapterClient::onProcessError(const QString& message)
154237
void AdapterClient::onProcessFinished()
155238
{
156239
_handshakeTimer.stop();
240+
_pendingAuxRequests.clear();
157241
if (_state == State::STOPPING)
158242
{
159243
_state = State::IDLE;
@@ -170,6 +254,7 @@ void AdapterClient::onHandshakeTimeout()
170254
{
171255
qCWarning(scopeComm) << "AdapterClient: handshake timed out in state" << static_cast<int>(_state);
172256
bool wasStopping = (_state == State::STOPPING);
257+
_pendingAuxRequests.clear();
173258
_state = State::IDLE;
174259
_pProcess->stop();
175260
if (wasStopping)
@@ -200,7 +285,7 @@ void AdapterClient::onNotificationReceived(QString method, QJsonValue params)
200285
obj.value(QStringLiteral("message")).toString());
201286
}
202287

203-
void AdapterClient::handleLifecycleResponse(const QString& method, const QJsonObject& result)
288+
void AdapterClient::handleLifecycleResponse(int id, const QString& method, const QJsonObject& result)
204289
{
205290
if (method == "adapter.initialize" && _state == State::INITIALIZING)
206291
{
@@ -258,6 +343,46 @@ void AdapterClient::handleLifecycleResponse(const QString& method, const QJsonOb
258343
_pProcess->stop();
259344
/* sessionStopped is emitted from onProcessFinished once the process exits */
260345
}
346+
else if (method == "adapter.dataPointSchema" && _state == State::AWAITING_CONFIG)
347+
{
348+
if (_pendingAuxRequests.value(method, -1) != id)
349+
{
350+
qCWarning(scopeComm) << "AdapterClient: ignoring stale response for" << method;
351+
return;
352+
}
353+
_pendingAuxRequests.remove(method);
354+
emit dataPointSchemaResult(result);
355+
}
356+
else if (method == "adapter.describeDataPoint" && (_state == State::AWAITING_CONFIG || _state == State::ACTIVE))
357+
{
358+
if (_pendingAuxRequests.value(method, -1) != id)
359+
{
360+
qCWarning(scopeComm) << "AdapterClient: ignoring stale response for" << method;
361+
return;
362+
}
363+
_pendingAuxRequests.remove(method);
364+
emit describeDataPointResult(result);
365+
}
366+
else if (method == "adapter.validateDataPoint" && (_state == State::AWAITING_CONFIG || _state == State::ACTIVE))
367+
{
368+
if (_pendingAuxRequests.value(method, -1) != id)
369+
{
370+
qCWarning(scopeComm) << "AdapterClient: ignoring stale response for" << method;
371+
return;
372+
}
373+
_pendingAuxRequests.remove(method);
374+
emit validateDataPointResult(result["valid"].toBool(), result["error"].toString());
375+
}
376+
else if (method == "adapter.buildExpression" && (_state == State::AWAITING_CONFIG || _state == State::ACTIVE))
377+
{
378+
if (_pendingAuxRequests.value(method, -1) != id)
379+
{
380+
qCWarning(scopeComm) << "AdapterClient: ignoring stale response for" << method;
381+
return;
382+
}
383+
_pendingAuxRequests.remove(method);
384+
emit buildExpressionResult(result["expression"].toString());
385+
}
261386
else
262387
{
263388
qCWarning(scopeComm) << "AdapterClient: unexpected response for" << method << "in state"

0 commit comments

Comments
 (0)