Icom Network Protocol Notes (RS-BA1 / "Icom-UDP")

Status: research draft, 2026-08-18. Not yet validated against hardware.

Read PROVENANCE.md before writing a single line of protocol code. The short version: wfview is GPLv3 and must not be copied into a a closed-source product, but two other complete implementations of this same protocol are MIT licensed, so a clean-room exercise is almost certainly unnecessary. That finding changes the plan; do not skip it.

Provenance of these notes

Everything below describes observable protocol behaviour: byte offsets, field widths, message ordering, magic constants. It was assembled by reading wfview (GPLv3) and cross-checking against kappanhang and rigplane (both MIT). Protocol facts are not themselves copyrightable. The implementation that consumes these notes should still be written against the MIT sources and your own packet captures, for the reasons in PROVENANCE.md.

wfview was cloned to a scratch directory outside this repository, deliberately. Do not vendor it here.

---

1. Transport overview

The radio exposes three independent UDP streams. Each is a separate socket pair with its own sequence space, its own keepalive timers and its own retransmission state. They are not multiplexed.

StreamDefault portCarries
Control50001Handshake, login, token, capabilities, stream setup
CI-V50002CI-V frames in both directions (this is where scope data arrives)
Audio50003RX and TX audio payloads

Some newer rigs add a fourth scope stream on 50004. The IC-7300 MK2 does not appear to use it; scope data rides the CI-V stream.

The control stream negotiates the other two. The radio tells the client which ports to use in a status packet, and the client tells the radio which local ports it wants the CI-V and audio streams delivered to. Both sides are therefore port-aware, which matters for NAT.

2. Common header

Every packet on every stream starts with the same 16 byte (0x10) header:

OffsetSizeFieldEndianMeaning
0x004lenLETotal packet length including header
0x042typeLEControl opcode (meaningful for 0x10-length packets)
0x062seqLEOuter sequence number, per stream
0x084sentidLESender's connection ID (client picks a random one)
0x0c4rcvdidLEPeer's connection ID (0 until learned)

Packets are discriminated by length first, then by type. This is unusual and worth internalising: the parser switches on len, and only 0x10-length packets treat type as a primary opcode. A 0x15-length packet is a ping or a CI-V header depending on context, not on a distinct type value.

Fixed sizes in use:

SizePacket
0x10Control (idle, are-you-there, i-am-here, ready, disconnect, retransmit request)
0x14Watchdog
0x15Ping, and the CI-V data header
0x16Stream open/close
0x18Audio, and retransmit-range request
0x40Token request/response
0x42Capabilities header (followed by N x 0x66 radio entries)
0x50Status
0x60Login response
0x66Per-radio capability entry
0x80Login
0x90Connection info / stream request

3. Control opcodes (0x10-length packets)

