OrbitDeck needed the IC-705 to do considerably more than respond to the occasional CI-V command. During a satellite pass it has to stay connected while OrbitDeck keeps adjusting frequencies for Doppler, and the same radio also needs to handle APRS, audio, PTT, split operation and the various state changes that go along with all of that. When OrbitDeck is finished, I also want it to put the radio back the way it found it rather than leave me wondering what it changed.

The IC-705 can do all of this over WiFi, which initially made the job look fairly straightforward. I already knew CI-V, after all, so I expected the network side to be mostly a matter of getting those commands to the radio. It did not take very long to discover that this was the wrong mental model. CI-V is certainly there, but it sits inside a much larger Icom LAN session, and before the radio will accept an ordinary command the client has to discover it, establish the right IDs, authenticate, negotiate the session, open a second channel for CI-V and then keep the whole thing alive. If something goes wrong along the way, the IC-705 is not especially interested in explaining which bit offended it. It will quite often just stop responding.

That was the point where this stopped being a matter of getting a few commands through and became something I actually needed to understand.

From icom-lan to OrbitDeck

Fortunately, I did not have to start with a blank sheet of paper. Sergey Morozik’s icom-lan project was enormously useful here because it already treated Icom’s LAN behaviour as a complete protocol rather than “CI-V, but on a network”. It handles discovery, authentication, command queuing, audio and the surrounding session behaviour together, so being able to read through a working implementation gave me a much better starting point for understanding what the IC-705 was expecting.

OrbitDeck still carries a vendored copy of icom-lan, and I still use it where its broader implementation makes sense, particularly for audio-capable workflows. As OrbitDeck developed, though, I also found myself wanting a smaller transport that fitted its own operating model more directly, which is how app/radio/icom_udp.py came about.

The native IcomUdpTransport is deliberately narrower than icom-lan. Its job is to own the control and CI-V path OrbitDeck uses constantly: bring the session up, open the CI-V stream, send commands, keep the connection healthy and deal with retransmission traffic while the rest of the application worries about things such as VFO roles, split operation and Doppler retuning. I kept the broader icom-lan path alongside it, wrapped in app/radio/icom_lan_session.py, because there was no particular benefit in reimplementing everything for the sake of it. The native transport does not try to become a complete audio stack, so when OrbitDeck needs the audio-capable session used for APRS, the vendored icom-lan implementation is still there.

That division ended up making sense in practice. OrbitDeck owns the narrower control path it depends on all the time, while icom-lan remains available for the parts of the protocol where its wider implementation is useful. Once I had made that decision, I could concentrate on making the native path behave properly against the actual IC-705 rather than trying to recreate a general-purpose library.

Establishing the session

The first thing that became obvious was that the radio cares quite a lot about session state. OrbitDeck starts a control connection by sending a disconnect packet, which looks backwards until a previous connection leaves enough state behind to make the next one behave strangely. Clearing that first made setup much more predictable, so it became the first step in a new connection.

After that comes Icom’s Are You There discovery packet. The IC-705 replies with I Am Here, and that response gives OrbitDeck the radio-side ID for the channel. Those IDs are not just decorative values in a header. They have to remain consistent through the session, with sender and receiver swapped correctly in replies, so learning the radio ID is part of establishing the conversation rather than merely discovering that a radio exists.

Once that ID is known, OrbitDeck performs the ready handshake and sends the login packet. The login itself is 128 bytes and contains the username, password and client name. Icom does not send the credentials as plain ASCII, although I would hesitate to call the alternative encryption. They are passed through a position-dependent substitution table, which makes them less immediately obvious in a packet capture but does not amount to meaningful cryptographic protection.

