Skip to content

Protocol Specification

The apollo.protocol module provides three categories of utility functions:

  1. Sync word functions — generate, parse, and convert the 32-bit PCM frame sync pattern
  2. AGC I/O packet functions — encode and decode the 4-byte Virtual AGC socket protocol
  3. A/D conversion functions — translate between 8-bit ADC codes and voltages
from apollo.protocol import (
generate_sync_word, parse_sync_word, sync_word_to_bytes,
sync_word_to_bits, bits_to_sync_word,
form_io_packet, parse_io_packet,
adc_to_voltage, voltage_to_adc,
)

The PCM frame sync word is a 32-bit pattern (4 words) at the start of every telemetry frame. Its bit layout:

Bit 31 27 26 12 11 6 5 0
┌──────────────┬──────────────┬───────────┬───────────┐
│ A field (5) │ Core (15) │ B field(6)│Frame ID(6)│
└──────────────┴──────────────┴───────────┴───────────┘
MSB LSB

The 15-bit core is bitwise-complemented on odd-numbered frames, providing a built-in frame parity indicator.

Generate a 32-bit PCM frame sync word as an integer.

from apollo.protocol import generate_sync_word
word = generate_sync_word(frame_id=1, odd=False)
# Returns: 0xACEB4001 (with default field values)
def generate_sync_word(
frame_id: int,
odd: bool = False,
a_bits: int = DEFAULT_SYNC_A, # 0b10101 = 21
core: int = DEFAULT_SYNC_CORE, # 0b111001101011100 = 29404
b_bits: int = DEFAULT_SYNC_B, # 0b110100 = 52
) -> int
ParameterTypeDefaultDescription
frame_idint— (required)Frame number within the subframe. Range: 1-50
oddboolFalseIf True, the 15-bit core is bitwise complemented
a_bitsint0b10101 (21)5-bit patchboard-selectable A field. Only lower 5 bits used
coreint0b111001101011100 (29404)15-bit fixed core pattern (even-frame value). Only lower 15 bits used
b_bitsint0b110100 (52)6-bit patchboard-selectable B field. Only lower 6 bits used

A 32-bit integer: (a << 27) | (core << 12) | (b << 6) | frame_id

When odd=True, the core value is replaced with (~core) & 0x7FFF before assembly.

  • ValueError if frame_id is not in range 1-50.

Parse a 32-bit sync word integer into its component fields.

from apollo.protocol import parse_sync_word
fields = parse_sync_word(0xACEB4001)
# Returns: {'a_bits': 21, 'core': 29404, 'b_bits': 52, 'frame_id': 1, 'word': 0xACEB4001}
def parse_sync_word(word: int) -> dict
ParameterTypeDescription
wordint32-bit sync word value
KeyTypeBit RangeDescription
a_bitsint31-275-bit A field
coreint26-1215-bit core (as-is, not un-complemented)
b_bitsint11-66-bit B field
frame_idint5-06-bit frame ID
wordintOriginal 32-bit word (pass-through)

Convert a 32-bit sync word to 4 bytes, MSB first (matching NRZ serial output order).

from apollo.protocol import sync_word_to_bytes
raw = sync_word_to_bytes(0xACEB4001)
# Returns: b'\xac\xeb\x40\x01'
def sync_word_to_bytes(word: int) -> bytes
ParameterTypeDescription
wordint32-bit sync word value

4 bytes in big-endian (MSB first) order.


Convert a 32-bit sync word to a list of 32 individual bit values, MSB first.

from apollo.protocol import sync_word_to_bits
bits = sync_word_to_bits(0xACEB4001)
# Returns: [1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, ...] (32 elements)
def sync_word_to_bits(word: int) -> list[int]
ParameterTypeDescription
wordint32-bit sync word value

List of 32 integers (each 0 or 1), bit 31 first.


Convert a list of 32 bit values back to a 32-bit integer. Inverse of sync_word_to_bits.