typeDirectionMeaning
0x00client -> radioIdle keepalive
0x01eitherRetransmit request for the single packet named in seq
0x03client -> radioAre you there
0x04radio -> clientI am here (carries the radio's sentid)
0x05client -> radioDisconnect
0x06client -> radioAre you ready
0x07eitherPing (sent as a 0x15-length packet)

Ping packets carry a reply byte at 0x10 and a 32 bit radio uptime in ms at 0x11. reply == 0x00 is a request, non-zero is the response. The uptime value wraps daily and wfview uses it to estimate one-way path delay; that is a nicety, not a requirement.

4. Timers

These are wfview's chosen values and they work in practice. They are not necessarily what Icom's own client uses.

IntervalPurpose
100 msIdle packet, reset whenever any other packet is sent
100 msRetransmit scan
500 msPing
500 msAre-you-there retry, until "I am here" arrives
500 msWatchdog tick
60 sToken renewal
15 sConnection considered stale if nothing received

The CI-V stream has its own watchdog: if no CI-V data arrives for 2 seconds, the client re-sends the stream-open packet. Treat that as load-bearing, not defensive; radios do stop sending.

5. Connection and login sequence (control stream)

  1. Client picks a sentid and sends are-you-there (type 0x03, untracked) every 500 ms. (See correction 3 below: the identifier is derived from the client's own local address, not random, and the login later fails silently if it is random.)
  2. Radio replies i-am-here (type 0x04). Client records the radio's sentid as its own rcvdid. Are-you-there retries stop.
  3. Client sends are-you-ready (type 0x06, untracked).
  4. Client sends the login packet (0x80 bytes):
  5. payloadsize = sizeof(packet) - 0x10, big-endian
  6. requesttype = 0x00, requestreply = 0x01
  7. innerseq = incrementing auth counter starting at 0x30, big-endian
  8. tokrequest = random 16 bit value the client invents
  9. username at 0x40, password at 0x50, both 16 bytes, both obfuscated (section 6)
  10. name at 0x60, 16 bytes, the client's computer name in plain text
  11. Radio replies with a login response (0x60 bytes):
  12. error == 0xfeffffff means bad username or password
  13. otherwise token at 0x1c is the session token
  14. tokrequest must echo what the client sent, else ignore the packet
  15. a 16 byte ASCII connection type string sits at 0x40. Observed values include FTTH (radio believes it is on a fast link) and WFVIEW (peer is a wfview server rather than a real radio). This string gates codec choice.
  16. Client sends a token packet with requesttype 0x02 to confirm, then repeats every 60 s to renew. The response is a token packet with requesttype 0x05, requestreply 0x02:
  17. response == 0x00000000 means renewal accepted
  18. response == 0xffffffff means rejected, restart the login
  19. Radio sends a capabilities packet: a 0x42 header whose numradios field at 0x40 gives a count, followed by that many 0x66-byte radio entries. Each entry carries the radio's name (32 bytes at 0x10), its audio device name (32 bytes at 0x30), its CI-V address, supported RX and TX sample rate bitmaps, serial baud rate (big-endian), and an identity that is either a 6 byte MAC address plus commoncap == 0x8010, or a 16 byte GUID. Which one is in use is decided by that commoncap value. Both forms must be supported.
  20. Client sends the connection info / stream request (0x90 bytes), requesttype 0x03, containing:
  21. the radio identity copied back verbatim (MAC or GUID)
  22. the session token and tokrequest
  23. the radio name at 0x40
  24. obfuscated username at 0x60
  25. rxenable / txenable flags, rxcodec / txcodec IDs
  26. rxsample / txsample rates (big-endian)
  27. the client's own local UDP ports for CI-V and audio (big-endian)
  28. txbuffer latency in ms (big-endian)
  29. Radio replies with a status packet (0x50 bytes):
  30. error == 0xffffffff means the connection failed. wfview's advice is to reboot the radio, which is a fair signal about how robust this is.
  31. error == 0x00000000 with disc == 0x01 means the radio dropped us
  32. otherwise civport at 0x42 and audioport at 0x46 (both big-endian) name the radio-side ports for the two data streams
  33. Client opens the CI-V and audio sockets and repeats steps 1 to 3 on each.
  34. On the CI-V stream, client sends the open/close packet (0x16 bytes) with data = 0x01c0 and magic = 0x04 to start CI-V flow. magic = 0x00 closes it.

Once connected, the radio also emits a conninfo packet per connected radio, reporting busy, the controlling computer's name and its IP. This is how you detect that somebody else already owns the radio.

6. Credential obfuscation

Usernames and passwords are not hashed or encrypted. They are passed through a fixed 128 entry byte substitution table, position-dependent:

p = ascii[i] + i
if p > 126: p = 32 + (p % 127)
out[i] = table[p]

Maximum 16 characters. The table is a fixed permutation of printable ASCII.

Two things follow. First, this is obfuscation and not security, so the control stream is trivially sniffable on the LAN; anything exposed to the internet needs a real tunnel. Second, the table itself is the one piece of "protocol fact" that looks most like copied expression if lifted verbatim from someone's source. Take it from an MIT source with attribution, or recover it from your own captures. See PROVENANCE.md.

7. Reliability layer

Icom built a retransmission scheme on top of UDP rather than using TCP.

You must implement this. Audio and scope both break visibly without it.

8. CI-V over the network

The CI-V stream is a thin envelope around ordinary CI-V frames. Send a 0x15-length header followed by the raw CI-V bytes:

OffsetSizeFieldEndianValue
0x0016common headerlen = 0x15 + payload length
0x101reply0xC1
0x112datalenLEpayload length
0x132sendseqBEinner CI-V sequence, separate from the outer seq

The payload is a complete CI-V frame, FE FE <to> <from> <cmd> ... FD, exactly as it would appear on the serial wire. This is the key architectural fact: the network protocol does not replace CI-V, it tunnels it. Everything in civ-notes.md applies unchanged over the LAN. The command layer is shared between the USB and network transports, so it can be written and tested over USB long before the UDP work is finished.

9. Scope and waterfall data

Scope data arrives as CI-V command 0x27 0x00 on the CI-V stream. There is no separate binary scope format on this radio.

Frame structure (indices after the 0x27 0x00 command bytes):

Sequence 1 is a header with no pixel data:

Sequences 2 to (max-1) carry 50 amplitude bytes each. The final sequence carries the remainder (25 bytes on the 7300 family).

Over the LAN the whole sweep arrives in a single message rather than being split. wfview detects this by checking sequence == sequenceMax on the first packet. Both paths must be handled since the same code serves USB and LAN.

IC-7300 MK2 numbers, taken from wfview's rig definition:

PropertyValue
Bins per sweep475
Amplitude range0 to 160 (0xA0)
Sequences (serial)11
Receivers1

Amplitude is a single byte per bin, so a full sweep is 475 bytes. Even at 30 sweeps per second that is roughly 14 kB/s before framing. Scope data is not a bandwidth problem on a LAN.

Over USB serial the measured cost is 579 bytes per sweep including the 11 CI-V frame headers and terminators. At 115200 baud with 8N1 that is 5790 bits, giving a hard ceiling near 19.9 sweeps/sec, and 15 sweeps/sec consumes about 75% of the link.

Measured on an IC-7300 MK2 over USB on 2026-08-18, with the radio's scope Sweep Speed set to FAST:

MetricValue
Completed sweeps4.0 /sec
Waveform frames received45 /sec (4.1 sweeps worth)
Serial throughput2.4 kB/s
Link utilisation21% of 115200 baud
Frames lostnone: dropped 0, desyncs 0, orphaned static at 7

Every frame the radio sent was received and assembled. The radio simply does not emit more than about four sweeps per second over CI-V, and FAST sweep speed does not change that. The serial link sits four fifths idle, so neither the cable, the baud rate nor the host is the constraint.

This is the strongest practical argument for the LAN transport. Over the network the whole sweep arrives in one packet rather than 11 CI-V frames, which removes both the framing overhead and the serial ceiling. The USB path is correct and reliable, but its waterfall will always be coarse.

Related scope commands (all under 0x27): scope on/off, data output enable, main/sub select, single/dual, mode (fixed/centre), span, edge selection, hold, reference level, sweep speed. The MK2 rig definition exposes four edge sets across 1.6 MHz, 2 MHz, 6 MHz and wider spans.

10. Audio streams

Audio packets use the 0x18-length header:

OffsetSizeFieldEndianNotes
0x0016common header
0x102identLE0x9781 when payload is exactly 0xA0 bytes, else 0x0080 for TX
0x122sendseqBEaudio sequence, separate space
0x142unused
0x162datalenBEpayload length

Payloads are chunked at 1364 bytes to stay inside a normal MTU. Audio is sent in 20 ms blocks by default.

Codec IDs

IDCodec
1uLaw 1ch 8 bit
2LPCM 1ch 8 bit
4LPCM 1ch 16 bit
8PCM 2ch 8 bit
16LPCM 2ch 16 bit
32uLaw 2ch 8 bit
64Opus 1ch
65Opus 2ch
128ADPCM 1ch

Opus is not available from a real radio. wfview only permits codec IDs 64 and 65 when the login response reported connection type WFVIEW, meaning the peer is another wfview instance acting as a server. Against actual Icom hardware it forces LPCM 16 bit. Plan on uncompressed audio between daemon and radio, and do your own Opus encoding in the daemon before sending to the browser.

Sample rates offered: 8000, 16000, 24000, 48000 Hz.

Practical consequence for bandwidth: LPCM 1ch 16 bit at 48 kHz is 96 kB/s on the LAN leg. Fine on Ethernet, wasteful over WiFi, and a good argument for requesting a lower rate (16 kHz is plenty for SSB) rather than the maximum.

11. Endianness trap

The protocol mixes endianness within single packets. This will cost you an afternoon if you do not tabulate it up front.

Little-endian: len, type, seq, sentid, rcvdid, token, tokrequest, CI-V datalen.

Big-endian: payloadsize, innerseq, CI-V sendseq, audio sendseq, audio datalen, civport, audioport, rxsample, txsample, txbuffer, baudrate, resetcap.

The rule of thumb is that the outer transport header is little-endian and the Icom application-layer fields are big-endian, but there are enough exceptions that you should encode the table above directly in your codec tests.

12. IC-7300 MK2 specifics

PropertyIC-7300 (MK1)IC-7300 MK2
CI-V address0x94 (148)0xB6 (182), confirmed on hardware 2026-08-18
EthernetNoYes
WiFiNoNo
Network protocolNot supportedSupported
Spectrum bins475475
Spectrum amplitude max160160
Receivers11
VFOs22

The MK2 gaining an Ethernet port is precisely why this project is possible. The MK1 had no LAN at all and required a tethered PC. Icom sells RS-BA1 v2 as the official way to use that port, including remote power on and off.

Note that wfview's MK2 rig definition lists 183 commands, identical in count to the MK1, so the CI-V surface is essentially unchanged. The delta is the transport, not the command set.

13. Confirmed against an IC-7300 MK2

Authenticated against real hardware on 2026-08-18. The radio reported:

FieldValue
Radio nameIC-7300MK2
Audio device nameICOM_VAUDIO
CI-V address0xB6, matching what the USB transport uses
Connection typeFTTH
Identity formMAC address, commoncap 0x8010
MAC OUI00:90:C7, registered to Icom Incorporated
Reported serial baud19200
RX / TX sample rate bitmaps0x018B (both)
Control stream round trip~3 ms on a wired LAN

Consequences worth carrying forward:

Corrections to these notes

Three things in the earlier draft were wrong, and each one broke the implementation until fixed. To be clear about what happened: all three were errors in this document, not gaps in what was publicly known. wfview and kappanhang each had them right, and the mistakes were introduced transcribing from those sources into prose.

  1. commoncap is little-endian, not big-endian. Read the wrong way the MK2's 0x8010 comes out as 0x1080, which silently misclassifies a MAC-identified radio as GUID-identified. wfview reads it correctly and compares against 0x8010; this document said big-endian and was wrong.
  2. The tracked sequence counter starts at 1, and the handshake control packets do not advance it. kappanhang sets this explicitly; it was simply not carried across into these notes. Are-you-there uses sequence 0 and are-you-ready uses sequence 1, both untracked; the login is the first tracked packet and must therefore carry sequence 1. Sending it as 2 makes the radio perceive a gap, request retransmission of sequence 1, and stall indefinitely while still answering pings.
  3. The client must bind its local UDP port to the same number as the remote port, and derive its session identifier from its own address. kappanhang does both; neither had been written down here (low 16 bits of the local IPv4 address in the high half, local port in the low half). A random session ID from an ephemeral port completes the are-you-there exchange and then fails silently at login.

Ending a session

Disconnecting and logging out are two different things, and only doing the first leaves the radio believing a client is still attached. Its front panel keeps showing the network connection, and the next attempt is refused until an internal timeout expires, which on this hardware is long enough that operators reach for the power switch instead.

The full teardown is:

  1. Stop CI-V flow with the open/close packet, magic 0x00
  2. Send an auth packet with requesttype 0x01, which logs the session out
  3. Wait around 500 ms with the read loops still running, because the radio frequently asks for a retransmit of the logout and answering it is the difference between a clean release and a stuck session
  4. Send the transport disconnect (type 0x05) on every open stream, twice each
  5. Let the datagrams leave before closing the sockets

The three auth magics are 0x02 to confirm a fresh login, 0x05 to renew the token, and 0x01 to log out. Only the first two are needed to connect, which is why the third is easy to miss.

What is actually new here, and what is not

An honest accounting, because it is easy to mistake rediscovery for discovery.

Already public, in wfview or kappanhang source code. The packet layouts, the login and token sequence, the credential obfuscation table, the scope encoding, the requirement to answer retransmit requests, the logout step, the local port binding, the session identifier derivation, s16le audio, and the IC-7300 MK2's CI-V address of 0xB6, which came from wfview's own rig definition. None of this was worked out here. It was read, transcribed, and in three places transcribed wrongly.

Plausibly not written down anywhere, though all small.

The actual contribution is synthesis, not discovery. This document is the protocol written as prose in one place, derived from two implementations in two languages plus a radio to check it against, and verified on a model neither reference supports. That has value. It is a different and smaller claim than finding out something nobody knew.

Sample rates are advertised as a bitmap, not a rate

The capabilities entry carries two 16-bit fields, one for receive and one for transmit, at offsets 0x53 and 0x55 of the radio's entry. They are not sample rates. They are bitmaps of the rates the radio accepts, one bit per rate in ascending order:

BitRate
08000
111025
212000
316000
422050
524000
632000
744100
848000

An IC-7300 MK2 reports 395 for both directions, which is 0b110001011, meaning 8000, 11025, 16000, 44100 and 48000 are accepted while 12000, 22050, 24000 and 32000 are not. The two fields being separate implies a radio could accept different rates in each direction, though this one does not.

This is worth stating plainly because the field name invites reading it as a rate, and 395 is a plausible enough number that the mistake is not obvious. The mapping is inferred from one radio and would benefit from confirmation against others.

Solved: transmit audio needs the right source address

Transmit audio works. It was silently broken for a whole afternoon by a VPN, and the failure is worth recording because nothing in it looked like a network problem.

The symptom. Audio reached the radio and the radio did not modulate it. The transmitter keyed, and power, ALC and compression stayed at zero. Control worked perfectly. Receive audio worked perfectly, at the negotiated rate, measured. The packets were accepted at the socket and discarded.

The cause. The client's sockets were sourced from a Tailscale address rather than the address on the radio's own subnet. A tailnet carrying a route that covers the radio's network, from a subnet router or an exit node, wins over the local ethernet, and a UDP socket bound to the wildcard address then gets the tunnel's source address. The radio associates a session with the address it was established from, answers control traffic and sends receive audio regardless, and refuses to modulate transmit audio arriving from anywhere else.

Why it was hard. Every visible signal said the network was fine, because for every other purpose it was. The failure is silent, applies to one direction of one stream, and looks exactly like a malformed packet or an unset radio menu item. A great deal of effort went into the packet format, which was correct from early on.

The fix. When the radio is on a directly attached subnet, bind to that interface explicitly instead of letting the routing table choose. Interfaces that are down, loopback or point to point are skipped, which is what excludes the tunnel. The chosen local address is logged, so the next person can see it.

If you are debugging something similar: check the local address in the error text of any timeout, and in the "control socket bound to" line. If it is not on the radio's subnet, that is the bug.

Known gaps after the 2026-08-18 review

Two of the three gaps that review recorded are now closed, on 2026-08-19:

Still open:

14. Scope over LAN, measured

Measured on an IC-7300 MK2 on 2026-08-18, the same radio and the same 40 metre band segment as the serial measurement:

USB CI-VLAN
Sweeps per second4.030.0
Frames per sweep111
Throughput2.4 kB/s14.6 kB/s
Bytes per sweep~600~487
Dropped, desyncs, orphaned00

The network transport is 7.5 times faster and this is the whole argument for it. Two effects compound: the whole sweep arrives in a single packet instead of 11 CI-V frames, removing the per-frame overhead, and the 115200 baud ceiling disappears entirely.

487 bytes per sweep is 475 amplitude bins plus around 12 bytes of framing, which confirms directly that section 9's single-packet path is what the radio uses over the network.

The radio's scope engine is therefore capable of 30 sweeps per second. The 4 per second seen over serial was never the scope; it was the transport.

A measurement trap worth remembering

Two link statistics were transport-specific and quietly wrong once the network path worked:

Both are now derived rather than assumed. Frames per sweep is scopeFramesPerSec / sweepsPerSec, which is 11 on serial, 1 on LAN, and climbs above either only when frames really are failing to complete a sweep.

15. Still open

  1. Does the MK2 use the fourth scope port (50004) under any configuration, or is scope always on the CI-V stream?
  2. How do the 0x018B sample rate bitmaps map to actual rates? The IC-705 reports 0x01FF, so the field is a bitmap rather than an enum, but the bit assignment is unconfirmed.
  3. Remote power-on, which RS-BA1 advertises and which is not in any path read so far.
  4. How does the radio behave when a second client connects while one holds the session?