SWMx DIDO Controller Integration with ThinkLink
Integration Documentation
1. Sensor Overview
SWMx is a DIDO controller from JSDZ, mainly used for digital input status acquisition and relay output control.
In this solution, SWMx is connected to the KC11 collector through RS-485. The built-in EdgeBus in KC11 executes the Modbus RTU acquisition logic and uploads data to the ThinkLink platform through LoRaWAN.
Basic device information:
| Item | Description |
|---|---|
| Device Model | SWMx |
| Manufacturer | JSDZ |
| English Name | JSDZ |
| Device Type | DIDO Controller |
| Business Code | 22108 |
| ThinkLink Template | SWMx-22108 |
2. Product Features
The SWMx DIDO controller supports the following features:
- Digital input status acquisition
- Relay output control
- RS-485 / Modbus RTU communication
- LoRaWAN network access through KC11 + EdgeBus
- Status parsing, data visualization, and remote control through ThinkLink
- Reporting of complete RY / DI status bytes
- Parsing of the first 5 RY and DI channels as independent fields
- RPC-based configuration of acquisition period, upload period, and Modbus address
- RPC-based control of specified relay output status
3. Application Scenarios
This solution is suitable for the following scenarios:
- Remote relay control
- Digital input status monitoring
- Equipment running status acquisition
- Access control, magnetic door contact, and switch status monitoring
- Wireless retrofit of DIDO controllers in industrial sites
- Connecting traditional RS-485 controllers to LoRaWAN / ThinkLink
- Remote control through MQTT / RPC from third-party platforms
4. Collector Information
4.1 Hardware Information
This solution uses KC11 as the collector.
| Item | Description |
|---|---|
| Collector Model | KC11 |
| Acquisition Interface | RS-485 |
| Communication Protocol | Modbus RTU |
| Uplink Method | LoRaWAN |
| Power Supply | 220V / 12V |
| Edge Computing Capability | EdgeBus supported |
| Platform Access | ThinkLink |
KC11 reads Modbus data from SWMx through RS-485, uploads the acquisition results to the gateway through LoRaWAN, and finally sends the data to the ThinkLink platform.
4.2 Wiring Information
Power and communication interface:
| Wiring Item | Description |
|---|---|
| KC11 Power | 220V / 12V power supply |
| SWMx Power | Connect according to the actual SWMx power supply requirements |
| RS-485 A | KC11 RS-485 A connected to SWMx A |
| RS-485 B | KC11 RS-485 B connected to SWMx B |
| GND | Connect communication reference ground if required on site |
Sensor interface:
SWMx itself is a DIDO controller. Sensors or field devices are connected to the DI input terminals of SWMx, while controlled devices are connected to the RY / relay output terminals of SWMx.
In this solution, KC11 does not directly connect to DI signals or relay loads. Instead, it reads and controls SWMx through RS-485.
5. Data Acquisition
In this solution, the following data is read through Modbus:
| Data Type | Modbus Function Code | Description |
|---|---|---|
| RY Status | 0x01 | Read coil status |
| DI Status | 0x02 | Read discrete input status |
This acquisition uses Modbus function codes 01 and 02:
0x01: Read RY / Coil status0x02: Read DI / Discrete Input status
It should be noted that function codes 01 and 02 read data by bit, unlike function codes 03 and 04, which read register data directly. However, in this solution, EBHelper already handles this at the lower layer and automatically combines bit data into bytes.
Therefore, the EB code configuration:
{ start: "0", end: "15", covType: "HEX" }means reading 16 bits from bit 0 to bit 15. The lower layer automatically combines the result into 2 bytes for upload.
5.1 Register Definition
RY Status Acquisition
| Item | Description |
|---|---|
| Protocol | Modbus RTU |
| Function Code | 0x01 |
| Read Object | Coil / relay status |
| Start Address | 0 |
| End Address | 15 |
| Data Length | 16 bits, combined into 2 bytes by the lower layer |
| Upload Format | HEX |
| Change Trigger | Supported, covType: "HEX" |
Corresponding EB configuration:
{
protocol: "modbus",
code: "0x01",
periodIndex: 74,
indexAPP: 150,
indexCMD: 0,
copySize: 1,
isLast: false,
listVal: [
{ start: "0", end: "15", covType: "HEX" }
]
}DI Status Acquisition
| Item | Description |
|---|---|
| Protocol | Modbus RTU |
| Function Code | 0x02 |
| Read Object | Discrete Input / digital input status |
| Start Address | 0 |
| End Address | 15 |
| Data Length | 16 bits, combined into 2 bytes by the lower layer |
| Upload Format | HEX |
| Change Trigger | Supported, covType: "HEX" |
Corresponding EB configuration:
{
protocol: "modbus",
code: "0x02",
periodIndex: 74,
indexAPP: 150,
indexCMD: 0,
copySize: 1,
isLast: false,
listVal: [
{ start: "0", end: "15", covType: "HEX" }
]
}5.2 Status Bit Definition
In this solution, the thing model parses both the complete status word and the first 5 channel statuses.
RY Status Bit Definition
| Field Name | field_name | Data Position | Type | Description |
|---|---|---|---|---|
| RY | ry_status_hex | index 6 | hexbe2 | RY 2-byte status value, HEX string |
| RY1 | ry1 | index 6 bit0 | bitLE0-0 | Relay channel 1 status |
| RY2 | ry2 | index 6 bit1 | bitLE1-1 | Relay channel 2 status |
| RY3 | ry3 | index 6 bit2 | bitLE2-2 | Relay channel 3 status |
| RY4 | ry4 | index 6 bit3 | bitLE3-3 | Relay channel 4 status |
| RY5 | ry5 | index 6 bit4 | bitLE4-4 | Relay channel 5 status |
DI Status Bit Definition
| Field Name | field_name | Data Position | Type | Description |
|---|---|---|---|---|
| DI | di_status_hex | index 8 | hexbe2 | DI 2-byte status value, HEX string |
| DI1 | di1 | index 8 bit0 | bitLE0-0 | DI channel 1 status |
| DI2 | di2 | index 8 bit1 | bitLE1-1 | DI channel 2 status |
| DI3 | di3 | index 8 bit2 | bitLE2-2 | DI channel 3 status |
| DI4 | di4 | index 8 bit3 | bitLE3-3 | DI channel 4 status |
| DI5 | di5 | index 8 bit4 | bitLE4-4 | DI channel 5 status |
6. EdgeBus Model
In this solution, SWMx is an RS-485 / Modbus RTU device rather than a native LoRaWAN device. Therefore, the built-in EdgeBus in KC11 is required to execute the acquisition logic.
EdgeBus is responsible for:
- Periodically reading the RY status of SWMx
- Periodically reading the DI status of SWMx
- Reading bit status using Modbus function codes 01 / 02
- Encapsulating the reading results into LoRaWAN uplink data
- Supporting change-triggered upload logic
- Supporting parameter modification through platform RPC
- Supporting relay output control through platform RPC
6.1 EB Configuration Parameters
| Parameter | Description |
|---|---|
| port | 22 |
| dataType | 0x86 |
| version | 0x08 |
| upPeriodIndex | 70 |
| readPeriodIndex | 74 |
| Modbus address parameter index | 150 |
| Modbus baud rate | 9600 |
| Data bits | 8 |
| Stop bits | 1 |
| Parity | NONE |
| Business type | 22108 |
| Business version | 13 |
| EB software version | 31 |
| Battery device | false |
Parameter description:
| Parameter Index | Field | Description |
|---|---|---|
| 70 | period_up | Upload period, in seconds |
| 74 | period_read | Acquisition period, in seconds |
| 150 | addr_modbus | SWMx Modbus address |
6.2 EB Code
import { Buffer } from "buffer";
import { buildOtaFile } from "@EBSDK/run";
import {
ActionAfertExpr, CalcData,
CrcMode,
CvtRule,
EBBuffer,
EBModel,
ExprCondition,
LoraUpEvent,
QueryEvent, SetUpCovDataType, QuItemBase,
UserConfUPItem, EventInfoItem, QuItemModBus, UserConfQueryItem, EventConfig, Utils
} from "@EBSDK/EBCompiler/all_variable";
import { CheckbitEnum, getOtaConfig, HwTypeEnum, UpgrdTypeEnum } from "@EBSDK/otaConfig";
////////////////////////////////////////////////////////////////////////////////////////
// Function codes 01 and 02 read data by bit. EBHelper automatically combines bits into bytes.
// start: "0", end: "15" means reading 16 bits, uploaded as 2 bytes.
// If covType is HEX, the acquired data will be compared with the previous data.
// If there is any difference, an upload will be triggered.
const eventInfo: UserConfUPItem[] = [
{
name: "swm",
dataType: "0x86",
port: 22,
version: "0x08",
upPeriodIndex: 70,
quInfo: [
{
protocol: "modbus",
code: "0x01",
periodIndex: 74,
indexAPP: 150,
indexCMD: 0,
copySize: 1,
isLast: false,
listVal: [
{ start: "0", end: "15", covType: "HEX" }
]
},
{
protocol: "modbus",
code: "0x02",
periodIndex: 74,
indexAPP: 150,
indexCMD: 0,
copySize: 1,
isLast: false,
listVal: [
{ start: "0", end: "15", covType: "HEX" }
]
}
]
}
];
let otaConfig = getOtaConfig({
SwVersion: 31,
BaudRate: 9600,
StopBits: 1,
DataBits: 8,
Checkbit: CheckbitEnum.NONE,
Battery: false,
ConfirmDuty: 60,
BzType: 22108,
BzVersion: 13
});
const MODBUS_TT = (ebModel: EBModel) => {
for (let i = 0; i < eventInfo.length; i++) {
let event = new EventInfoItem(eventInfo[i]);
event.upEventSetup();
event.eventInstall();
}
return JSON.stringify(ebModel, null, 2);
};6.3 Description
Current EB logic:
- KC11 connects to SWMx through RS-485.
- EdgeBus reads SWMx data according to the configured period.
- Modbus function code
0x01is used to read RY status. - Modbus function code
0x02is used to read DI status. start: "0", end: "15"means reading 16 bits.- EBHelper automatically combines the 16-bit result into 2 bytes at the lower layer.
- The thing model uploads the RY status as
ry_status_hex. - The thing model uploads the DI status as
di_status_hex. - RY1–RY5 and DI1–DI5 are also parsed as independent status fields.
- When
covTypeis set toHEX, the acquisition result is compared with the previous data. If the data changes, an upload is triggered. - RPC can be used to configure the acquisition period, upload period, and Modbus address.
- RPC can be used to control specified relay switches.
7. Thing Model
7.1 Basic Thing Model Information
| Item | Description |
|---|---|
| Thing Model Name | SWMx-22108 |
| idName | swm_22108 |
| Business Code | 22108 |
| Data Uplink Port | 22 |
| Parameter Port | 214 |
| Transparent Transmission Port | 51 |
| Data Length | 9 |
| Parse RSSI | Yes |
| Battery Field Index | 4 |
| Data Identifier | 0x86 |
| Version Identifier | 0x08 |
7.2 Uplink Frame Structure
In this solution, SWMx data is uploaded through LoRaWAN port 22.
let frameInfo = {
port: 22,
dataLen: 9,
rssi: true,
battery: 4,
tagList: [
{ index: 0, tag: 0x86 },
{ index: 1, tag: 0x08 }
]
};Uplink data structure:
| Index | Content | Description |
|---|---|---|
| 0 | 0x86 | Data type |
| 1 | 0x08 | Protocol version |
| 4 | battery | Battery or power status field |
| 6~7 | RY status | 2-byte HEX |
| 8~9 | DI status | 2-byte HEX |
Note: According to the provided thing model script, dataLen is configured as 9, RY is parsed from index 6 with 2 bytes, and DI is parsed from index 8 with 2 bytes. During actual deployment, it is recommended to verify whether the frame length needs adjustment based on the real uplink payload.
7.3 Thing Model Script
let port = msg?.userdata?.port || null;
if (port === 51) {
let retval = Utils.paraCheck(Utils.msgType.transParent, device.server_attrs, { device: device, msg: msg });
return {
telemetry_data: retval.tdata,
server_attrs: retval.sdata,
shared_attrs: retval.pdata,
actions: retval.actions
};
}
if (port !== 22) return null;
let frameInfo = {
port: 22,
dataLen: 9,
rssi: true,
battery: 4,
tagList: [
{ index: 0, tag: 0x86 },
{ index: 1, tag: 0x08 }
]
};
let appInfo = [
{ name: "RY", field_name: "ry_status_hex", unit: "", index: 6, type: "hexbe2" },
{ name: "DI", field_name: "di_status_hex", unit: "", index: 8, type: "hexbe2" },
{ name: "RY1", field_name: "ry1", unit: "", index: 6, type: "bitLE0-0" },
{ name: "RY2", field_name: "ry2", unit: "", index: 6, type: "bitLE1-1" },
{ name: "RY3", field_name: "ry3", unit: "", index: 6, type: "bitLE2-2" },
{ name: "RY4", field_name: "ry4", unit: "", index: 6, type: "bitLE3-3" },
{ name: "RY5", field_name: "ry5", unit: "", index: 6, type: "bitLE4-4" },
{ name: "DI1", field_name: "di1", unit: "", index: 8, type: "bitLE0-0" },
{ name: "DI2", field_name: "di2", unit: "", index: 8, type: "bitLE1-1" },
{ name: "DI3", field_name: "di3", unit: "", index: 8, type: "bitLE2-2" },
{ name: "DI4", field_name: "di4", unit: "", index: 8, type: "bitLE3-3" },
{ name: "DI5", field_name: "di5", unit: "", index: 8, type: "bitLE4-4" }
];
let payParser = new PayloadParser({
device: device,
msg: msg,
frameInfo: frameInfo,
appInfo: appInfo,
});
let tdata = payParser.telemetry();
if ((tdata?.status & 0x02) === 0x02) {
// time out, only update status
const status = tdata.status;
tdata = { ...(device.telemetry_data?.[thingModelId] ?? {}) };
tdata.status = status;
}
return {
telemetry_data: tdata,
server_attrs: null,
shared_attrs: null
};8. Third-Party Platform Data Subscription
8.1 MQTT Topic
A third-party platform can subscribe to ThinkLink uplink data through the following topic:
/v32/{Organization Account}/tkl/up/telemetry/{eui}Where:
| Parameter | Description |
|---|---|
| Organization Account | ThinkLink organization account |
| eui | LoRaWAN device EUI |
8.2 Example Uplink Data
{
"eui": "6353012af10a9331",
"active_time": "2026-02-05T08:35:48.000Z",
"thingModelId": "88304368717139973",
"thingModelIdName": "swm_22108",
"telemetry_data": {
"snr": 13.5,
"rssi": -51,
"battery": 3.37,
"ry_status_hex": "001F",
"di_status_hex": "000B",
"ry1": 1,
"ry2": 1,
"ry3": 1,
"ry4": 1,
"ry5": 1,
"di1": 1,
"di2": 1,
"di3": 0,
"di4": 1,
"di5": 0,
"status": 0
}
}Field description:
| Field | Description |
|---|---|
ry_status_hex | RY status 2-byte HEX string |
di_status_hex | DI status 2-byte HEX string |
ry1–ry5 | Relay channel 1–5 status |
di1–di5 | DI input channel 1–5 status |
battery | Battery or power status field |
rssi | LoRaWAN RSSI |
snr | LoRaWAN SNR |
status | Data status |
9. RPC
This solution supports the following RPC functions:
- Parameter configuration
- Parameter reading
- Relay control
9.1 RPC Names
| RPC Function | RPC Name |
|---|---|
| Parameter configuration | swm_set_22108 |
| Parameter reading | swm_get_22108 |
| Relay control | swm_action_22108 |
9.2 Parameter Definition
Parameter configuration definition:
let paraDef = {
app_70: {
name: "period_up",
field_name: "period_up",
unit: "s",
type: "uint32LE"
},
app_74: {
name: "period_read",
field_name: "period_read",
unit: "s",
type: "uint32LE"
},
app_150: {
name: "addr_modbus",
field_name: "addr_modbus",
unit: "",
type: "uint8"
}
};Parameter table:
| Parameter | field_name | Type | Unit | Description |
|---|---|---|---|---|
app_70 | period_up | uint32LE | s | Upload period |
app_74 | period_read | uint32LE | s | Acquisition period |
app_150 | addr_modbus | uint8 | - | SWMx Modbus address |
Relay control parameters — each relay channel has its own boolean parameter; any one or several channels can be controlled in a single call. When the form opens, each field is auto-populated from the device shared attributes (ry1~ry5) with the last commanded state.
| Parameter | Type | Description |
|---|---|---|
switch_ry1 | boolean | Relay channel 1: true close / turn on, false open / turn off |
switch_ry2 | boolean | Relay channel 2 |
switch_ry3 | boolean | Relay channel 3 |
switch_ry4 | boolean | Relay channel 4 |
switch_ry5 | boolean | Relay channel 5 |
Only the fields actually supplied are sent (one Modbus FC05 frame each, coil address = channel number − 1); unsupplied channels are left untouched. On success, each channel's state is written back to both telemetry
ryNand shared attributeryN.
Example (close channel 1, open channel 3):
{
"switch_ry1": true,
"switch_ry3": false
}9.3 RPC Code
9.3.1 Parameter Configuration RPC
let classMode = (device && device.shared_attrs && device.shared_attrs.class_mode) || "ClassA";
let intervalms = classMode === "ClassA" ? 0 : 2000;
const rpcName = "swm_set_22108";
let paraDef = {
app_70: { name: "period_up", field_name: "period_up", unit: "s", type: "uint32LE" },
app_74: { name: "period_read", field_name: "period_read", unit: "s", type: "uint32LE" },
app_150: { name: "addr_modbus", field_name: "addr_modbus", unit: "", type: "uint8" }
};
let frames = RPCHelper.buildFrame({
paraDef: paraDef,
params: params
});
let redoBuffer = RPCHelper.redo();
let dnBuffer = Buffer.alloc(frames.writeBuffer.length + frames.readBuffer.length);
frames.writeBuffer.copy(dnBuffer, 0);
frames.readBuffer.copy(dnBuffer, frames.writeBuffer.length);
logger.info("set para");
let msgQue = Utils.makeParaSetMSG({
device: device,
classMode: classMode,
rpcName: rpcName,
params: params,
paraDownBuffer: dnBuffer,
extraAppBuffer: redoBuffer
});
if (msgQue.length == 0) return null;
return msgQue;9.3.2 Parameter Reading RPC
let classMode = (device && device.shared_attrs && device.shared_attrs.class_mode) || "ClassA";
let sleepMs = classMode === "ClassA" ? 500 : 5000;
let paraDef = {
app_70: { name: "period_up", field_name: "period_up", unit: "s", type: "uint32LE" },
app_74: { name: "period_read", field_name: "period_read", unit: "s", type: "uint32LE" },
app_150: { name: "addr_modbus", field_name: "addr_modbus", unit: "", type: "uint8" }
};
let frames = RPCHelper.buildFrame({
paraDef: paraDef,
params: params
});
let msg = RPCHelper.makeMSG({
msgType: Utils.msgType.paras,
device: device,
dnBuffer: frames.readBuffer,
sleepTime: sleepMs,
});
return [msg];9.3.3 Relay Control RPC
The form exposes five boolean parameters switch_ry1~switch_ry5, one per relay channel. The script iterates over the fields supplied in the call, sending one Modbus function-code-05 frame per channel (coil address = channel number − 1); the frames are concatenated into a single downlink. Each frame's coil value is verified against the device echo, and on success every channel's state is written back to both telemetry ryN and shared attribute ryN (used to auto-populate the form next time it is opened).
let classMode = (device?.shared_attrs?.class_mode) || "ClassA";
let addr_modbus = device.shared_attrs?.addr_modbus ?? 1;
let rpcName = "swm_action_22108";
// 5 relays: form fields switch_ry1..switch_ry5, each populated from shared_attrs ry1..ry5.
// A single submit may contain any subset; each present field sends one Modbus FC05 frame (coil address = channel-1).
let frames = [];
let appInfo = [];
let telemetry_data = {};
let shared_attrs = {};
for (let n = 1; n <= 5; n++) {
let key = "switch_ry" + n;
if (params[key] === undefined) continue;
let on = params[key] ? 1 : 0;
let checkVal = params[key] ? 0xFF00 : 0x0000;
frames.push(RPCHelper.modbusAction(addr_modbus, 5, n - 1, checkVal));
let tdataKey = "ry" + n;
// Each echo is an 8-byte FC05 frame: [addr][05][coilH][coilL][valH][valL][crcL][crcH]
// Echoed coil value = uint16BE @ (frameStart+4); frameStart+6 is the CRC, never use it for verification.
appInfo.push({field_name: tdataKey, index: appInfo.length * 8 + 4, type: "uint16BE", val: checkVal});
telemetry_data[tdataKey] = on;
shared_attrs[tdataKey] = on;
}
if (frames.length === 0) return null;
let dnBuffer = Buffer.alloc(frames.length * 8);
frames.forEach((f, i) => f.copy(dnBuffer, i * 8));
let checkInfo = {
frameInfo: {port: 51, dataLen: dnBuffer.length},
appInfo: appInfo,
telemetry_data: telemetry_data,
server_attrs: null,
shared_attrs: shared_attrs
};
let msgQue = Utils.makeParaSetMSG({
msgType: Utils.msgType.transParent,
checkInfo: checkInfo,
device: device,
classMode: classMode,
rpcName: rpcName,
params: params,
paraDownBuffer: dnBuffer,
timeout: 3000,
maxRetries: 3
});
if (msgQue.length === 0) return null;
return msgQue;Control logic description:
| Item | Description |
|---|---|
| Modbus Function Code | 05 (one frame per channel) |
| Control Object | Any one or several relay channels (single call) |
| ON Value | 0xFF00 |
| OFF Value | 0x0000 |
| Form Fields | switch_ry1 ~ switch_ry5 (boolean) |
| Coil Address | channel − 1 (switch_ry1→0 … switch_ry5→4) |
| Write-back on Success | telemetry ryN + shared attribute ryN (for form populate) |
10. Template Selection
Search for the following template in the ThinkLink platform:
SWMx-22108Or search by business type:
22108Recommended template information:
| Item | Description |
|---|---|
| Template Name | SWMx-22108 |
| Device Type | DIDO Controller |
| Manufacturer | JSDZ |
| Business Code | 22108 |
| Access Method | KC11 + RS-485 + EdgeBus + LoRaWAN |
| Data Upload | RY status, DI status |
| Control Capability | Relay RPC control supported |
| Parameter Configuration | Upload period, acquisition period, and Modbus address configuration supported |
11. Integration Summary
This solution connects the SWMx DIDO controller to the IoT platform through KC11 + EdgeBus + LoRaWAN + ThinkLink.
Key features:
- KC11 connects to SWMx through RS-485.
- EdgeBus uses Modbus function code 01 to read RY status.
- EdgeBus uses Modbus function code 02 to read DI status.
- Function codes 01 / 02 read data by bit, and EBHelper already handles the differences from function codes 03 / 04 at the lower layer.
- RY and DI statuses are uploaded as 2-byte HEX strings.
- The platform parses RY1–RY5 and DI1–DI5 status at the same time.
- RPC is supported for modifying the upload period, acquisition period, and Modbus address.
- RPC is supported for controlling any one or several relay channels (switch_ry1~switch_ry5) through Modbus function code 05, with state written back to shared attributes for form populate.
This solution is suitable for quickly upgrading traditional RS-485 DIDO controllers into LoRaWAN wireless devices and connecting them to ThinkLink for remote monitoring, remote control, and third-party platform data subscription.