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

Why Your SECS-II Message Gets No Reply: The W-Bit Shares a Byte With the Stream

S1F1 sent, no S1F2, T3 expires — and the tool did nothing wrong. Captures of Header Byte 2 showing what packing the W-bit and Stream together costs.

SECS/GEMMESSCADATroubleshootingChecklists

The host log says T3 timeout: S1F1 went out, S1F2 never came back. Then you pull the wire and the header that left looks like 00 0B 01 01 00 00 00 00 0A 03. Third byte is 01. The tool did not fail to answer — it was asked not to. In an HSMS header the Stream and the W-bit live in the same byte. SEMI E37 numbers the ten header bytes from zero, and in byte 2 — the third one you see — the top bit is the W-bit and the remaining seven bits are the Stream. Lose that one bit and the symptom reads as "the equipment doesn't respond", which is how you end up on the phone with the tool vendor.

The 10-byte HSMS data message header — Header Byte 2 splits into a one-bit W-bit and a seven-bit Stream HSMS header, 10 bytes — S1F1 with W-bit 1 0–1 2 3 4 5 6–9 00 0B 01 00 00 00 00 0A 02 81 SessionID Function PType SType SystemBytes 765 432 10 000 000 1 1 bits 6–0 = Stream 1 bit 7 = W-bit

Two S1F13s, one bit apart

I attached to the simulator's EQ1 passive listener at 127.0.0.1:5501 from outside, with a plain Python socket client, everything on one TCP connection. Every hex string below was on that socket, and only frames the equipment side logged as RX are shown. Times are measured at the client. Captured 2026-09-19 with no equipment faults set — {} throughout, confirmed {} afterwards — and the raw export is committed at content/demos/hsms-wbit-header-byte-2.json.

E37 numbers the ten header bytes after the four-byte length prefix from zero.

offsetfieldon a data message (SType 0)
0–1SessionID00 0B = 11
2W-bit + Streambit 7 is the W-bit, the low seven bits are the Stream
3Function0D = 13
4PType00, meaning the body is SECS-II encoded
5SType00 makes it a data message
6–9SystemBytesthe reply carries them back unchanged

Select first.

06:03:55.044 TX Select.req   00 00 00 0A 00 0B 00 00 00 01 00 00 04 01
06:03:55.046 RX Select.rsp   00 00 00 0A 00 0B 00 00 00 02 00 00 04 01   1.9 ms

SType 01/02 makes these control messages. On a control message byte 2 is not a W-bit + Stream field at all, and byte 3 carries the Select Status — here 00, accepted (reading a Select.rsp). That is SELECTED.

Now two S1F13s on that same connection, identical except for byte 2 and the SystemBytes.

06:03:55.046 TX S1F13 W=1    00 00 00 1A 00 0B 81 0D 00 00 00 00 04 02 01 02 41 07 48 4F 53 54 2D 30 31 41 03 31 2E 30
06:03:55.047 RX S1F14        00 00 00 37 00 0B 01 0E 00 00 00 00 04 02 01 02 21 01 00 01 02 41 15 ...   1.3 ms

81 = 1000 0001. Bit 7 set, so W-bit; the low seven bits are 000 0001, Stream 1. The 0D in byte 3 is Function 13. The S1F14 that came back has 01 in byte 2 — same Stream 1, W-bit cleared, which is what a secondary should carry — and 21 01 00 at the head of its body is COMMACK 0. SystemBytes 00 00 04 02 came back unchanged.

06:03:55.047 TX S1F13 W=0    00 00 00 1A 00 0B 01 0D 00 00 00 00 04 03 01 02 41 07 48 4F 53 54 2D 30 31 41 03 31 2E 30
06:04:05.057 (no reply)      10,010.1 ms of silence, socket still open

81 became 01. Same Stream 1, same Function 13, same body bytes. The equipment's export holds this frame as RX with wbit: false and nothing follows it — no E→H line at all. It arrived, and the reply was withheld. Ten seconds later the peer still had not closed the socket.

Same experiment with S1F1.

06:04:05.057 TX S1F1  W=1    00 00 00 0A 00 0B 81 01 00 00 00 00 04 04
06:04:05.058 RX S1F2         00 00 00 32 00 0B 01 02 00 00 00 00 04 04 01 02 41 15 ...   0.6 ms