from apollo.protocol import bits_to_sync_word
word = bits_to_sync_word([1, 0, 1, 0, ...]) # 32 bits
def bits_to_sync_word(bits: list[int]) -> int
ParameterTypeDescription
bitslist[int]Exactly 32 bit values (0 or 1), MSB first

32-bit integer.

  • ValueError if len(bits) != 32.

The Apollo Guidance Computer emulator (yaAGC) communicates over TCP using a 4-byte packet format. Each packet carries one I/O channel update: a 9-bit channel number and a 15-bit data value.

These functions are direct ports of FormIoPacket() and ParseIoPacket() from yaAGC/SocketAPI.c in the Virtual AGC project.

Byte 0 Byte 1 Byte 2 Byte 3
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
┌─┬─┬─────────┐ ┌─┬─┬─────┬─────┐ ┌─┬─┬───────────┐ ┌─┬─┬───────────┐
│0│0│Ch[8:3] │ │0│1│Ch[2:0]│V[14:12]│ │1│0│ V[11:6] │ │1│1│ V[5:0] │
└─┴─┴─────────┘ └─┴─┴─────┴─────┘ └─┴─┴───────────┘ └─┴─┴───────────┘
sig=0x00 sig=0x40 sig=0x80 sig=0xC0
ByteSignature (bits 7-6)Data BitsContent
000bits 5-0Channel bits 8-3
101bits 5-3: channel 2-0, bits 2-0: value 14-12Channel low bits + value high bits
210bits 5-0Value bits 11-6
311bits 5-0Value bits 5-0

The 2-bit signature prefix on each byte enables packet resynchronization after data loss.


Encode a channel/value pair into a 4-byte Virtual AGC I/O packet.

from apollo.protocol import form_io_packet
packet = form_io_packet(channel=0o45, value=0x1234)
# Returns: b'\x04\x49\x88\xf4' (4 bytes)
def form_io_packet(channel: int, value: int, u_bit: bool = False) -> bytes
ParameterTypeDefaultRangeDescription
channelint— (required)0-511 (9 bits)I/O channel number. Values beyond 9 bits are masked
valueint— (required)0-32767 (15 bits)Data value. Values beyond 15 bits are masked
u_bitboolFalseIf True, sets bit 5 in byte 3 data field, marking this as a mask update rather than data. This is a yaAGC extension

4 bytes following the packet format above.

b0 = (channel >> 3) & 0x3F
b1 = 0x40 | ((channel & 0x07) << 3) | ((value >> 12) & 0x07)
b2 = 0x80 | ((value >> 6) & 0x3F)
b3 = 0xC0 | (value & 0x3F)
if u_bit: b3 |= 0x20

Decode a 4-byte Virtual AGC I/O packet into channel, value, and u_bit.

from apollo.protocol import parse_io_packet
channel, value, u_bit = parse_io_packet(b'\x04\x49\x88\xf4')
# Returns: (37, 4660, False)
def parse_io_packet(packet: bytes) -> tuple[int, int, bool]
ParameterTypeDescription
packetbytesExactly 4 bytes

A tuple of:

IndexTypeDescription
0intChannel number (0-511)
1intData value (0-32767)
2boolu_bit flag (always False in current implementation — standard data packets)
  • ValueError if len(packet) != 4
  • ValueError if any byte has an invalid signature prefix (bits 7-6 must follow the 00, 01, 10, 11 sequence)
channel = ((b0 & 0x3F) << 3) | ((b1 >> 3) & 0x07)
value = ((b1 & 0x07) << 12) | ((b2 & 0x3F) << 6) | (b3 & 0x3F)

Complete table of AGC I/O channels relevant to the telecommunications system:

