Learning to Speak Icom: OrbitDeck’s WiFi Control Protocol for the IC-705
How OrbitDeck learned to control the IC-705 over Wi-Fi, what it borrowed from icom-lan, where it diverged, and the session logic that finally worked.
OrbitDeck needed the IC-705 to behave like a dependable part of a working system.
That sounds obvious, but it is the difference between dabbling with a radio protocol and having to rely on one. OrbitDeck is a cross-platform amateur-satellite operations dashboard, which means the radio side has to serve real operating flows. It has to support direct validation, pass-driven tuning, APRS work, RX and TX audio handling, PTT control, and some attempt at putting the radio back the way it was when the software is done interfering with it. That is a much less romantic job than “reverse-engineering a protocol”, but it is the more useful one.
This Needed To Work
The IC-705’s WiFi control stack is usable, but it is not especially self-explanatory. If you get the session order wrong, the radio does not usually tell you what it wanted. It usually just stops being helpful. That meant OrbitDeck could not get away with vague folklore about “CI-V over UDP”. It needed a proper account of what the radio expected, in what order, and which bits of the session were merely convenient versus which ones were mandatory.
Credit Where It’s Due
Before getting into OrbitDeck’s own implementation, it is worth being explicit about something up front: icom-lan was genuinely helpful here, and Sergey Morozik’s work on it deserves real credit. There is a large difference between vaguely knowing that a thing is possible and being able to inspect a serious implementation that already treats the protocol like an engineering problem instead of a rumour. icom-lan does that. It is trying to be a reusable library: async APIs, command queueing, discovery, audio support, broader radio control, and a cleaner general-purpose surface over Icom’s LAN behaviour. I benefited from that work directly, and this article would be poorer, and the implementation slower to mature, without it.
Why OrbitDeck Did Not Just Stop There
That said, OrbitDeck and icom-lan are not trying to become the same thing. icom-lan is building a reusable library. OrbitDeck is building a specific application for satellite operation. Those are overlapping problems, but not identical ones. I ended up carrying both a vendored icom-lan tree and a native IcomUdpTransport because I had two jobs to do. One was taking advantage of a broader upstream library where that made sense, especially around audio-capable workflows. The other was owning a narrower transport path that was shaped directly by the behaviour OrbitDeck needed from a live IC-705 in the context of its own software.
That difference matters because it explains why OrbitDeck diverges in a few places. The native transport is there because I needed a control path that matched the software’s own operating model. I needed a clean way to bring up the control session, open the CI-V stream, push commands through reliably enough for pass work, cope with retransmit traffic, and keep the session alive while higher-level logic worried about things like VFO roles, split control, and recommendation-driven retunes. I also needed to prepare the radio for APRS over WiFi, verify that profile state, feed RX audio into a local decode path, key TX deliberately, and restore the radio afterwards in a predictable way.
The Radio Has Opinions
That is where this stops being a generic “how the protocol works” story and becomes an OrbitDeck story. The native transport in OrbitDeck’s app/radio/icom_udp.py is plainly written around a session flow that proved itself against a real IC-705: clean disconnect first, discovery, ready handshake, login, token acknowledgement, status handling, ConnInfo, then a second channel for CI-V data. It is not trying to cover every possible use case. It is trying to do the things I actually needed, then keep doing them reliably. I also kept a threaded wrapper around the vendored icom-lan async API in OrbitDeck’s app/radio/icom_lan_session.py, which is really just a practical way of drawing boundaries around two different approaches.
One of the more revealing parts of the code is that OrbitDeck treats the IC-705 like a network peer with strict expectations about order. The control channel and the serial channel are separate. The radio’s IDs matter. The token flow matters. The ConnInfo exchange matters. The radio-reported ports matter, even if they are often just 50002 and 50003. The keepalive traffic matters. The retransmit handling matters. Once you accept that, the protocol becomes much easier to reason about: a proprietary UDP session wrapper carrying fairly ordinary Icom CI-V once the session has been set up properly.
Where The Pain Was
OrbitDeck’s APRS work makes that especially obvious. If all you want is a bench script that sets a frequency and exits, you can get away with a shallower understanding for quite a while. I was not trying to do that. In OrbitDeck’s app/aprs/service.py, the IC-705 WiFi path has to do more than “connect successfully”. It snapshots state, disables VOX when necessary, selects the right VFO, forces a predictable FM and data-mode profile, equalises the VFOs, opens squelch, configures scope defaults, verifies that the profile really stuck, starts RX audio, feeds that audio into a local decoder sidecar, and then manages TX audio and PTT in a controlled way. That is not the sort of workload that tolerates hand-wavy protocol knowledge for very long.
The other obvious pain point is Doppler. For satellite work, getting the radio to connect once is not enough. The frequencies have to keep moving during the pass, and they have to keep moving without knocking the rest of the control path sideways. OrbitDeck’s radio service has explicit retune logic for that in OrbitDeck’s app/radio/service.py. It works out when a full reapply is needed, when an incremental retune will do, and which side of the pair is actually safe to touch without disturbing the rest of the session.
There is also the less glamorous problem that success is not always neat. Looking back at the notes I made during the process, manual writes could be followed by intermittent timeouts on later readback even when the radio appeared to have applied the write correctly. That is annoying, but it is also a good reminder that in real hardware work, “the command path works” and “every immediate confirmation is tidy” are not always the same sentence. What I ended up building in OrbitDeck reflects that sort of reality. It cares about keeping the working state coherent and moving the session forward rather than assuming the hardware will always behave politely.
Once all of that was clear, the only useful thing left to do was write the protocol down properly.
The Part Where CI-V Is Not The Hard Part
At the broadest level, the IC-705 LAN behaviour OrbitDeck uses is not just “CI-V over WiFi”. It is two layers. First there is an outer proprietary UDP session protocol that handles discovery, authentication, stream setup, keepalive, and retransmission. Inside that, once the session is established, there are ordinary Icom CI-V frames carried over a second UDP channel. If you skip the outer layer and jump straight to CI-V, the radio will usually ignore you.
Every outer packet begins with the same 16-byte header: total length, packet type, sequence number, sender ID, receiver ID. In OrbitDeck that layout is implemented directly in the code, and the packet types that matter are the usual ones for this session flow: data, retransmit control, discovery, ready, disconnect, and ping. The control port is normally 50001. CI-V defaults to 50002. Audio defaults to 50003. The important word there is “defaults”. OrbitDeck starts with that assumption, but still updates the stored CI-V and audio ports if the radio reports different non-zero values back in status traffic.
The control-channel bring-up sequence is stricter than the “just send some commands” version of events people sometimes imagine. OrbitDeck first sends a disconnect packet as a cleanup step, then discovery with “Are You There”, then learns the radio’s ID from the “I Am Here” reply, then performs the ready handshake. Only after that does it send the login packet. That login packet is 128 bytes long and includes the encoded username, the encoded password, and the client name. The credentials are not encrypted in any serious sense. They are obfuscated through a position-dependent substitution table. In practice, that is an inconvenience rather than meaningful security.
Once the radio accepts the login, it returns a session token. OrbitDeck acknowledges that token, then waits for more control-plane state to arrive. In practice, that means an 80-byte status packet and a ConnInfo packet from the radio. The ConnInfo exchange matters more than it first appears to. OrbitDeck captures the radio’s GUID from its ConnInfo, then echoes that GUID back in the host ConnInfo it sends. The icom-lan documentation is explicit that failing to echo the GUID can leave the CI-V port unresolved, and that lined up closely enough with what I was seeing in OrbitDeck that I took it seriously.
OrbitDeck’s host ConnInfo also shows how application requirements shape protocol use. The packet structure itself is not the interesting part. The interesting part is the values. OrbitDeck advertises a specific capability profile, enables RX, leaves TX off by default in the native control path, uses a known codec and sample-rate combination, and announces local CI-V and audio ports that match the project’s own expectations. That is different in flavour from icom-lan, which is much more willing to inhabit a broader, more audio-capable session model. Neither approach is inherently more correct. They are just answers to different jobs.
What Stuck
After the control channel is established, OrbitDeck opens a second UDP channel for CI-V. This second channel has its own shorter bootstrap: disconnect, discovery, ready handshake, then an “open” packet that tells the radio to start the CI-V-over-UDP stream. Once that stream is open, ordinary CI-V frames are wrapped inside Icom’s UDP data packet format with a 0xC1 marker and a small stream sub-header. Inside the wrapper, it is just Icom CI-V: preamble, destination address, source address, command, optional payload, terminator.
OrbitDeck’s higher-level controller code in OrbitDeck’s app/radio/controllers/ic705.py is a useful guide to what “normal” means in practice. Frequency writes are just CI-V 0x05 with a 5-byte little-endian BCD payload. Mode writes are 0x06 with a one-byte mode code. VFO selection is 0x07. Split control is 0x0F. Squelch and scope settings are handled through the usual Icom command families, with the project making some very specific controller choices on top. The hard part of this protocol is not really the inner CI-V bytes. The hard part is getting the outer session machinery right.
The reliability work is where the protocol becomes usable in a real application. OrbitDeck tracks sent packets by their 16-bit sequence number. If the radio requests a retransmit, the transport tries to replay the original packet from a small buffer. If it no longer has that packet, it falls back to sending an idle packet with the requested sequence number instead. The keepalive loops matter too. OrbitDeck runs pings and idle traffic on both channels, renews the token periodically, and handles pings addressed to the wrong receiver ID in a deliberately explicit way. None of that is especially glamorous, but it is what keeps the session alive under normal use.
There is a temptation, when writing up a protocol like this, to claim that the mystery has finally been solved. That would be overstating it. What I ended up with in OrbitDeck is a careful, working account of the IC-705 WiFi control path that proved useful enough to support the software I was trying to build. The packet fields, token flow, ConnInfo exchange, serial-channel open sequence, keepalive traffic, and retransmit handling documented here are the pieces that made the difference between “the radio sometimes responds” and “the radio behaves like part of an application”.
If you want the packet layouts, field offsets, and implementation details written down cleanly, the companion reference page is here: OrbitDeck IC-705 WiFi Protocol Reference. That page is for the packet offsets, byte order, and all the other fussy little details the IC-705 declines to explain politely. This post is for how OrbitDeck went and found them anyway.