06:04:05.058 TX S1F1  W=0    00 00 00 0A 00 0B 01 01 00 00 00 00 04 05
06:04:15.068 (no reply)      10,010.2 ms of silence

Fourteen bytes, no body. With the W-bit set, S1F2 with MDLN and SOFTREV in 0.6 ms. With it clear, nothing. Both times the difference is one bit at the top of byte 2.

The 10,010 ms figures are my client's own recv timeout, not a value from the standard. T3 had not come close to expiring.

What the W-bit decides

In SEMI E5 an odd function is a primary and the even one that follows is its secondary. The W-bit is how the sender of that primary tells the other side whether it intends to wait for the secondary. W=1 means the receiver owes you a reply; W=0 means it must not send one. That is exactly what the equipment did above.

So a host that sends S1F3 with W=0 and then waits for S1F4 waits, exactly per spec, until T3 expires. T3 is the reply timeout E5 and E30 use — 45 s default, range 1–120 s — so a timeout that lands a full 45 seconds in is usually this one. Nobody broke a rule. The tool stayed quiet because it was told to.

Two paths lead here. One is a library with separate send_and_wait and send_no_wait calls where the W setting lives in the message builder instead — you call the waiting API and hand it a message with W clear. The other is hand-assembled headers with byte2 = stream in them. Stream 1 really is 1. The W is just missing.

The mirror-image bug: reading without the mask

On the receiving side, reading byte 2 straight as the Stream turns an S1F1 with W set into S129F1, because 0x81 is 129. No such Stream exists. You get "unknown stream 129" in a log or a silent drop, and either way the sender sees a message that vanished.

The simulator's decoder reads stream = byte2 & 0x7F and wbit = byte2 & 0x80. In the export above it split 0x81 into stream 1 / wbit true and 0x01 into stream 1 / wbit false. One comparison anywhere in your parser that skips the mask and every W=1 message dies there — and those are disproportionately the primaries that open a conversation, S1F1 and S1F13 among them, so the symptom is loud.

E5 has a stream for saying so: S9F3 for an unrecognized Stream, S9F5 for an unrecognized Function. Whether a tool actually emits stream 9 varies by implementation and this capture does not confirm it either way. The point to keep is narrower — silence from the far end is not evidence your message arrived intact.

Setting W on an even function

S6F12 is the reply to S6F11, a secondary. In E5 the W-bit of a secondary is 0; setting it asks for a reply to a reply, which does not exist. I sent it anyway.

06:04:15.068 TX S6F12 W=1    00 00 00 0D 00 0B 86 0C 00 00 00 00 04 06 21 01 00
06:04:15.069 RX S6F0         00 00 00 0A 00 0B 06 00 00 00 00 00 04 06   1.0 ms

86 is W-bit 1 + Stream 6, and 0C in byte 3 is Function 12. What came back has 06 in byte 2 and 00 in byte 3 — Stream 6, Function 0, empty body. Function 0 is E5's abort-transaction message. That is this simulator's choice and not behaviour every tool will match; another one may simply discard the message.

In practice this comes out of one wrapper function written as "set W on everything we send" — S6F12 for event reports, S5F2 for alarms, all of it goes out with a bad header.

Where to look in host code

  • Build the header as byte2 = (stream & 0x7F) | (0x80 if you will wait for a reply else 0). Don't drop the raw stream in.
  • Parse it as stream = byte2 & 0x7F. The mask cannot be missing from a single site.
  • When you build a secondary — any even function — the W-bit is 0.
  • Don't arm a T3 timer on a transaction you sent with W=0. If you do, correct behaviour logs as a timeout every time, and the real T3 gets buried in it. Which timer actually fired is an article of its own.
  • Match replies on SystemBytes. Code that matches by arrival order falls apart the moment two replies cross.

The first question to ask about a T3 timeout is whether the top bit of byte 2 was set on the way out. A capture answers that; a log usually doesn't, because the log line says S1F1 sent whether the W-bit was there or not.

The captures above came from the passive listener in the SECS/GEM simulator. Open a socket, change 81 to 01 in byte 2, and you get the same silence.