If the login succeeds, the radio returns a session token. OrbitDeck acknowledges that, and the rest of the control-plane information then starts arriving, including an 80-byte status packet and the radio’s ConnInfo. This is where a few things I had initially thought of as fixed settings turned out to be session information. The familiar ports are 50001 for control, 50002 for CI-V and 50003 for audio, but OrbitDeck does not blindly assume that the last two will always remain there. The radio can report non-zero CI-V and audio ports in its status traffic, and if it does, OrbitDeck follows what the radio actually says.

I found that to be a useful rule for the protocol generally. Where the IC-705 gives you a value, it is safer to use that value than to assume the usual default must always be right.

ConnInfo was another example of something that initially looked less important than it turned out to be. The radio includes a 16-byte GUID in the packet it sends, and OrbitDeck echoes that GUID back in its own host ConnInfo. The icom-lan documentation notes that failing to do this can leave the CI-V port unresolved, which matched what I was seeing closely enough that I stopped treating the GUID as incidental metadata.

The host ConnInfo also tells the radio what sort of session OrbitDeck wants. In the native path that means enabling RX, leaving TX disabled by default, supplying the codec and sample-rate values OrbitDeck expects and announcing the local CI-V and audio ports. Those are application choices rather than universal Icom requirements, which is one reason I have kept the exact values and packet offsets in the separate protocol reference rather than reproducing a specification here.

Opening the CI-V channel

Once that exchange is complete, the control side is established and OrbitDeck can move on to the second UDP channel that actually carries CI-V. That channel has its own shorter discovery and ready sequence, after which OrbitDeck opens the serial stream.

This is the point where the radio-control side becomes much more familiar. CI-V frames are carried inside Icom UDP data packets with a 0xC1 marker and a small stream sub-header, but once that wrapper is stripped away you are back to ordinary Icom CI-V:

FE FE → destination → source → command → optional payload → FD

OrbitDeck’s higher-level IC-705 controller lives in app/radio/controllers/ic705.py, and most of what happens there is conventional Icom control. Frequency writes use command 0x05 with a five-byte little-endian BCD value, mode writes use 0x06, VFO selection uses 0x07, and split control uses 0x0F. Squelch, scope settings and the rest sit in the usual Icom command families as well.

There are still some traps, particularly around encoding. The wider LAN protocol has a fairly mixed attitude to byte order: most of the outer header is little-endian, while some internal sizes, reported ports and stream sequence values are big-endian. After a while I stopped trying to think of a whole packet as belonging to one convention and just treated byte order as something each field needed to declare for itself.

Even with those quirks, though, CI-V itself was not where most of the difficulty lay. Once the stream was open, the commands were mostly familiar. The harder part was making sure the surrounding session was reliable enough for OrbitDeck to use the radio continuously rather than just prove that it could talk to it once.

APRS and radio state

APRS was where that became obvious. The APRS service in app/aprs/service.py cannot simply connect, set a frequency and disappear because it may be taking over a radio that is already configured for something else. It first has to preserve that state and then put the IC-705 into a known packet-radio configuration, which means disabling VOX where necessary, selecting the right VFO, setting the expected FM and data modes, equalising the VFOs, opening squelch and setting up the scope before audio ever enters the picture.

I also did not want OrbitDeck to assume that sending those commands meant the job was done. The profile matters to what follows, so the important parts are checked before the APRS workflow continues. Once the radio is actually ready, OrbitDeck can start RX audio and feed it into the local decoder sidecar; transmitting then adds TX audio and deliberate PTT control. When all of that is finished, the saved radio state can be restored.

That workload is also why retaining icom-lan for the audio-capable path made sense. I wanted the native transport to own the control path without turning it into another complete implementation of everything icom-lan already knew how to do.

Doppler and readback

Satellite Doppler correction puts a different sort of pressure on the same connection because the radio cannot be configured once and then left alone. Frequencies are moving throughout the pass, sometimes often enough that repeatedly applying the entire radio configuration would be both unnecessary and clumsy. OrbitDeck handles that in app/radio/service.py, where it decides whether a particular change actually needs a full reapply or whether an incremental retune will do.

