Skip to content

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 (version 1.02.011).


1. Supported Protocols

protocol field values (case-sensitive):

ProtocolUse caseSub-device address length
modbusStandard RTU Modbus1 byte (default)
DLT64507DL/T 645-2007 electricity meterForced to 6 bytes (addrSize<6 auto-coerced to 6)
CJ188CJ/T 188 water / gas / heat meterForced to 7 bytes (auto-coerced to 7)
anyCustom protocol (manual cmd / ack)Uses configured addrSize
modbusBitCovDeprecated — 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.


Every UserConfUPItem generates a txBuffer with this layout:

FieldOffsetSizeDescription
version01 BProtocol version; default 0x83.
dataType11 BData-type encoding; auto-incremented from 0x31 across compile run if not set.
covStatusindexCovStatus (default 2)1 BCOV status bits, OR-accumulated by each ValItem.
statusindexStatus (default 3)1 BQuery-event status from EBModel.APP_STATUS[2].
batteryindexBattery (default 4)1 BBattery level from EBModel.DEVICE_STATUS[3].
addr[]indexAddr (default 5)addrSize BSub-device address; length overridden by protocol (§1).
appData[]indexAddr + addrSizegrown by quInfoApplication data; parsed by the Thing Model.

indexApp is automatically indexAddr + addrSizedo not set it manually.


FieldTypeDefaultDescription
nameStringevent_<i>Uplink event name (used in logs).
portNumber22LoRaWAN uplink port.
versionString / Number0x83Protocol version byte.
dataTypeString / Numberauto (0x31+)Data-type encoding. Each UserConfUPItem in a compile run gets the next value.
indexCovStatusNumber2COV status byte offset.
indexStatusNumber3Status byte offset.
indexBatteryNumber4Battery byte offset.
indexAddrNumber5Address field starting offset.
addrSizeNumber1 (protocol-overridden)Address length in bytes.
upPeriodStringPeriod literal: "900s", "15m", "1h", "1d", "1y".
upPeriodIndexNumberAPP-segment address for a 4-byte LE period register. -1 auto-allocates from ConfigPeriod.periodIndex (starts at 70).
quInfoArrayList of query event configs; see §4.

Period priority (ConfigPeriod logic)

  1. If upPeriodIndex is set (including -1 for auto-alloc) → index mode: period lives in the APP segment and can be tuned at runtime via RPC.
  2. Else if upPeriod is "trigger" → uses PERIOD_DUMB (≈10 years): never auto-uplinks; driven entirely by ActAfterCvt triggers.
  3. Else if upPeriod is set → parsed by Utils.parseSeconds() (units: s m h d y).
  4. Neither set → auto-allocates an APP slot; value falls back to PERIOD_DUMB.

4. quInfo Item (UserConfQueryItem)

FieldTypeDefaultDescription
nameString<protocol>_<idx>Query event name.
protocolStringmodbusSee §1.
preambleNumber5Leading 0xFE bytes for CJ188 / DLT64507 frames.
isLastBooleanautoWhen 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).
periodStringSame semantics as upPeriod but for this query event.
periodIndexNumberSame semantics as upPeriodIndex but for this query event.
codeStringModbus function code (hex). Defaults to 0x03; if blank, taken from cmd[1].
addrStringSub-device address (hex string, e.g. "0x02").
cmdStringautoCustom query frame (hex). If set, fully overrides the auto-generated frame (but addr and code still overwrite bytes 0–1).
ackStringCustom response template (hex). Providing this enables "freeValue" mode — no automatic Modbus register layout.
payIndexNumberprotocol defaultData start offset in ACK frame. Modbus: 3, CJ188: 0, DLT64507: 0.
ackAddrIndexNumberprotocol defaultAddress offset inside ACK. Modbus: 0, CJ188: 2, DLT64507: 1.
indexAPP / indexCMD / copySizeNumberApp-parameter injection; see §5.
listTagArrayPer-byte ACK validation rules; see §4.2.
listValArrayrequiredData 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):

text
[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.

typescript
// 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 indexCMD offsets in ConfigPreCopy.
  • Sets addAckCheckRule for 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.

FieldDescription
indexAPPAPP-segment start offset. -1 auto-allocates from ConfigPreCopy.appIndex (starts at 150), advancing by copySize.
indexCMDDestination offset inside cmd.
copySizeNumber of bytes to copy.

Example — inject a 1-byte Modbus address stored at APP[150] into cmd[0]:

typescript
{
    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:

FieldTypeDescription
start / endStringClosed interval. For Modbus: register addresses. For other protocols: byte offsets. Hex strings supported (e.g. "0x0102").
covTypeStringCOV data type (§6.1). Omitting or setting "INVALID" disables COV — bytes are copied raw.
covAppIndexNumberCOV threshold offset in APP segment. -1 auto-allocates from ValItem.covIndex (starts at 110), advancing by 4.

6.1 Supported covType values

TypeDescription
Uint8 / Int88-bit unsigned / signed integer
Uint16BE / Uint16LE / Int16BE / Int16LE16-bit integer
Uint32BE / Uint32LE / Int32BE / Int32LE32-bit integer
FloatBE / FloatLE32-bit float
FloatCDAB / IntCDAB / UintCDABCDAB byte-order 32-bit value
BCDBCD-encoded value
HEXPer-byte compare; triggers COV if any byte changed
INVALIDDisables COV; raw byte copy only

6.2 COV trigger logic

  • INVALID (or omitted) → bytes in [start, end] are copied raw to txBuffer.
  • HEX → bytes compared against previous snapshot in SENSOR_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 at covAppIndex.

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:

RangePurposeAuto-startStep
70 – 109Period parameters (4 B each)ConfigPeriod.periodIndex = 70+4
110 – 149COV threshold cache (4 B each)ValItem.covIndex = 110+4
150 – 199Dynamic parameter source (ConfigPreCopy)ConfigPreCopy.appIndex = 150+copySize

Do not hard-code offsets that overlap these ranges. If you set an explicit upPeriodIndex, covAppIndex, or indexAPP value, EBHelper uses it as-is and does not advance the counter for that slot.


8. Utils Constants

ConstantValueMeaning
Utils.INVALID_NUM-1000000000Numeric sentinel — means "not configured / auto-allocate".
Utils.INVALID_STR"INVALID"String sentinel.
Utils.PERIOD_DEFAULT90015 minutes in seconds.
Utils.PERIOD_DUMB86400 × 3650≈ 10 years; effectively "never auto-uplink".
Utils.PERIOD_TRIGGER"trigger"String literal for upPeriod; maps to PERIOD_DUMB.
Utils.PERIOD_MAX86400 × 3651 year; threshold for auto-setting isLast on the last quInfo entry.

9. Exported Members

typescript
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

typescript
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);
typescript
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

typescript
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: -1 lets 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 dataType per uplink config to make Thing Model parsing deterministic across firmware versions.