fleetsync

package
v1.1.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 15, 2026 License: Apache-2.0 Imports: 2 Imported by: 0

Documentation

Overview

Package fleetsync decodes Kenwood FleetSync (and FleetSync II) in-band ANI signaling.

FleetSync is the analog in-band data burst Kenwood commercial two-way radios key at the start of a PTT transmission. It carries the transmitting radio's identity — a Fleet number and a Unit ID (the ANI, automatic number identification) — plus status and messaging, on otherwise-analog conventional NBFM voice channels. Scanner operators use it to see *which* radio is transmitting on a system that is otherwise just FM voice, the same role MDC1200 plays for Motorola.

On the air it is a 1200-baud FFSK burst (CCIR tones: mark = 1200 Hz, space = 1800 Hz) carried inside the narrowband-FM voice channel — the same modulation class GopherTrunk already demodulates for MDC1200 and MPT 1327, so a DSP front end can reuse internal/dsp/demod.FFSK. This package owns the protocol layer only: a stream of sliced FSK bits is framed by Framer (sync hunt → capture), and each captured frame is handed to DecodeFrame here, which validates it and returns a typed Message carrying the Fleet/Unit ANI.

Frame layout, after a ≥24-bit alternating preamble and the 16-bit sync word 0xA23E (most-significant bit first, read straight off the FSK slicer): the payload begins 4 bits into the captured stream and is two 32-bit words —

word1[31:0], word2[31:16] : data (fleet / unit / status)
word2[15:0]               : the 16-bit block check (CRC)

FleetSync I validates word2's low 16 bits against [fsyncCRC]. FleetSync II adds forward error correction: four consecutive 64-bit blocks, each 16-bit half-word carrying a single-error-correcting code ([fs2ECCRepair]); the corrected nibbles reassemble word1/word2, which are then CRC-checked the same way. The ANI is read identically from the recovered words.

The Fleet/Unit field extraction, the CRC (polynomial 0x6815, parity bit, 0x0002 final term) and the FS-II parity-check / repair tables are the FleetSync framing facts as implemented by the multimon-ng `fsync` decoder, which is proven on air; this is a clean-room Go port of that framing, cross-checked against a working Python reference contributed on issue #437. No third-party source is incorporated.

Verification status: the protocol constants below are pinned to the multimon-ng reference and exercised by reference-literal + single-bit ECC-correction tests, and the DSP front end (internal/radio/fleetsync/ afsk) decodes synthesised FFSK IQ end-to-end — but this decoder has NOT yet been confirmed against a real Kenwood off-air capture. That gate is cmd/gophertrunk's TestFleetSyncReplay (GT_FLEETSYNC_IQ) against the captures contributed on issue #1184; wiring the events bus, storage and the REST/web surface is deliberately staged for after that on-air A/B (issue #437).

Index

Constants

View Source
const (
	// SyncWord is the 16-bit FleetSync frame synchronization word, most-
	// significant bit first, read directly off the FSK slicer.
	SyncWord uint16 = 0xA23E

	// SyncBits is the length of SyncWord in bits.
	SyncBits = 16

	// FrameBits is the number of payload bits captured after the sync
	// word. This is the FleetSync II maximum (a 4-bit lead-in plus four
	// 64-bit ECC blocks); a FleetSync I frame uses only the first
	// fs1FrameBits of them.
	FrameBits = frameOffset + 4*64 // 260

)
View Source
const (
	MinFleet = 99
	MaxFleet = 99 + 0xFF
	MinUnit  = 999
	MaxUnit  = 999 + 0xFFF
)

ANI range the field layout can express: fleet = byte2 + 99 with one byte of headroom, unit = (byte3<<4 | nibble) + 999 with 12 bits.

View Source
const SynthPreambleBits = 32

SynthPreambleBits is the alternating lead-in SynthBurst emits ahead of the sync word — longer than the 24 bits the framer checks so the register is clean when the sync arrives, matching the on-air "3 × 0xAA bytes before sync" observation with headroom.

Variables

This section is empty.

Functions

func ANIWords added in v1.1.4

func ANIWords(fleet, unit int) (word1 uint32, dataHi16 uint16, err error)

