← Articles
SECS/GEM/10 min read/— views

Your SECS-II Length Byte Counts Bytes — Unless the Item Is a List

An S1F3 body byte by byte: the item header packs format code and length-byte count, and one wrong length silences the whole connection.

SECS/GEMMESSCADATroubleshootingChecklists

You sent S1F3 and the tool went quiet. The host log has one T3 timeout line, and the capture shows your 24 bytes going out exactly as expected. At that point there's nothing left to do but read the body hex yourself. The first 10 bytes are SEMI E37's header; everything after them is SEMI E5's. E5's rule is short: an item starts with one header byte, and that byte carries both the format code and the number of length bytes that follow.

The 14-byte S1F3 body — a 2-byte List header and two U4 items, with the item header byte B1 split into format code 101100 and length-byte count 01 S1F3 body — 14 bytes List header item 1 — U4 101 item 2 — U4 102 0102 0400 000065 0400 000066 B1B1 2 elements length 4 length 4 item header byte 101 100 01 format code 101100 = U4 1 length byte

Two things live in one header byte

In E5 the item header's top 6 bits are the format code and the bottom 2 bits are how many length bytes follow. That count is 1, 2 or 3. Never 0. So a single item's body tops out at 2^24-1 bytes, and if you need to send more than that, splitting the item is the only option.

E5 gives the format codes in octal. Trimmed to what you actually see on a line:

octal codetypeheader byte (with 1 length byte)
00List01
010Binary21
011Boolean25
020ASCII41
030 / 031 / 032 / 034I8 / I1 / I2 / I461 / 65 / 69 / 71
040 / 044F8 / F481 / 91
050 / 051 / 052 / 054U8 / U1 / U2 / U4A1 / A5 / A9 / B1

The arithmetic is (format code << 2) | length byte count. U4 is octal 054, decimal 44; shift it two places and you get 0xB0; add one length byte and you get B1. ASCII is octal 020, decimal 16, shifts to 0x40, becomes 41. Anyone who has stared at a SECS trace knows 41 on sight — it's the start of a string item. Spread B1 out into bits and you get 1011 0001: the top six, 101100, are the format code U4, and the bottom two, 01, mean 1 length byte.

The trap is what the length bytes count. For every item except a List, the length is a byte count. For a List, it's the number of elements. 01 02 is not a 2-byte list, it's a list with two elements, and how many bytes those two elements occupy is something you only learn by reading their own headers. Write your own encoder and mix these up, and short messages happen to work while nested lists come apart.

An S1F3 actually put on a socket

I opened a plain socket to the simulator's passive listener, played host, sent Select.req first, then S1F3. The hex below is what the equipment side logged as RX.

TX Select.req  00 00 00 0A 00 0B 00 00 00 01 00 00 03 01
RX Select.rsp  00 00 00 0A 00 0B 00 00 00 02 00 00 03 01

TX S1F3 W      00 00 00 18 00 0B 81 03 00 00 00 00 03 02
               01 02 B1 04 00 00 00 65 B1 04 00 00 00 66
RX             (nothing within 1000 ms)

Field by field:

  • 00 00 00 18 — E37's 4-byte length prefix. 24: header 10 plus body 14.
  • 00 0B — SessionID 11.
  • 81 — Header Byte 2. Top bit is the W-bit, the other seven are the Stream. 0x81 is W-bit set, Stream 1.
  • 03 — Function 3. Together, S1F3, a status variable request that wants a reply.
  • 00 00 — PType 0, SType 0. SType 0 is what makes this a data message.
  • 00 00 03 02 — SystemBytes.
  • 01 02 — List, two elements.
  • B1 04 00 00 00 65 — U4, length 4 bytes, value 101. SVID 101.
  • B1 04 00 00 00 66 — U4, value 102.

Nothing comes back because this listener doesn't answer data messages. There is no S1F4 here. What I was checking wasn't the equipment's reply but whether the bytes arrived intact, and the equipment-side RX hex matched what I sent, byte for byte.

The capture does not prove the item decoding — this listener never parses the body. That reading comes from E5, and I'm stating it as a citation rather than a measurement.

An ASCII item and an empty List

Next message down the same connection, an S2F41:

TX S2F41 W     00 00 00 15 00 0B 82 29 00 00 00 00 03 03
               01 02 41 05 53 54 41 52 54 01 00

82 is W-bit plus Stream 2, 29 is Function 41. The body opens with 01 02, a List of two elements. First element is 41 05 53 54 41 52 54 — ASCII, length 5, START. Second is 01 00, a List of zero elements, meaning no parameters. An empty List still needs its header byte and its length byte. I've seen an encoder drop those two bytes entirely, and everything after them shifts by one.

The total length 00 00 00 15 is 21: header 10 plus body 11. That 11 is List header 2 plus ASCII 7 plus empty List 2. The outer length prefix has to equal the sum of the item lengths inside. Keeping those two in agreement is entirely the encoder's job.

Now drop the length prefix by one

Same S1F3, every byte untouched except the first four, which I changed to 00 00 00 17 — 23.

TX S1F3 W (length -1)
   00 00 00 17 00 0B 81 03 00 00 00 00 04 02
   01 02 B1 04 00 00 00 65 B1 04 00 00 00 66

what the equipment logged
   00 00 00 17 00 0B 81 03 00 00 00 00 04 02
   01 02 B1 04 00 00 00 65 B1 04 00 00 00

The trailing 66 is gone. The equipment cut where the prefix told it to, at 23 bytes, and the leftover byte stayed in the receive buffer. TCP is fine. No error was raised.

The damage shows up next. On the same connection I sent a perfectly valid S2F41, then a Linktest.req.

TX S2F41 W (correct length)  → nothing logged
TX Linktest.req              → no reply

The equipment log holds the truncated S1F3 and nothing after it. That leftover 66 became the first byte of the next message, so the parser read a length of 66 00 00 00 — about 1.7 billion. It settles in to wait for that much, and everything arriving on the connection afterwards gets swallowed into it. As a control I fixed the length and sent the same sequence again; Linktest.rsp came straight back.

The lesson is where the symptom lands. Your log records that the next message failed, while the thing that was wrong is the length of the one before it. Linktest died, so you start suspecting T6, and the actual cause is a number your encoder computed a few seconds earlier.

This listener doesn't implement T8

Going silent forever is not what E37 asks for. E37 defines T8, the network intercharacter timeout — how long to wait for the next byte in the middle of a message. It exists for exactly this situation. The simulator doesn't implement it, so it waited indefinitely. Real equipment should let T8 expire and drop the connection.

From the host's side the difference is small. Either way that connection is finished; what changes is how long it takes you to find out.

What to bolt onto the encoder

  • After assembling a frame, assert length prefix == total length - 4. One line, and it removes the entire failure above.
  • Recompute the body length by walking the item tree and compare it against the prefix. If they disagree, don't send.
  • Put the element count in a List's length bytes. Not the byte count.
  • In the decoder, if items are exhausted and body bytes remain, raise an error instead of discarding them. Leftover bytes mean the encoder was wrong.
  • When logging a header byte, keep the raw hex like B1 alongside. Logging only U4 throws away how many length bytes there were.

Every capture here came from a real socket opened against the passive listener in the SECS/GEM simulator. To reproduce it, point your own host stack at it, encode a body, send it, and compare the equipment-side RX hex against what you sent.

How the length prefix marks message boundaries in the first place is its own article, and what packing the W-bit and the Stream into Header Byte 2 costs you is here.