Zigbee.EZSP.ASH (zigbee v0.1.0)

Copy Markdown View Source

ASH (Asynchronous Serial Host) protocol codec for the EmberZNet NCP-UART interface, per Silicon Labs UG101.

ASH is the reliable data-link layer that sits directly on top of the raw serial byte stream and carries EZSP frames. This module is pure: it turns EZSP payloads (and control frames) into on-the-wire byte sequences and back, with no I/O and no connection state. The stateful concerns (reset handshake, frame/ack numbering, retransmission) live in Zigbee.EZSP.ASH.Connection.

Frame anatomy (on the wire)

[ control byte ][ data field ][ CRC-16 hi ][ CRC-16 lo ][ 0x7E flag ]
\_____________ byte-stuffed ________________/
  • The control byte identifies the frame type and carries frame/ack numbers.
  • The data field is present only on DATA frames; it is the (randomized) EZSP payload.
  • The CRC (CRC-16-CCITT, init 0xFFFF) is computed over control byte + data field, transmitted high byte first.
  • Everything except the trailing flag is then byte-stuffed to escape reserved control bytes.

Summary

Functions

Encode an ACK frame acknowledging up to (but not including) ack_num.

The cancel byte (0x1A); sent before RST to flush a partial frame.

CRC-16-CCITT (a.k.a. CRC-16/CCITT-FALSE): initial value 0xFFFF, polynomial 0x1021, no reflection, no final XOR. Returned as a 16-bit integer.

Encode a DATA frame carrying an EZSP payload.

Decode a single raw frame (the bytes between flags, without the trailing 0x7E) into a structured map.

The frame delimiter byte (0x7E).

Encode a NAK frame requesting retransmission from ack_num.

Randomize (or de-randomize, the operation is its own inverse) a DATA field by XORing it with an LFSR pseudo-random sequence seeded at 0x42.

Encode the RST (reset) frame, prefixed with a Cancel byte to flush any partial frame the NCP may be mid-way through. The result is always the fixed sequence 1A C0 38 BC 7E.

Escape reserved bytes. Each reserved byte is replaced by the escape byte (0x7D) followed by the original byte XORed with 0x20.

Reverse of stuff/1. Returns {:ok, binary} or {:error, :bad_escape}.

Functions

ack_frame(ack_num, not_ready? \\ false)

@spec ack_frame(0..7, boolean()) :: binary()

Encode an ACK frame acknowledging up to (but not including) ack_num.

cancel()

The cancel byte (0x1A); sent before RST to flush a partial frame.

crc(data)

@spec crc(binary()) :: 0..65535

CRC-16-CCITT (a.k.a. CRC-16/CCITT-FALSE): initial value 0xFFFF, polynomial 0x1021, no reflection, no final XOR. Returned as a 16-bit integer.

For example crc(<<0xC0>>) is 0x38BC, which is why the RST frame ends in 38 BC before the flag.

data_frame(frame_num, ack_num, payload, retransmit? \\ false)

@spec data_frame(0..7, 0..7, binary(), boolean()) :: binary()

Encode a DATA frame carrying an EZSP payload.

  • frame_num: this frame's sequence number (0..7)
  • ack_num: the next frame number we expect from the NCP (0..7)
  • payload: the EZSP frame bytes (will be randomized)
  • retransmit?: set on retransmissions so the NCP can detect duplicates

decode(raw)

@spec decode(binary()) :: {:ok, map()} | {:error, atom()}

Decode a single raw frame (the bytes between flags, without the trailing 0x7E) into a structured map.

Returns {:ok, frame} where frame is one of:

%{type: :data, frame_num: 0..7, ack_num: 0..7, retransmit?: bool, payload: binary}
%{type: :ack,  ack_num: 0..7, not_ready?: bool}
%{type: :nak,  ack_num: 0..7, not_ready?: bool}
%{type: :rstack, version: byte, reset_code: byte}
%{type: :error,  version: byte, error_code: byte}
%{type: :rst}

or {:error, reason} on a CRC mismatch, bad escape, or truncated frame.

flag()

The frame delimiter byte (0x7E).

nak_frame(ack_num, not_ready? \\ false)

@spec nak_frame(0..7, boolean()) :: binary()

Encode a NAK frame requesting retransmission from ack_num.

randomize(data)

@spec randomize(binary()) :: binary()

Randomize (or de-randomize, the operation is its own inverse) a DATA field by XORing it with an LFSR pseudo-random sequence seeded at 0x42.

Only the EZSP payload of DATA frames is randomized; control bytes and CRC are never randomized.

rst_frame()

@spec rst_frame() :: binary()

Encode the RST (reset) frame, prefixed with a Cancel byte to flush any partial frame the NCP may be mid-way through. The result is always the fixed sequence 1A C0 38 BC 7E.

stuff(data)

@spec stuff(binary()) :: binary()

Escape reserved bytes. Each reserved byte is replaced by the escape byte (0x7D) followed by the original byte XORed with 0x20.

unstuff(data)

@spec unstuff(binary()) :: {:ok, binary()} | {:error, :bad_escape}

Reverse of stuff/1. Returns {:ok, binary} or {:error, :bad_escape}.