ANIWords maps a Fleet/Unit ANI into the word1 data bytes and the word2 high-16 data field — the inverse of newMessage. The remaining data bytes (word1 bytes 0–1, word2's low data nibble and byte 1) are zero; a real burst carries status/message there.

func Dotting added in v1.1.4

func Dotting(n int) []byte

Dotting returns n alternating bits starting with 1 (1,0,1,0,…) — the FleetSync preamble pattern, also useful as a symbol-clock training run ahead of a synthesised burst.

func FS1Payload added in v1.1.4

func FS1Payload(word1, word2 uint32) []byte

FS1Payload renders a FleetSync I capture as DecodeFrame expects it: the 4-bit lead-in, word1 and word2 MSB-first, zero-padded to FrameBits.

func FS2Payload added in v1.1.4

func FS2Payload(word1, word2 uint32) []byte

FS2Payload renders a FleetSync II capture: the 4-bit lead-in, then four 64-bit blocks whose four 16-bit half-words each carry one data nibble inside a valid 15-bit ECC code word — blocks 0–1 carry word1's eight nibbles, blocks 2–3 word2's, in decodeFS2's extraction order.

func SolveBlockCheck added in v1.1.4

func SolveBlockCheck(word1 uint32, dataHi16 uint16) (word2 uint32, ok bool)

SolveBlockCheck finds the 16-bit block check that makes (word1, word2) validate under fsyncCRC and returns the assembled word2. ok is false if no check value satisfies the CRC (never observed; kept so callers cannot silently emit an invalid frame).

func SynthBurst added in v1.1.4

func SynthBurst(fleet, unit int, fs2 bool) ([]byte, error)

SynthBurst assembles the wire bits of one FleetSync I (fs2=false) or FleetSync II (fs2=true) ANI burst: SynthPreambleBits of alternating preamble, the 16-bit sync word MSB-first, then the FrameBits payload. One byte per bit, mark (binary 1) = 1. Feed it to a Framer directly or to demod.ModulateFFSK to produce IQ for the DSP front end.

Types

type Framer

type Framer struct {
	// contains filtered or unexported fields
}

Framer turns a demodulated FleetSync bit stream into decoded Message bursts. It hunts the 24-bit alternating preamble plus 16-bit sync word, captures the FrameBits payload that follows, hands it to DecodeFrame, and invokes the OnMessage callback for each burst.

This is the protocol-side framer only: the DSP layer above it (a 1200-baud FFSK demodulator + slicer, e.g. internal/dsp/demod.FFSK) feeds it one wire bit per Push call. Keeping the framer callback-based and free of the events bus / storage lets it be unit-tested in isolation; the bus/REST/web wiring is a separate, on-air-gated step (issue #437).

Polarity: an FM discriminator can present the burst with either tone sense depending on tuning, so the sync hunt accepts both the sync word and its bitwise complement; when it locks on the complement the captured payload bits are inverted to recover the true data — the same approach internal/radio/mdc1200/receiver uses.

func NewFramer

func NewFramer(onMsg func(Message)) *Framer

NewFramer constructs a Framer. onMsg is invoked once per decoded burst (including CRC-failed bursts, with Message.CRCOK == false, so a caller can choose to surface marginal signals); it must not be nil.

func (*Framer) Push

func (f *Framer) Push(bit byte)

Push feeds one sliced wire bit through the framer. Bits outside {0, 1} are masked to their low bit.

func (*Framer) Stats

func (f *Framer) Stats() Stats

Stats returns the current counters.

type Message

type Message struct {
	Fleet  int    // transmitting radio's Fleet number
	Unit   int    // transmitting radio's Unit ID
	IsFS2  bool   // decoded via the FleetSync II ECC path
	CRCOK  bool   // the block check validated
	RawHex string // hex of the two recovered 32-bit words
	Body   string // one-line summary for logs / panel
}

Message is one decoded FleetSync ANI burst.

func DecodeFrame

func DecodeFrame(raw []byte) (Message, bool)

DecodeFrame decodes the raw FSK bits captured immediately after the 16-bit sync word (one byte per bit; only bit 0 of each is read). It tries FleetSync I first, then falls back to the FleetSync II ECC path when enough bits are present. The bool reports whether the block check validated; a failure still returns the best-effort FleetSync I words with CRCOK=false so a caller can surface marginal bursts.

type Stats

type Stats struct {
	BurstsIn      uint64 // preamble+sync locks
	BurstsBadCRC  uint64 // decoded frames that failed the block check
	BurstsEmitted uint64 // messages passed to OnMessage
}

Stats reports cumulative framer counters for metrics / debugging.

Directories

Path Synopsis
Package afsk is the DSP front end for Kenwood FleetSync: an IQ stream (or already-discriminated FM audio) becomes 1200-baud FFSK audio, then a sliced bit stream, then framed FleetSync ANI bursts via internal/radio/fleetsync.Framer.
Package afsk is the DSP front end for Kenwood FleetSync: an IQ stream (or already-discriminated FM audio) becomes 1200-baud FFSK audio, then a sliced bit stream, then framed FleetSync ANI bursts via internal/radio/fleetsync.Framer.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL