Files
cimtechniques-service-suite/docs/DA-07 SERVICE-TOOL-ICD.md

48 KiB
Raw Permalink Blame History

DA-07 Service Tool — Interface Control Document (ICD)

Scope: DA-07 / DA-07B / DA-07C firmware family (build symbols MODEL_DA07, +MODEL_DA07B, +MODEL_DA07C). Status: descriptive, not prescriptive — the DA-07 firmware is EOL; this document records what the shipped firmware does so a replacement service-tool application can be built against it without access to the firmware source. Firmware is authoritative.

Primary sources (firmware): service.c (protocol + state machine), usart.c (transport, framing, serializers), utility.c (hex/time helpers), config.h/io.h/modbus.h/protocol.h (structures & enums), crc.c (Modbus CRC16). Line citations are file:line against the repository at D:\Work\DA-07 source code.

Reader's note on byte fidelity. The DA-07 build compiles with -fpack-struct (PackStructureMembers=True in DA-PQ-Xmega.cproj), so all structs are packed with no padding; a struct's wire image is the simple concatenation of its members. char is unsigned (-funsigned-char). The MCU is little-endian. float is 32-bit IEEE-754, little-endian. These facts are assumed throughout.


1. Transport & PHY

Property Value Evidence
Physical port Dedicated service port on a 3.5 mm jack; USB-A↔3.5 mm cable to the tech's PC hardware; user-confirmed
MCU UART USART2 = USARTE0 (Port E0) usart.c:16, USART2_Init usart.c:435
Baud rate 9600 main.c:205 (USART2_Init(9600, …))
Data bits 8 (USART_DATA_8BITS) main.c:205
Parity None (USART_PARITY_NONE) main.c:205
Stop bits 1 (USART_STOP_1BIT) main.c:205
Flow control None usart.c (no RTS/CTS on USARTE0)
Direction Full-duplex async, idle-high (standard UART)

So: 9600 8-N-1, no flow control. This port is electrically and logically separate from the RS-485 Modbus bus to the sensor pods (USART0 = USARTF0, Port F). The service tool never sees the RS-485 wires directly; instead the firmware mirrors Modbus frames onto the service port on request (see §10, I/O capture).

Other UARTs (context, not used by the service tool): USART1=USARTD0 = NetBurner SBL2e Ethernet module — this is the DA-07's path to the CimScan server (115200 8-N-1, usart.c:385-413, netburner.c). USART3=USARTC0 is the DNT90 radio port but is only initialized on DA-33 (USART3_Init is #ifdef MODEL_DA33, usart.c:480-513); on DA-07 the radio is absent. USART4=USARTD1 secondary LAN. (usart.c:14-18.)

Scope note: the DA-07 reaches the server over Ethernet (NetBurner), not the DNT90 radio. The radio/wireless.c is entirely DA-33. The service tool itself only ever uses the local service UART (USART2); there is no remote/server-routed service-tool path on DA-07 (SERVICE_TOOL_CMD_ID(36) has no ProcessMsg case and fails the inbound length gate, so it is unhandled and NAKed).

Special option 0x42 (inert on DA-07 — do not use): setting special option 0x42 (command ~O42) makes ServiceTick early-return (service.c:2430, not model-guarded) and the service-port RX ISR try to forward bytes to the DNT radio UART (usart.c:258-263). On DA-07 that radio UART (USARTC0) is never initialized, and the 115200 re-init at service.c:2329 is #ifdef MODEL_DA33, so 0x42 simply freezes normal service communication while doing nothing useful. It is a DA-33 diagnostic. A DA-07 tool must not send it.


2. Frame format & integrity

The service protocol is a line-oriented ASCII-hex text protocol, not raw binary. Every frame in both directions has the form:

~  <type>  <payload …>  <CC>  <CR>
│  │       │            │     └─ 0x0D, terminator
│  │       │            └─────── 2 ASCII-hex chars = 8-bit checksum (see below)
│  │       └──────────────────── zero or more fields, ASCII (mostly hex digits)
│  └──────────────────────────── 1 ASCII char = message/command type ('A'..'Z')
└─────────────────────────────── 0x7E '~' = start-of-frame
  • Start of frame: ~ (0x7E). The receiver resets its buffer on any ~ (firmware RX: usart.c:267-268).
  • Type byte: a single ASCII letter selecting the message (§7) or command (§8).
  • End of frame: CR (0x0D). Firmware marks a frame ready only when CR arrives and the buffer started with ~ (usart.c:272-273).
  • Max frame length: firmware service RX buffer is SV_SIZE bytes (serial.h/usart.c); transmit buffer svTxBuf likewise. Keep frames within the buffer (the firmware clamps/truncates rather than overflowing — usart.c:277-278, usart.c:925).

2.1 Checksum

An 8-bit additive checksum, modulo 256, emitted as 2 uppercase ASCII-hex digits immediately before the CR.

  • Outbound (firmware→tool), SendSvcCheck usart.c:1040-1057: sum every non-zero byte already in the frame buffer (this includes the leading ~ and all ASCII-hex payload bytes), take sum & 0xFF, append as 2 hex chars, then append CR.
  • Inbound (tool→firmware), SvcChecksumOK service.c:1917-1927: sum bytes svRxBuf[0 .. svRxTail) (i.e. from ~ up to but not including the checksum), take & 0xFF, compare to the 2 hex chars at the checksum position.