OctalDecimalConstantDirectionPurpose
0108AGC_CH_OUT0OutputRelay rows
0119AGC_CH_DSALMOUTOutputDSKY alarm indicators
01311AGC_CH_CHAN13OutputRadar activity
03024AGC_CH_CHAN30InputStatus and alarm bits
03327AGC_CH_CHAN33InputAGC warning input
03428AGC_CH_DNTM1OutputDownlink telemetry word 1 (high 7 bits)
03529AGC_CH_DNTM2OutputDownlink telemetry word 2 (low 8 bits)
04537AGC_CH_INLINKInputUplink data from ground (triggers UPRUPT)
05747AGC_CH_OUTLINKOutputDigital downlink data

The AGC_TELECOM_CHANNELS frozenset contains channels 034, 035, 045, and 057 (the four primary telecom channels). The AGCBridgeClient uses this set as its default channel filter.


The Apollo PCM encoder uses an 8-bit analog-to-digital converter with a non-standard code mapping. Per IMPL_SPEC section 5.3:

Input VoltageADC CodeBinary
Below range000000000
0V100000001
4.98V (full scale)25411111110
>5V (overflow)25511111111

Step size: 4.98V / 253 = 19.7 mV per LSB.

For low-level analog inputs (0-40 mV range), a x125 gain amplifier is applied before the ADC. The conversion functions handle this gain transparently.


Convert an 8-bit ADC code to a voltage.

from apollo.protocol import adc_to_voltage
v = adc_to_voltage(128)
# Returns: 2.4980... (approximately 2.5V, midscale)
v_low = adc_to_voltage(128, low_level=True)
# Returns: 0.01998... (approximately 20 mV, after removing x125 gain)
def adc_to_voltage(code: int, low_level: bool = False) -> float
ParameterTypeDefaultDescription
codeint— (required)8-bit ADC value (0-255)
low_levelboolFalseIf True, divide result by 125 to recover actual input voltage for low-level (0-40 mV) channels

Voltage in volts (float).

CodeReturnsReason
00.0Below range
≥ 2555.0Overflow
1-254(code - 1) * 4.98 / 253Normal conversion

Convert a voltage to an 8-bit ADC code. Inverse of adc_to_voltage.

from apollo.protocol import voltage_to_adc
code = voltage_to_adc(2.5)
# Returns: 128
code = voltage_to_adc(0.020, low_level=True)
# Returns: 128 (0.020V * 125 = 2.5V internal)
def voltage_to_adc(voltage: float, low_level: bool = False) -> int
ParameterTypeDefaultDescription
voltagefloat— (required)Input voltage in volts
low_levelboolFalseIf True, apply x125 gain before conversion (for 0-40 mV inputs)

8-bit ADC code (int), clamped to range 1-254.

VoltageReturnsReason
≤ 0.01Zero code
≥ 4.98254Full-scale code
Betweenround(voltage * 253 / 4.98) + 1Normal conversion, clamped to 1-254
If low_level: voltage = voltage * 125
code = round(voltage * 253 / 4.98) + 1
code = clamp(code, 1, 254)

from apollo.protocol import generate_sync_word, parse_sync_word, sync_word_to_bits, bits_to_sync_word
# Generate even frame 1
word = generate_sync_word(frame_id=1, odd=False)
fields = parse_sync_word(word)
assert fields["frame_id"] == 1
# Round-trip through bits
bits = sync_word_to_bits(word)
assert len(bits) == 32
recovered = bits_to_sync_word(bits)
assert recovered == word
from apollo.protocol import form_io_packet, parse_io_packet
# Encode an uplink command to INLINK channel
packet = form_io_packet(channel=0o45, value=0x5A00)
channel, value, u_bit = parse_io_packet(packet)
assert channel == 0o45 # 37 decimal
assert value == 0x5A00
from apollo.protocol import adc_to_voltage, voltage_to_adc
# Standard high-level channel
for code in [1, 64, 128, 192, 254]:
v = adc_to_voltage(code)
back = voltage_to_adc(v)
assert abs(back - code) &lt;= 1 # rounding tolerance
# Low-level channel (0-40 mV)
v = adc_to_voltage(128, low_level=True) # ~20 mV
code = voltage_to_adc(v, low_level=True)
assert code == 128