To make that reliable, the IC-705 controller also treats VFO A and VFO B as stable identities rather than thinking purely in terms of whichever VFO happens to be selected at the time. OrbitDeck explicitly selects VFO A when it connects and returns to it after operations where that matters, so the higher-level satellite logic has a predictable idea of which part of the radio it is changing. That is an OrbitDeck design choice rather than something required by Icom’s LAN protocol, but it grew directly out of trying to use the radio continuously rather than occasionally.

The same was true of readback. During development I saw cases where the IC-705 had clearly applied a requested write but a subsequent query would time out. Initially that looks as though something has failed, but the radio itself has already told you otherwise by changing state, so “the write succeeded” and “the next read will confirm it neatly” cannot always be treated as the same event.

OrbitDeck therefore keeps a coherent working view of the radio while still verifying the state that genuinely needs verification. APRS setup is a good example because the FM and data-mode configuration matters to the whole workflow; a routine Doppler adjustment does not need the same level of suspicion simply because one readback was untidy. That was something I only really appreciated once the software was spending enough time talking to a real radio.

Keeping the connection alive

The other thing a real application exposes fairly quickly is that completing the handshake does not mean you can forget about the network session. Both the control and CI-V channels need maintenance traffic, so OrbitDeck sends pings and idle packets while periodically renewing the authentication token. The native transport currently uses a three-second ping interval, a one-second idle interval, a five-second pending-request resend interval and a 60-second token-renewal interval.

The outer UDP protocol also tracks packets with 16-bit sequence numbers, and the IC-705 can explicitly ask for packets it believes it has missed. OrbitDeck therefore keeps a small replay buffer. If the requested packet is still there, it sends that exact packet again; if it has already dropped out of the buffer, OrbitDeck sends an idle packet with the requested sequence number so the sequence can continue cleanly. It also handles pings addressed to the wrong receiver ID explicitly rather than accepting them and hoping nothing strange follows.

None of that changes the frequency or mode command OrbitDeck ultimately wants to send, but it is the sort of detail that starts to matter once the connection has to remain usable throughout an actual pass rather than survive a short bench test.

Shutting down cleanly

Shutdown turned out to deserve the same attention. OrbitDeck closes the CI-V side first with a disconnect and close-stream packet, then tells the control session that RX and TX are disabled before sending the final disconnect and token teardown. Given that the radio appears capable of retaining some session state between attempts, cleaning up properly has made subsequent connections much less temperamental, which also explains the apparently odd disconnect at the beginning of a new session.

A more useful mental model

By the time I had all of this working, I had mostly stopped thinking of the IC-705 network interface as “CI-V over WiFi”. CI-V is certainly the bit that ultimately controls the radio, but it is really the inner layer of a larger session protocol that deals with discovery, authentication, IDs, session state, stream setup and keeping the connection alive. Once that distinction is clear, the way OrbitDeck is structured makes more sense as well: the higher-level controller can mostly concern itself with ordinary IC-705 behaviour, while the transport deals with the stranger parts required to get those CI-V frames to and from the radio reliably.

I would not claim that what I have documented is every corner of Icom’s LAN protocol, nor would I assume that everything I have observed on this IC-705 applies unchanged to every Icom radio or firmware revision. What I do have is the path OrbitDeck actually uses successfully, along with enough of the surrounding behaviour to explain why the implementation looks the way it does.

For anybody trying to implement that path, rather than read through how I got there, the companion OrbitDeck IC-705 WiFi Protocol Reference is where I’d recommend you go. That is where I have put the wire-level detail, including the actual handshake, packet structures, authentication flow, ConnInfo, CI-V framing, keepalive behaviour and the various byte-order traps that are much easier to deal with once they are written down in one place.

This post is really the context around that reference. It explains what OrbitDeck needed from the IC-705, where the protocol became awkward in practice, and why I ended up structuring the implementation the way I did.