The tool must compute its outgoing checksum exactly as the firmware verifies it: sum of all bytes from ~ through the last payload byte, mod 256, 2 hex digits. (The firmware's own outgoing routine skips embedded zero bytes, but a well-formed frame has no embedded zeros before the checksum, so the two definitions agree in practice. See BUG-ICD-07.)

2.2 Escaping

The only reserved byte is ~ (0x7E). When building a frame, any payload byte equal to ~ is replaced by a space ' ' (0x20): outbound AddToSvBuf usart.c:920-923 and SendSvcChar usart.c:970-973. Because the payload is otherwise ASCII-hex (digits 0-9A-F) and tab delimiters, ~ can only occur inside free-text string fields (e.g. a station name); such a ~ is lossily converted to a space. The tool should likewise avoid sending literal ~ inside string fields.

2.3 Field delimiter

A TAB (0x09) separates the human-readable label from the value in station-setting messages (the label text is stored with a trailing \t, service.c:396-435). Most other messages are fixed-position concatenated hex with no delimiters.


3. Data-type encodings

All multi-byte values are serialized to ASCII-hex by PrintHex (utility.c:65-76), which walks the value in memory order (low address first). Because the MCU is little-endian, multi-byte integers and floats are emitted least-significant-byte first, two hex chars per byte. Example: uint16_t 0x1234 → memory 34 12 → text "3412".

Contrast: the RS-485/Modbus path uses PrintHexRev (big-endian hex). The service path uses PrintHex (little-endian hex) for SendSvcInt/Long/Float (usart.c:994-1016). Do not assume Modbus byte order on the service port. Exception: PrintTime (utility.c:97-100) emits a uint32 big-endian via sprintf("%04X%04X", hi, lo) — but the service H/F/G messages send time via SendSvcLong (little-endian), so time on the service wire is little-endian unless a specific message says otherwise. Verify per message in §7. (See BUG-ICD-03.)

The firmware exposes these data-type codes to the tool via the type nibble in station-setting label strings (the B/C record's 4th char, scheme documented at service.c:38-52). Note this is a different numbering from the internal DTYPE_* enum in service.h:5-14 (which is 0=UBYTE,1=SBYTE,2=UINT,…,9=HEX and is used only by the #ifdef DEBUGGING ~I path). The station-setting type codes are:

Code Meaning Wire encoding
0 unsigned byte 2 hex chars
1 unsigned int (16-bit) 4 hex chars, little-endian
2 signed int (16-bit) 4 hex chars, little-endian, two's-complement
3 unsigned long (32-bit) 8 hex chars, little-endian
4 signed long (32-bit) 8 hex chars, little-endian, two's-complement
5 float (32-bit IEEE-754) 8 hex chars, little-endian
6 char string raw ASCII bytes (2 hex chars each when sent via SendSvcHex; or literal chars via SendSvcString)
7 IP address 4 bytes (8 hex chars), in xpLocalIp order
8 MAC address 6 bytes (12 hex chars)
9 version high.low bytes
A 4-byte serial number (MAC-like) / rssi — see note
B baud rate 16-bit, little-endian
C rssi (from radio) byte

Doc inconsistency in firmware: the two comment tables in service.c disagree on codes A/B/C (one says A=serial, B=baud, C=rssi service.c:50-52; the other says A=rssi service.c:386-387). Treat the first table (lines 38-52) as canonical for station settings; it matches the actual PrintHex field sizes used in SendStationSettings. (BUG-ICD-04.)

Important on TIME: timestamps are a 32-bit time_t-style count of seconds since 1970-01-01, sent little-endian on the service wire (SendSvcLong). Treat it as LOCAL time, not UTC. The firmware's clock-validity constant is JAN012014 = 1388552400 (config.h:26), which is midnight 2014-01-01 EST (UTC would be 1388534400 — a 5-hour/18000-second difference), so the device's clock is maintained in local time, and the VB6 tool renders these timestamps directly as local HH:MM:SS with no UTC offset applied. The Python tool should do the same: display the epoch as-is in the unit's local zone; do not convert as UTC or you'll be off by the local offset. (Caveat: the exact zone is whatever the server/installer set the clock to; EST is what the validity constant assumes.) Values below JAN012014 are an unsynced clock = seconds-since-boot, not a real date.

Strings / names: NAME_SIZE = 16 (config.h:90). Station and channel names are fixed 16-byte fields, space-padded, not necessarily null-terminated on the wire. Serial numbers are SERIAL_SIZE = 8 bytes (config.h:91).

3.1 Inbound (tool→firmware) value encoding — CRITICAL ASYMMETRY

The firmware does NOT echo-decode the same way it encodes. When the firmware sends a setting it uses little-endian hex (PrintHex); but when the tool writes a setting back, the firmware parses the value field with atoi/atof — i.e. plain DECIMAL ASCII text, not hex. A tool that echoes the received hex back into a write sends the wrong number (atoi("3C00") = 3, not 0x3C = 60). This is the single most important thing to get right in the rewrite, and it is per-field:

Command Value field encoding on write Evidence
~B station setting (most) decimal (atoi/atof) — e.g. comms timeout, ports, baud, activation energy & pressures (floats), modbus timeout service.c:696-697
~B idx 1 station name raw ASCII bytes (memcpy) service.c:724
~B idx 4 baseSN ("High 4 bytes of serial") hex, per byte (AscToByte), big-endian byte order service.c:768-779
~B idx 7/10/11 IP addresses dotted-decimal text ("192.168.2.18", atoi per octet) — NOT 8 hex chars service.c:700-717
~C device setting value decimal (atoi) service.c:1757
~C case 5 device serial[3] hex, per byte (AscToByte) service.c:1807-1809
~D channel setting value (limits 2-5, scale 6, offset 7) decimal float (atof) — NOT hex floats service.c:1575-1576
~E alarm-indicator (index/type/data) hex (AscToByte) both directions — symmetric service.c:1837-1839

So the encoding is direction- and field-dependent: read = hex; write = decimal, except names (ASCII), serials (hex), IPs (dotted-decimal), and the whole ~E alarm-indicator message (hex). (Logged as BUG ST-13.)


4. Session model & state machine

4.1 Handshake (ACK / NAK / IDLE)

The session is a strict ping-pong: the firmware sends exactly one frame, then waits for the tool to respond before sending the next. The tool's responses:

Tool frame Meaning Firmware reaction
~Z1 + CC + CR ACK — got it, send next RefreshServiceTool() advances and sends the next item (service.c:2390-2398)
~Z0 + CC + CR NAK — resend last firmware re-sends the previous frame (SendToSvc(), service.c:2391-2394)

The firmware itself emits:

Firmware frame Meaning
~Z1 ACK of a tool command that was processed OK (SendAcknowledgement(ACK), service.c:2415)
~Z0 NAK — bad start char, too-short frame, bad checksum, or unknown type (service.c:2224,2238,2403,2410)
~Z2 IDLE — "nothing to send right now", emitted about once per second when idle (service.c:2504)

A received frame of any recognized type also counts as an implicit ACK of the firmware's previous message (service.c:164-166).

4.2 Liveness / timeout

  • svcActive is set to 10 whenever a frame arrives from the tool, and decremented once per second when idle (service.c:2441,2489,2501). It is the firmware's "tool is connected" counter.
  • While in I/O-capture mode (svcSendIO), if no traffic is seen for >25 seconds the firmware auto-disables capture (service.c:2458-2461).
  • After 5 consecutive IDLEs with no tool message, the firmware forces svcState = SVC_POLL (steady-state, service.c:2506-2510).

4.3 Refresh sequence (svcState)

On connect (or on a ~A refresh request) the firmware walks an ordered sequence, sending one frame per tool-ACK. States (service.h:34-43, driver RefreshServiceTool service.c:1967-2202):

Order State Frames sent Builder
0 SVC_START one ~A config frame (§7.A0) SendServiceToolConfig service.c:1267
1 SVC_TYPES one ~A device-type frame per supported type SendDeviceTypes service.c:1284
2 SVC_STATION one ~B/~C station-setting frame per setting SendStationSettings service.c:496
3 SVC_INIT per device: one ~D device-settings, then one ~E per channel (and ~W per channel on 07C) service.c:1999-2064
4 SVC_ALARM one ~M alarm-group frame per indicator (×16) SendAlarmIndSettings service.c:1422
5 SVC_AVERAGES ~F average-values frame for each device flagged D_UPDATE_AVG SendDeviceAverages service.c:1093
6 SVC_CURRENT ~G current-values frame for each device flagged D_UPDATE_INPUT SendDeviceInputs service.c:1028
7 SVC_NAMES ~P channel-name/CT-serial frames for devices flagged D_UPDATE_NAMES SendChanName service.c:1205
8 SVC_DEVICE ~D/~E/~W for devices flagged D_UPDATE_DEVICE service.c:2141-2188
9 SVC_POLL one ~H operational-statistics frame per second SendOperationalStatics service.c:1305

The states fall through in the source: once a phase's queue empties it advances and the next phase may begin within the same call. After SVC_POLL the firmware loops back to SVC_AVERAGES (service.c:2199-2200), so the steady state continually re-pushes averages → current → names → device → stats as new data is flagged.

A tool that wants a clean full snapshot sends ~A (refresh), then ACKs each frame with ~Z1 until it observes ~H frames (which signal it has reached steady-state SVC_POLL).


5. Firmware → tool message reference

Frame = ~ + type + fields + CC + CR. Offsets below are within the payload, in decoded bytes (each byte = 2 hex chars on the wire) unless noted. All multi-byte values little-endian (§3).

5.A0 ~A (subtype 0) — Service-tool configuration · SendServiceToolConfig service.c:1267

Sent first in SVC_START. Payload bytes:

# Field Notes
0 00 subtype 0 = "config" (distinguishes from device-type ~A)
1 MODEL 7 for DA-07 (config.h:59)
2 message version always 01
3 MAX_DEVICES 16 for DA-07 (config.h:60)
4 MAX_DEVICE_CHANS 10 (config.h:63)
5 GetMaxDevTypes() number of selectable device types
6 MAX_INDICATORS 16 (io.h:304)
7 MAX_IND_ADDR 8 (io.h:305)

5.A1 ~A (device-type) — Device-type descriptor · SendDeviceTypesSendDeviceInfo service.c:1284

Sent once per type in SVC_TYPES. Payload (confirmed against the legacy VB6 decoder):

Field Encoding Meaning
idx byte device-type index (1-based)
chans byte number of channels this type has
genType nibble generic class: 0 analog, 1 particle-counter, 2 pulse, 3 CS-series, 4 din/dout, 5 din, 6 dout, 7 alarm, 8 UniversalModbus (modbus.c:383-397)
dataDP nibble data type / decimal-places hint
id string + TAB type name (e.g. "PD-17"), TAB-terminated
chanNames pipe-|-delimited string per-channel default names

(genType/dataDP are packed as two nibbles of one byte. SendDeviceInfo lives in modbus.c; the PROGMEM descriptor strings are modbus.c:401-457.)

5.B/C ~B/~C — Station setting · SendStationSettings service.c:496

One frame per station setting. The payload is the PROGMEM label record followed by the value. The label record (service.c:396-435) is: <editable><line><type><text…>\t where

  • editable: 'B' = user may edit, 'C' = display-only.
  • line: 2 hex chars = display row.
  • type: 1 hex char = data type code (§3).
  • text: human label, terminated by TAB (0x09). Then the value is appended as hex per the type. See §6 for the full per-index table.

5.D ~D — Device settings · SendDeviceSettings service.c:1243

~D + device(byte) + D_PARAMS minus the channel array (io.h:179-193). For the DA-07 build the sent bytes are:

Off Size Field Notes
0 1 type device type (modbus.h enum); 0/0xFF = unused
1 1 addr Modbus address (union with setID)
2 1 delay alarm delay (update cycles)
3 1 control device control byte (DA-07 only)
4 3 serial[3] bytes 4-6 of serial no.
7 payload bytes (14 hex chars). The chan[10] array is intentionally excluded
(sizeof(D_PARAMS) - MAX_DEVICE_CHANS, service.c:1251).

5.E ~E — Channel settings · SendChanSettings service.c:1125

~E + device(byte) + deviceChan(byte) + C_PARAMS (io.h:60-73) + optional 8-byte CT serial. For the DA-07 build (USE_SCALE_OFFSET on, NEED_C_PARAMS_DISPLAY_FIELD off) C_PARAMS is 27 bytes:

Off Size Field
0 1 device (0xFF = unused)
1 1 active (b7 active, b6 disabled, b5 no-alarm, b4-0 calc type)
2 16 limits[4] = LO_ALR, LO_WRN, HI_WRN, HI_ALR (each float)
18 4 scale (float)
22 4 offset (float)
26 1 alarm (link to local alarm indicator)
If the channel has a CT serial (gChans[c].serNo <= MAX_SERNO), 8 more bytes of
serial are appended (service.c:1146-1150).

5.W ~W — Universal-Modbus channel settings (07C only) · SendUModbusChanSettings service.c:1165

~W + device(byte) + chan(byte) + UMODBUS_PARAMS (packed, 17 bytes, UniversalModbus.h:39-56). Only compiled for MODEL_DA07C. On plain DA-07/07B this message never appears. Layout:

Off Size Field Meaning
0 1 OperMode how/when to read the measurement
1 2 DataRegNo Modbus reg # of first measurement register (LE)
3 2 TriggerRegNo reg # to test to trigger a read
5 2 TriggerMask AND-mask applied to the trigger register
7 2 StatusRegNo reg # holding status info
9 7 ErrorBitSet1..7 1 byte each — map status-register bits to CIMScan error bits
16 1 Options option flags

The measurement decode type referenced by OperMode/Options uses these codes (UniversalModbus.h:61-72): 0=UBYTE, 1=SBYTE, 2=UINT16, 3=INT16, 4=UINT32, 5=SINT32, 6=FLOAT32, 0xC=UINT32-reversed, 0xD=SINT32-reversed, 0xE=FLOAT32-reversed. Written back via tool command ~D with setting 12 (sub-fields service.c:1667-1719).

5.F ~F — Device average values · SendDeviceAverages service.c:1093

~F + device(byte) + uTime(ulong, update time) + for each active device channel: value(float, the averaged/scaled value sent to the server). Channel count = GetChansByDeviceId(device).

5.G ~G — Device current (instantaneous) values · SendDeviceInputs service.c:1028

~G + device(byte) + iTime(ulong, input time) + for each active device channel: status(byte) + input(float, raw/unscaled). Note: if svcDebugMode==1, the per-channel status byte is replaced by the channel index (service.c:1070-1076) — a debug aid the tool must account for when debug mode is on.

5.H ~H — Operational statistics (steady-state poll) · SendOperationalStatics service.c:1305

Sent once per second in SVC_POLL. Payload:

  1. 15 stat bytes = svcStats[0..14] in the order of service.h:47-63 (§7).
  2. recordCount(uint16, little-endian) = buffered NVM records (CountRecords()).
  3. Now()(ulong) = current time_t.
  4. MAX_DEVICES (16) nibbles = gDevice[i].status low nibble each (SendSvcNibble), device 0..15.
  5. For each active alarm indicator group i: i(byte) + local(nibble) + server(nibble) (service.c:1330-1338).

svcStats[0..7] are zeroed after each send (service.c:1320,1342); the higher-index stats are recomputed live each cycle (service.c:1309-1316).

5.K ~K — Diagnostic timing · SendDiagnostics service.c:1396

~K + podTime(uint16) + serTime(uint16) + svcTime(uint16) — per-subsystem processing time since last query. Sent in response to tool command ~J.

5.M ~M — Alarm-indicator group settings · SendAlarmIndSettings service.c:1422

~M + index(byte) + active(byte) + addr[MAX_IND_ADDR] (8 bytes) — the device addresses of the indicators in the group (A_PARAMS, io.h:313-317).

5.N ~N — Server alarm-update times · SendAlarmUpdateTimes service.c:1448

~N + for each indicator group n with a nonzero update time: n(byte) + uTime(ulong). Sent when gAlarmUpdated is set.

5.P ~P — Channel name / CT serial number · SendChanName service.c:1205

~P + device(byte) + chan(byte) + 8-byte CT serial number. Only sent for channels with a valid serNo.

5.R ~R — Display special message · SendDisplayMessage service.c:1520

~R + msgID(byte, bit-significant). SVC_MSG_REFRESH = 0x01 = "press the refresh button" (service.h:105).

5.Y ~Y — Mirrored Modbus I/O frame · SendAcBufToSvcTool service.c:1000

~Y + direction ('1' = transmit to pods, '0' = received from pods) + the raw Modbus frame bytes from acBuf as hex (acTail bytes). Emitted only while I/O capture is enabled (§10).

5.J ~J — Modbus passthrough response (§9)

5.Z ~Z — ACK/NAK/IDLE (§4.1)

5.I ~I — Debug string — not compiled on DA-07 (#ifdef DEBUGGING, see §10).

5.x — Message types in the firmware header comment that are NOT emitted on DA-07

The service.c:1-305 header documents three additional firmware→tool messages that have no emitter in the compiled tree (verified: no SendSvcChar('L'|'O'|'Q') anywhere). A tool cross-referencing that header comment should treat them as dead:

  • ~L (single-channel average) — superseded by the D_UPDATE_AVG → ~F path that re-sends a device's averages after a scale/offset edit.
  • ~O (RSSI values) — radio signal strength; relevant only to the wireless DA-33.
  • ~Q (rows to hide on the station tab) — no sender exists.

The complete set the DA-07 firmware actually emits is: A, B, C, D, E, F, G, H, J, K, M, N, P, R, W, Y, Z (plus ~I only in a DEBUGGING build, and ~W only on 07C). That is the full §5 catalog.


6. Station-settings table (DA-07)

Index = position in the px[] table (service.c:437-453); sent in SVC_STATION, written back via tool command ~B. For DA-07, DEBUGGING is off so index 29 is the debug-level entry but carries no value bytes (see note). Editable flag and type are from the label record (service.c:396-435); the value source/sink is the firmware field shown.

Idx Label Edit Type Bytes Field (send service.c / recv service.c)
1 Station Name (16 chars) B 6 str 16 gStation.name (507 / 724)
2 Update Interval (sec) B 1 uint 2 gStation.updateInterval (512 / 738)
3 Reporting Interval (# updates) B 0 byte 1 gStation.reportInterval (516 / 753)
4 High 4 bytes of Serial Number B A 4 gStation.baseSN (520 / 768)
5 Comm-loss timeout (sec) B 1 uint 2 gStation.commsTimeout (524 / 783)
6 LAN MAC Address C 8 mac 6 gStation.xpMac (528 / —)
7 Local IP Address B 7 ip 4 gStation.xpLocalIp (532 / 787)
8 Local Port Number B 1 uint 2 gStation.xpLocalPort (536 / 792)
9 Subnet Mask Bits B 0 byte 1 gStation.xpSubnetBits (540 / 797) [host-bit count; bits→mask conversion buggy outside 18 — see BUG-ICD-13]
10 Gateway IP Address B 7 ip 4 gStation.xpGatewayIp (544 / 802)
11 Server's IP Address B 7 ip 4 gStation.xpRemoteIp (548 / 807)
12 Server's Port Number B 1 uint 2 gStation.xpRemotePort (552 / 812)
13 Model Number C 0 byte 2 gInfo.model (556 / —)
14 Firmware Version C 9 ver 2 gInfo.firmware (560 / —)
15 RS-485 Baud Rate B B baud 2 gStation.baudRate (564 / 817, re-inits USART0)
16 Poll Devices (0/1) B 0 byte 1 gStation.enablePolling (568 / 822)
17 Activation Energy (MKT) B 5 float 4 gStation.actEnergy (572 / 826)
18 Update Control (0=none 1=warn 2=alarm) B 0 byte 1 gStation.limitsUsage (576 / 831)
19 Pump Control Address B 0 byte 1 gStation.pumpCtrlAddr (580 / 843)
20 Flatline Detection (scans) B 0 byte 1 gStation.flatlineTOut (584 / 847)
21 Calibration Pressure (DP) B 5 float 4 gStation.calPres (588 / 851)
22 Barometric Pressure (DP & RH) B 5 float 4 gStation.baroPres (592 / 856)
23 Stacklight Style (0-4) B 0 byte 1 gStation.stacklights (596 / 861) [dead — see BUG-ICD-08]
24 Alarm Ind. Operating Mode (0-3) B 0 byte 1 gStation.alarmIndMode (600 / 865)
25 Beeper Operation (0-2) B 0 byte 1 gStation.beeperMode (604 / 869)
26 Buffer Operating Mode (0-3) B 0 byte 1 gStation.bufferMode (608 / 873)
27 NVRam Size (# Records) C 3 ulong 4 gMaxRecords (615 / —)
28 Modbus Timeout (ms) B 1 uint 2 gStation.modbusTimeOut (620 / 881, clamped 1002000)

Count: production DA-07 transmits exactly 28 station settings. px[] has 28 entries — the index-29 DebugLevel record (p29) is #ifdef DEBUGGING, which is off in every shipped DA-07 build, so SVC_STATION iterates 1..28 and never sends a 29th (service.c:424-453, 1988). Indices 29-32 (Samples-per-Update, Reserved, Wireless Network, DebugLevel) exist only on DA-33/DA-09 or DEBUGGING builds. A tool counting station-setting frames should expect 28.

The label line number in the record (e.g. 016, 021) is the legacy display row, not the index; the tool keys on index (position in px[]).


7. The svcStats[15] block (in ~H)

Order is fixed by service.h:47-63; the tool must read them positionally:

# Symbol Meaning
0 O_COUNT outgoing Modbus message count
1 O_RETRIES retries
2 O_VALUES values sent
3 I_COUNT incoming message count
4 I_CHECKSUM messages with checksum errors
5 I_STRUCT messages with structure errors
6 I_TRASH discarded bytes
7 I_CHARS incoming character count
8 P_ACTIVE active pods (gInfo.devices)
9 P_ERROR pods returning errors (gInfo.pdError)
10 P_COMM pods in comm loss (gInfo.pdFail)
11 P_TRANS transaction count
12 S_SENSOR active channels (gInfo.sensors)
13 S_ERROR channels in error
14 S_TIME minutes since last message received

Caution (BUG-ICD-05): the prose table in service.c:72-87 lists these in a different order (it swaps S_TIME/S_SENSOR and omits S_ERROR). The service.h enum order above is authoritative — it is what SendSvcHex(svcStats, MAX_SVC) actually transmits.


8. Enumerations

8.1 Device / pod types (DA-07 set, modbus.h:50-95)

The numeric type values the tool will see in ~D/~A device descriptors. (Values forced to 255 in other models are normal here.)

Val Sym Val Sym Val Sym
0 xNONE 16 xP230 32 xM6015
1 xOLDCS05 17 xP231 (particle ctr) 33 xMiraclean
2 xOLDCS10 18 xPULSE (analog) 34 xKenomax3714
3 xCS22 19 xDOUT (digital out) 35 xKenomax3715
4 xCS24 20 xDIN (digital in) 36 xUniversalModbus
5 xCS41 21 xM6003 (MetOne) 37 xM3413
6 xPD15 22 xCS05 38 xM3415
7 xPD15T 23 xPD90 39 xM3445
8 xPD16 24 xCS10 40 xM3423
9 xPD17 25 xDRAGER 41 xM3425
10 xPD18 26 xCI3100 (Climet) 42 xCS48 (Alphasense)
11 xPD19 27 LightHouse 43 xGateway
12 xPD50 28 sc200 44 xCS31 (DQ node ctrl)
13 xPD51 29 xPD51B 45 xRTA (RTA 460)
14 xPD52 30 xM6005
15 xPD68 (alarm) 31 xM6013

Which of these are selectable in the tool's device-type dropdown is reported at runtime by GetMaxDevTypes()/SendDeviceInfo; do not hard-code the count. The per-type channel count and data type come from SendDeviceInfo (modbus.c).

8.2 Channel status bits (~G status byte, io.h:140-156)

Low nibble = sensor error code (mutually-exclusive small values), high bits = alarm state:

Bit/val Sym Meaning
0x01 C_ERR_UNDER under-range
0x02 C_ERR_OVER over-range
0x03 C_ERR_SENSOR sensor error
0x04 C_ERR_EXCITE RTD excitation / open T-C / flow
0x08 C_ERR_FLAT flat-line
0x10 C_ERR_TRIM offset/trim pending (CT sensors)
0x20 C_ERR_ACKED alarm acknowledged at server
0x40 C_ERR_WARN channel in warning
0x80 C_ERR_ALARM channel in alarm
(C_ERR_MASK=0x07 sensor bits; C_ERR_AMASK=0xC0 alarm bits.)

8.3 Device status nibble (~H, io.h:246-255)

0x01 COM (no response), 0x02 FLOW, 0x03 ERR, 0x04 ACCURACY, 0x08 CAL-overdue, 0x40 WARN, 0x80 ALARM. (~H sends only the low nibble per device.)

8.4 Alarm-indicator state (~H local/server nibbles, io.h:307-311)

0x01 IND_OK, 0x02 IND_WARN, 0x04 IND_ALARM, 0x08 IND_ERROR. MAX_INDICATORS=16 groups, MAX_IND_ADDR=8 devices/group.

8.5 Station-config field encodings

  • stacklights 0-4: 0=normal 4-level, 1=red, 2=red+grn, 3=red+yel+grn, 4=blu+red+yel+grn (config.h:141). Dead — not consumed by firmware (BUG-ICD-08).
  • alarmIndMode 0-3: none / local-only / server-else-local / both (config.h:155-159).
  • beeperMode 0-2: none / single chirp / continuous-until-cleared (config.h:143-147).
  • bufferMode 0-3: normal / 15-min+hourly / measurements-only / stats-only (config.h:149-153).
  • limitsUsage 0-2: none / warn / alarm (auto rate-change trigger).

9. Modbus passthrough (tool ↔ downstream pod)

The tool can inject a raw Modbus transaction onto the RS-485 bus and read the reply, without the firmware interpreting it.

Request — tool sends ~M (ProcIncomingMsg case 'M', service.c:2311-2317):

~ M aa ff rrrr nnnn CC CR
    │  │  │    └ register/quantity count (uint16, MakeUInt — see note)
    │  │  └────── starting register (uint16)
    │  └───────── Modbus function code (byte)
    └──────────── Modbus slave address (byte)

All fields ASCII-hex. The firmware loads testBuffer{addr,func,reg,cnt} and sets testStatus = TEST_OUT (service.h:90-99). The Modbus engine then builds a proper Modbus frame (address, function, register, count, CRC16) and transmits it on RS-485 (ReceiveTestBuffer/SendModbusPoll, modbus.c).

Endianness (RESOLVED — big-endian): reg/cnt are parsed by MakeUInt (utility.c) and are sent big-endian ASCII hex ("0064"→0x0064). Confirmed against the VB6 builder: ~M = M+Hex2(addr)+Hex2(func)+Hex4(register)+ Hex4(value/count), all plain (non-reversed) hex (Test.frm:281-288). This is the one place service-frame hex is big-endian, unlike the little-endian SendSvcInt/SendSvcLong used for normal values — because the register goes straight onto the big-endian Modbus wire.

Downstream Modbus framing (what actually goes on RS-485): addr · func · data · CRC16 (2 bytes, LSB first). Supported function codes (modbus.h:7-15): 1 read-coil, 2 read-input, 3 read-reg, 4 read-AOUT, 5 write-coil, 6 write-reg, 15 write-mult-coils, 16 write-mult-regs, 24 read-FIFO.

Modbus CRC16: standard Modbus CRC-16 — polynomial 0xA001 (reflected 0x8005), init 0xFFFF, refin/refout = true, no final XOR, appended little-endian (low byte first). The firmware has two identical-result implementations: the bitwise GetCRC (modbus.c:6003) actually computes the transmitted CRC, and a table-driven GetCRC16 (crc.c:48-58) is used to check received frames. The transmit path writes acBuf[6]=low, acBuf[7]=high (modbus.c:580-581); the read path confirms low-byte- first (crc.c:149).

Response — firmware sends ~J (SendTestBuffer, service.c:1875-1903):

  • On success: ~J + addr(byte) + func(byte) + byteCount(byte) + byteCount data bytes.
  • On error/timeout (testStatus == TEST_ERR): ~J + 00 (single zero byte).

10. Debug mode & live I/O capture

10.1 I/O capture (Modbus mirroring) — available on DA-07

  • The tool enables it with command ~Y1, disables with ~Y0 (sets svcSendIO, service.c:2382-2388).
  • While enabled, every Modbus frame the firmware sends to the pods is mirrored as a ~Y1… frame (SendToDeviceExSendAcBufToSvcTool(XMT), usart.c:567-568), and every complete frame received from a pod triggers a ~Y0… frame (usart.c:107-108,136-137, drained in main.c:340-343).
  • In capture mode the firmware suspends the normal refresh state machine and only watches for the tool's ~Z frames; if idle >25 s it auto-disables capture (service.c:2433-2467).

10.2 Debug mode (svcDebugMode) — available on DA-07

  • Set with command ~Pnn (service.c:2350-2352).
  • svcDebugMode == 1 changes the ~G current-values frame to send each channel's index in place of its status byte (service.c:1070-1076). Other values are reserved/no-op in the DA-07 build.

10.3 Debug strings (~I) — NOT available on DA-07

LoadDebugString/SendDebugString and the ~I message are wrapped in #ifdef DEBUGGING (service.c:942-993, service.h:107-109). DEBUGGING is not defined in any shipped DA-07 build (per DA-PQ-Xmega.cproj), so the firmware never emits ~I, and station-setting index 29 carries no debug-level value. A tool targeting production DA-07 units must not depend on ~I.


11. Tool → firmware command reference (summary)

All commands are ~<C><args>CC CR; firmware replies ~Z1 (ACK) on success or ~Z0 (NAK) on bad frame, except where a specific reply is noted. Source: ProcIncomingMsg service.c:2253-2405.

Cmd Args Action Reply
A Full refresh: svcState=SVC_START and start sequence begins ~A… stream
B nn + value Update station setting nn (§6). Value DECIMAL, except name/baseSN(hex)/IP(dotted) — §3.1 ~Z1
C dd nn + value Update device dd setting nn (1=type,2=addr,3=control,4=delay,5=serial[3]) service.c:1745. Value DECIMAL; serial[3] hex — §3.1 ~Z1
D dd cc nn + value Update channel setting (1=active,2-5=limits,6=scale,7=offset,8=alarm,10=calc,11=name,12=UModbus[07C]) service.c:1547. Value DECIMAL float — §3.1 ~Z1
E ii nn dd Update alarm-indicator ii (n=0 active flag, n≥1 addr[n-1]) service.c:1832 ~Z1
F Request config update from server (QueueRequest(STATION_REQUEST_ID)) ~Z1
G Clear outgoing buffer (ResetHistory()) ~Z1
I Clear all channel disable flags ~Z1
J Request diagnostics ~K… (then no ~Z)
K tttttttt Set RTC from time_t (MakeLong) ~Z1
L Force server update of all sensors (SendAllSensorValues) ~Z1
M aa ff rrrr nnnn Modbus passthrough (§9) ~J…
N Reset the controller (WaitForReset) — (resets; see BUG-ICD-01)
O nn Set special-option byte (1=send err msg to server, 2=alarm-ack ind#1; 0x42=inert/harmful on DA-07, see §1) ~Z1
P nn Set debug mode (svcDebugMode) ~Z1
Q Erase all alarm-indicator settings ~Z1
R n R0 = erase EEPROM + restore (svcRestore=TRUE); else reset ~Z1/reset
S nn Erase device nn ~Z1
T nn Flag device nn for reset/reacquire (gResetID=nn+1) ~Z1
X 1 X1 = erase everything (all devices/settings) ~Z1
Y n Enable(1)/disable(0) Modbus I/O capture (§10.1)
Z n Z1=ACK→send next; Z0=NAK→resend last

Several legacy/DA-33-only commands exist in the source but are inert on DA-07.


12. Worked byte-level examples

Spacing/comments added for clarity; the real frame has no spaces. Multi-byte values are little-endian hex (§3).

(a) Tool requests a full refresh. Tool → ~A + checksum + CR. Bytes: 7E 41 … checksum = (0x7E + 0x41) & 0xFF = 0xBF → "BF". Frame on wire: 7E 41 42 46 0D = ~ABF\r.

(b) Firmware config frame (SVC_START). For a DA-07 with GetMaxDevTypes()=30: ~A 00 07 01 10 0A 1E 10 08 + CC + CR = subtype 0, model 7, ver 1, MAX_DEVICES=0x10, MAX_DEVICE_CHANS=0x0A, types=0x1E, indicators=0x10, addr/grp=0x08. Wire text: ~A000701100A1E1008 + 2-hex sum + \r.

(c) Operational-stats poll (~H). Layout: ~H + 15 stat bytes (30 hex) + recordCount(uint16 LE, 4 hex) + Now()(ulong LE, 8 hex) + 16 device-status nibbles + [per active indicator: idx byte + local nibble + server nibble] + CC + CR. To decode Now(): take the 8 hex chars, reverse byte order, parse as time_t.

(d) Modbus passthrough read (~M/~J). Read 2 holding registers (func 3) at register 0x0064 from slave 0x11: Tool → ~M 11 03 0064 0002 + CC + CR (register/count big-endian — verify §9). Firmware drives RS-485: 11 03 00 64 00 02 + CRC16(LE). Firmware → ~J 11 03 04 <4 data bytes> + CC + CR (byteCount 0x04 = two 16-bit registers). On timeout: ~J00.


13. ICD-relevant bugs & quirks

This section is fully self-contained — every issue below carries its own description and file:line evidence, and nothing here requires any other document. The bracketed [ST-n] labels are optional cross-reference tags that match the same entries in the firmware project's internal bug register; ignore them if you don't have it.

  • BUG-ICD-00 [ST-13] (most important) Read/write encoding asymmetry: the firmware sends values as little-endian hex but parses write-backs as decimal ASCII (atoi/atof), with per-field exceptions (names=ASCII, serials=hex, IPs=dotted-decimal, ~E alarm-indicator=hex both ways). See §3.1 — this is the #1 thing to get right in a rewrite.
  • BUG-ICD-01 [ST-1] service.c:2319-2322: case 'N': WaitForReset(); has no break/return and falls through into case 'O'. If WaitForReset() ever returns without resetting, the firmware then reinterprets the reset frame as a set-special- option command. Tools should not rely on ~N returning an ACK.
  • BUG-ICD-02 [ST-8] Special option 0x42 is inert/harmful on DA-07: sending ~O42 makes ServiceTick early-return (service.c:2430) and the service-port RX ISR tries to bridge to the uninitialized DNT radio UART (usart.c:258-263), so normal service comms freeze while nothing useful happens. A DA-07 tool must never send it (§1).
  • BUG-ICD-03 [ST-2, ST-3] service multi-byte values are little-endian (PrintHex), but the ~M register/count and the ~K set-clock time are big-endian (and the clock is read back little-endian). Get endianness right per field (§3, §9).
  • BUG-ICD-04 [ST-5] service.c:50-52 vs 386-387: contradictory data-type code tables (A/B/C). Use the lines 38-52 table.
  • BUG-ICD-05 [ST-6] service.c:72-87 vs service.h:47-63: ~H stat order documented two ways. The service.h enum order is what's actually sent.
  • BUG-ICD-06 [ST-3] (resolved) service.c:2314-2315: ~M register/count parsed by MakeUInt are big-endian (confirmed both from firmware and the VB6 builder), unlike the little-endian normal-value fields.
  • BUG-ICD-07 [ST-4] outbound checksum skips embedded zero bytes (usart.c:1049) while inbound verify does not (service.c:1922); harmless for well-formed frames.
  • BUG-ICD-08 [ST-11] gStation.stacklights (setting 23) is read/written by the tool but never consumed by the firmware — changing it has no effect.
  • BUG-ICD-09 [ST-7] SendDeviceSettings guards if (device > MAX_DEVICES) (off-by-one; should be >=, service.c:1245).
  • BUG-ICD-10 [ST-9] the ~I debug-string message is not compiled on DA-07 (#ifdef DEBUGGING off, service.c:942-993) — the firmware never emits it. Don't build a tool feature that waits for ~I (§5.I, §10.3).
  • BUG-ICD-11 [ST-10] the ~Y (I/O-capture) and ~Z (ack) command handlers return without sending an ACK (service.c:2382-2400); the steady refresh is ACK-driven (the tool advances it with ~Z1), so not every command is ACKed (§4.1, §11).
  • BUG-ICD-12 [ST-12] tool→firmware string fields lose any literal ~ (it is escaped to a space, usart.c:920-923) — e.g. a station name containing ~ is silently corrupted to a space (§2.2).
  • BUG-ICD-13 the subnet bits→mask conversion is buggy outside stored 18 (netburner.c:464-481; found 2026-06-12 building the service tool's subnet editor). xpSubnetBits is a HOST-bit count (trailing zeros of the mask — the legacy XPort encoding, "the number of zeros on the right side of the subnet mask", netburner.c:470-473). Two defects in the reconstruction: (1) a byte-swap precedence bugnetmaskIp & 0x0000FF00 << 8 parses as netmaskIp & (0x0000FF00 << 8) (netburner.c:475-476), so the two middle octets are never swapped during the endian reversal and stored 915 (/23…/17) reach the SBL2e Ethernet module with their middle octets exchanged (e.g. stored 9, meant as 255.255.254.0, goes out as 255.254.255.0 — a non-contiguous, illegal mask); (2) a 16-bit shift overflow(1 << bits) is a 16-bit int on the Xmega (netburner.c:474; the DA-12C path uses 1UL), so for stored 16255 the mask degenerates to 0.0.0.0 (strictly undefined behavior; collapse-to-0 is what the shipped avr-gcc/MCU does), killing the common /16 (stored 16) and /8 (stored 24). Only stored 0 (default → 255.255.255.0, netburner.c:464-467, factory default config.c:135; unlike the DA-12C, 255 is NOT a sentinel) and 18 (masks /24…/31) are applied correctly. Likely never field-visible because the mask is ignored in DHCP mode. The Python tool therefore rejects subnet input outside 18 and flags read-back 9255 (modules/da07/protocol/subnet.py, §6 row 9).

14. Appendix — VB6 / Python cross-check

Cross-checked against the legacy VB6 tool (D:\Work\cimtechniques-service-suite\…\da07\legacy\) and the Python rewrite (…\da07\). The DA-07 is the "eLink" in tool code.

14.1 Confirmed (firmware ↔ VB6 agree — high confidence)

  • Framing: ~ + payload + 2-hex checksum + CR; no 0xAA55, no length field, no byte-stuffing (bitstuff.bas is only bit helpers). (Main.frm:3312-3329.)
  • Checksum: 8-bit additive sum of ~+payload, mod 256, 2 upper-hex; init 0. Identical to firmware SendSvcCheck/SvcChecksumOK.
  • Serial: 9600,n,8,1, RTS asserted, NUL-discard, no flow control.
  • Handshake: one-frame-per-ACK; Z0=NAK→resend, Z1=ACK→next, Z2=idle; refresh always begins with outbound A; stream "ends" when ~H stats start (the tool infers end by a quiet timer — there is no end marker).
  • Encodings: all multi-byte values little-endian ASCII hex (LeftEx); float = IEEE-754 single; TIME Unix-secs with a < 2009-01-01 value shown as seconds-since-restart. Matches firmware PrintHex + the IsValidTime epoch gate.
  • ~M~J passthrough: tool sends ASCII M+addr+func+reg+count; the station builds the Modbus frame + CRC16 (the tool's own CRC16 in Support.bas is defined but never called). ~J 2-byte reply = error (00=CRC err, 01=no response).
  • Modbus CRC16 params (poly 0xA001, init 0xFFFF, reflected) match crc.c.
  • Station-setting typeCodes and the inbound message layouts (A/B/C/D/E/F/G/H/ L/M/N/P/W/Y) match the firmware serializers documented in §5§7.

14.2 Resolved open questions

  1. ~M register/count byte order = big-endian (§9). ✓
  2. ~K set-clock byte order: the tool sends the time big-endian (Hex8, no reversal) but reads times back little-endian. So the firmware's MakeLong parse of ~K is big-endian, an asymmetry vs the little-endian ~F/~G/~H times. The Python rewrite already flags this. (BUG ST-2.) ✓
  3. ~A device-type descriptor layout filled in §5.A1. ✓
  4. ~I / DEBUGGING: the VB6 tool can display ~I debug records, but the DA-07 firmware never emits them (DEBUGGING off). Production DA-07 units simply never send ~I. ✓

14.3 Tool-vs-firmware discrepancies (rewrite must heed)

  • Outbound vs inbound letters reuse the same letter for different meanings. Track direction. Outbound (tool→fw) catalog confirmed: A refresh, B write-station- setting, C write-device-field, D write-channel-field, E write-alarm-indicator, F request server config update (not a read), G erase outgoing buffer (destructive), I clear-disable/debug, J diagnostics (bare), K set-clock, L force-update, M passthrough, N reset, O special-option, P debug-mode, Q clear-alarm-ind, R0/R1 restore start/end, S remove-device, T reset- device, X1 erase-all, Y1/Y0 I/O-capture, Z ack. (Inbound meanings in §5.)
  • The Python rewrite currently mislabels F and G as value-reads. They are NOT reads; G erases the buffer. Fix in the rewrite (firmware-confirmed: case 'F' = QueueRequest(STATION_REQUEST_ID), case 'G' = ResetHistory(), service.c:2277-2291).
  • Commands the VB6 tool sends that the DA-07 firmware does NOT implement (would be NAKed or behave differently — likely targeting other "eLink" products):
    • 6 (bulk-set device serial) — no case '6' in ProcIncomingMsg → NAK.
    • J with arguments (alarm-group membership, Multi.frm) — firmware case 'J' always just sends diagnostics, ignoring any args.
    • M00/M01 (switch eLink PodNet↔Modbus mode, Mode.frm) — firmware case 'M' treats these as a passthrough to address 0x00, not a mode switch.
    • R1 (the tool's "restore-stream end marker") — firmware case 'R' with a non-'0' arg calls WaitForReset(), i.e. the station resets. (Possibly the intended post-restore reboot, but the tool's intent label differs.)
  • ~E channel record trailing fields (RESOLVED — follow the firmware): the VB6 decoder reads …alarm, disp, name, but the DA-07 firmware emits no disp fieldC_PARAMS ends at alarm (27 bytes total) because NEED_C_PARAMS_DISPLAY_FIELD is off (io.h:60-73), followed only by an optional 8-byte CT serial (service.c:1143-1150). The VB6's disp read is therefore consuming a phantom byte (the first serial byte, or nothing). The rewrite should parse ~E per §5.E, not per the VB6.
  • eLink ASCII vs Modbus service mode: the tool declares eLinkType (0=ASCII, 1=Modbus). Everything here is the ASCII path. No second (Modbus-framed) service protocol was found in the DA-07 firmware; treat ASCII as the only DA-07 service mode unless proven otherwise.