EBHelper
EBHelper is a "configuration-as-code" toolkit for EB development. Instead of manually constructing QueryEvent / LoraUpEvent instances and writing dozens of pushEBData / CRC rules by hand, you describe your device layout in a single UserConfUPItem JSON config and call EventInfoItem to do the rest.
Why use EBHelper vs. raw EBSDK?
- Auto-generates query frames for Modbus, DL/T 645-2007, and CJ/T 188 protocols.
- Auto-calculates CRC rules (CRC16 for Modbus, SUM for meter protocols).
- Auto-allocates APP-segment slots for periods, COV thresholds, and dynamic parameters.
- Produces a standard uplink frame layout (version / dataType / covStatus / status / battery / addr / appData) consistent across all device types.
This document mirrors
source/EBSDK/EBCompiler/plugins/EBHelper.ts(version1.02.011).
1. Supported Protocols
protocol field values (case-sensitive):
| Protocol | Use case | Sub-device address length |
|---|---|---|
modbus | Standard RTU Modbus | 1 byte (default) |
DLT64507 | DL/T 645-2007 electricity meter | Forced to 6 bytes (addrSize<6 auto-coerced to 6) |
CJ188 | CJ/T 188 water / gas / heat meter | Forced to 7 bytes (auto-coerced to 7) |
any | Custom protocol (manual cmd / ack) | Uses configured addrSize |
modbusBitCov | Deprecated — do not use in new projects | — |
Protocol coercion happens in EventInfoItem.protocolBuild: if addrSize is smaller than the protocol minimum it is silently forced up.
2. Standard Uplink Frame Layout
Every UserConfUPItem generates a txBuffer with this layout:
| Field | Offset | Size | Description |
|---|---|---|---|
version | 0 | 1 B | Protocol version; default 0x83. |
dataType | 1 | 1 B | Data-type encoding; auto-incremented from 0x31 across compile run if not set. |
covStatus | indexCovStatus (default 2) | 1 B | COV status bits, OR-accumulated by each ValItem. |
status | indexStatus (default 3) | 1 B | Query-event status from EBModel.APP_STATUS[2]. |
battery | indexBattery (default 4) | 1 B | Battery level from EBModel.DEVICE_STATUS[3]. |
addr[] | indexAddr (default 5) | addrSize B | Sub-device address; length overridden by protocol (§1). |
appData[] | indexAddr + addrSize | grown by quInfo | Application data; parsed by the Thing Model. |
indexApp is automatically indexAddr + addrSize — do not set it manually.
3. UserConfUPItem: Uplink Event Configuration
| Field | Type | Default | Description |
|---|---|---|---|
name | String | event_<i> | Uplink event name (used in logs). |
port | Number | 22 | LoRaWAN uplink port. |
version | String / Number | 0x83 | Protocol version byte. |
dataType | String / Number | auto (0x31+) | Data-type encoding. Each UserConfUPItem in a compile run gets the next value. |
indexCovStatus | Number | 2 | COV status byte offset. |
indexStatus | Number | 3 | Status byte offset. |
indexBattery | Number | 4 | Battery byte offset. |
indexAddr | Number | 5 | Address field starting offset. |
addrSize | Number | 1 (protocol-overridden) | Address length in bytes. |
upPeriod | String | — | Period literal: "900s", "15m", "1h", "1d", "1y". |
upPeriodIndex | Number | — | APP-segment address for a 4-byte LE period register. -1 auto-allocates from ConfigPeriod.periodIndex (starts at 70). |
quInfo | Array | — | List of query event configs; see §4. |
Period priority (ConfigPeriod logic)
- If
upPeriodIndexis set (including-1for auto-alloc) → index mode: period lives in the APP segment and can be tuned at runtime via RPC. - Else if
upPeriodis"trigger"→ usesPERIOD_DUMB(≈10 years): never auto-uplinks; driven entirely byActAfterCvttriggers. - Else if
upPeriodis set → parsed byUtils.parseSeconds()(units:smhdy). - Neither set → auto-allocates an APP slot; value falls back to
PERIOD_DUMB.
4. quInfo Item (UserConfQueryItem)
| Field | Type | Default | Description |
|---|---|---|---|
name | String | <protocol>_<idx> | Query event name. |
protocol | String | modbus | See §1. |
preamble | Number | 5 | Leading 0xFE bytes for CJ188 / DLT64507 frames. |
isLast | Boolean | auto | When true, uplink fires immediately after this query. Auto-set to true on the last quInfo entry when upPeriodIndex is in use and period ≥ PERIOD_MAX (1 year). |
period | String | — | Same semantics as upPeriod but for this query event. |
periodIndex | Number | — | Same semantics as upPeriodIndex but for this query event. |
code | String | — | Modbus function code (hex). Defaults to 0x03; if blank, taken from cmd[1]. |
addr | String | — | Sub-device address (hex string, e.g. "0x02"). |
cmd | String | auto | Custom query frame (hex). If set, fully overrides the auto-generated frame (but addr and code still overwrite bytes 0–1). |
ack | String | — | Custom response template (hex). Providing this enables "freeValue" mode — no automatic Modbus register layout. |
payIndex | Number | protocol default | Data start offset in ACK frame. Modbus: 3, CJ188: 0, DLT64507: 0. |
ackAddrIndex | Number | protocol default | Address offset inside ACK. Modbus: 0, CJ188: 2, DLT64507: 1. |
indexAPP / indexCMD / copySize | Number | — | App-parameter injection; see §5. |
listTag | Array | — | Per-byte ACK validation rules; see §4.2. |
listVal | Array | required | Data extraction rules; see §6. |
4.1 Modbus auto-generated command
When cmd is not set, QuItemModBus builds the command from addr, code, and the register range auto-detected from listVal (via QuFrameInfo):
[addr(1B)] [code(1B)] [start_reg_hi] [start_reg_lo] [reg_count_hi] [reg_count_lo] [CRC_lo] [CRC_hi]QuFrameInfo finds the smallest contiguous register range that covers all listVal entries — so you don't need to compute the register count yourself.
4.2 listTag — fixed-byte ACK validation
Each entry { index, val } asserts that ack[index] === val. Use this to verify protocol start/end bytes or address echoes.
// Assert first byte is 0x68 and last byte is 0x16 (DL/T 645 frame markers)
listTag: [
{ index: 0, val: "0x68" },
{ index: -1, val: "0x16" } // negative index = from end
]4.3 CJ188 / DLT64507 frame structure
Both protocols use a preamble of repeated 0xFE bytes followed by the protocol frame. The preamble field (default 5) sets how many 0xFE bytes precede the frame. EBHelper automatically:
- Adds the preamble to
indexCMDoffsets inConfigPreCopy. - Sets
addAckCheckRulefor protocol start/end markers (0x68/0x16). - Configures SUM checksum over the frame body.
5. ConfigPreCopy: Inject APP Parameters into the Command Frame
Use this to dynamically fill a sub-device address (or any APP-segment value) into the outgoing query frame at compile time. This is how multi-device mode works: different addresses stored in APP are copied into the command before each query.
| Field | Description |
|---|---|
indexAPP | APP-segment start offset. -1 auto-allocates from ConfigPreCopy.appIndex (starts at 150), advancing by copySize. |
indexCMD | Destination offset inside cmd. |
copySize | Number of bytes to copy. |
Example — inject a 1-byte Modbus address stored at APP[150] into cmd[0]:
{
protocol: "modbus",
addr: "0x01",
indexAPP: 150,
indexCMD: 0,
copySize: 1,
listVal: [...]
}If indexCMD or copySize equals Utils.INVALID_NUM, the whole pre-copy is disabled.
6. ValItem: Data Extraction & COV
Each listVal entry:
| Field | Type | Description |
|---|---|---|
start / end | String | Closed interval. For Modbus: register addresses. For other protocols: byte offsets. Hex strings supported (e.g. "0x0102"). |
covType | String | COV data type (§6.1). Omitting or setting "INVALID" disables COV — bytes are copied raw. |
covAppIndex | Number | COV threshold offset in APP segment. -1 auto-allocates from ValItem.covIndex (starts at 110), advancing by 4. |
6.1 Supported covType values
| Type | Description |
|---|---|
Uint8 / Int8 | 8-bit unsigned / signed integer |
Uint16BE / Uint16LE / Int16BE / Int16LE | 16-bit integer |
Uint32BE / Uint32LE / Int32BE / Int32LE | 32-bit integer |
FloatBE / FloatLE | 32-bit float |
FloatCDAB / IntCDAB / UintCDAB | CDAB byte-order 32-bit value |
BCD | BCD-encoded value |
HEX | Per-byte compare; triggers COV if any byte changed |
INVALID | Disables COV; raw byte copy only |
6.2 COV trigger logic
INVALID(or omitted) → bytes in[start, end]are copied raw totxBuffer.HEX→ bytes compared against previous snapshot inSENSOR_DATA; on any mismatch,txBuffer[indexCovStatus]bit is set and snapshot is updated after uplink.- Numeric types →
quEvent.setupCov(...)is called with the absolute delta threshold from APP atcovAppIndex.
6.3 Modbus register range mapping
QuFrameInfo computes the smallest covering range from all listVal.start / end values. Each register maps to 2 ACK bytes starting at payIndex + (register − rangeStart) × 2. For function codes 0x01 / 0x02 (bit reads), bits are coalesced into ceil(bitCount/8) bytes.
7. APP-Segment Address Plan
EBHelper auto-allocates three non-overlapping APP ranges using static class variables that accumulate across all EventInfoItem instances in one compile run:
| Range | Purpose | Auto-start | Step |
|---|---|---|---|
70 – 109 | Period parameters (4 B each) | ConfigPeriod.periodIndex = 70 | +4 |
110 – 149 | COV threshold cache (4 B each) | ValItem.covIndex = 110 | +4 |
150 – 199 | Dynamic parameter source (ConfigPreCopy) | ConfigPreCopy.appIndex = 150 | +copySize |
Do not hard-code offsets that overlap these ranges. If you set an explicit
upPeriodIndex,covAppIndex, orindexAPPvalue, EBHelper uses it as-is and does not advance the counter for that slot.
8. Utils Constants
| Constant | Value | Meaning |
|---|---|---|
Utils.INVALID_NUM | -1000000000 | Numeric sentinel — means "not configured / auto-allocate". |
Utils.INVALID_STR | "INVALID" | String sentinel. |
Utils.PERIOD_DEFAULT | 900 | 15 minutes in seconds. |
Utils.PERIOD_DUMB | 86400 × 3650 | ≈ 10 years; effectively "never auto-uplink". |
Utils.PERIOD_TRIGGER | "trigger" | String literal for upPeriod; maps to PERIOD_DUMB. |
Utils.PERIOD_MAX | 86400 × 365 | 1 year; threshold for auto-setting isLast on the last quInfo entry. |
9. Exported Members
export {
version, Utils,
UserConfQueryItem, UserConfUPItem, TypeVal, TypeProtocol, TypeQuItem, QuItemMap,
ConfigPreCopy, ConfigPeriod, TagItem, ValItem, EventConfig,
QuFrameInfo, QuItemBase, QuItemModBus, QuItemModBusBitCov, QuItemDLT64507, QuItemCJ188,
EventInfoItem
}In practice you almost always need only UserConfUPItem + EventInfoItem: fill the config, construct the instance, call upEventSetup(), then eventInstall().
10. Complete Examples
10.1 Single Modbus device with COV
import { EBModel } from "@EBSDK/EBCompiler/all_variable";
import { UserConfUPItem, EventInfoItem } from "@EBSDK/EBCompiler/plugins/EBHelper";
import { CheckbitEnum, getOtaConfig } from "@EBSDK/otaConfig";
import { buildOtaFile } from "@EBSDK/run";
const eventInfo: UserConfUPItem[] = [
{
name: "meter1",
dataType: "0x31",
upPeriodIndex: 70, // period stored at APP[70..73]; adjustable via RPC
quInfo: [
{
protocol: "modbus",
addr: "0x01",
code: "0x03",
periodIndex: 74, // query period at APP[74..77]
listVal: [
{ start: "0", end: "1", covType: "Uint32BE", covAppIndex: 110 },
{ start: "2", end: "3", covType: "Uint32BE", covAppIndex: 114 },
{ start: "4", end: "7" } // raw copy, no COV
]
}
]
}
];
let otaConfig = getOtaConfig({
SwVersion: 31, BaudRate: 9600, StopBits: 1, DataBits: 8,
Checkbit: CheckbitEnum.NONE, Battery: true, ConfirmDuty: 60,
BzType: 10101, BzVersion: 1
});
const MODBUS_TT = (ebModel: EBModel) => {
for (const cfg of eventInfo) {
const ev = new EventInfoItem(cfg);
ev.upEventSetup();
ev.eventInstall();
}
return JSON.stringify(ebModel, null, 2);
};
buildOtaFile(__filename, otaConfig, MODBUS_TT);10.2 Two uplink configs in one compile run
const eventInfo: UserConfUPItem[] = [
{
name: "channel_A",
dataType: "0x31",
upPeriodIndex: 70,
quInfo: [
{
protocol: "modbus", addr: "0x01", code: "0x03", periodIndex: 74,
listVal: [{ start: "0", end: "3", covType: "Uint32BE", covAppIndex: 110 }]
}
]
},
{
name: "channel_B",
dataType: "0x32",
upPeriodIndex: 78, // next 4-byte slot after channel_A's 70–77
quInfo: [
{
protocol: "modbus", addr: "0x02", code: "0x03", periodIndex: 82,
listVal: [{ start: "0", end: "1", covType: "Uint16BE", covAppIndex: 114 }]
}
]
}
];
// Static counters advance automatically; no manual offset management needed.10.3 DL/T 645-2007 meter
const eventInfo: UserConfUPItem[] = [
{
name: "elecMeter",
dataType: "0x41",
upPeriodIndex: 70,
quInfo: [
{
protocol: "DLT64507",
addr: "010203040506", // 6-byte meter address (hex)
preamble: 4, // 4 × 0xFE preamble bytes
listVal: [
{ start: "0", end: "3", covType: "BCD" }
]
}
]
}
];
// addrSize is auto-forced to 6; SUM checksum and frame tags are auto-configured.11. Best Practices
- Minimize query splits: a single ACK can hold ~250 bytes — pull contiguous registers in one query rather than multiple separate queries.
- Split only when the gap is large: splitting is worth it only when invalid/unused data exceeds ~50 bytes.
- Prefer index-based config:
upPeriodIndex: -1,periodIndex: -1,covAppIndex: -1lets you adjust at runtime via RPC — no recompile needed. - Don't hard-code offsets inside 70/110/150 ranges unless you explicitly want to fix a value and prevent auto-allocation from using that slot.
- Set explicit
dataTypeper uplink config to make Thing Model parsing deterministic across firmware versions.