Open890: A web-based ham radio UI built with Phoenix LiveView

I was asked in another thread to share some details about my project so here goes!

open890: A web-based ham radio remote control app built with Phoenix & LiveView.

Introduction

open890 is an open source, browser-based remote control app for the Kenwood TS-890 ham radio. It is written in Phoenix and LiveView. Binary builds are also provided for Ubuntu and Windows.

The TS-890 is an HF (high-frequency, think shortwave) ham radio transceiver. The radio provides connectivity to a computer via a USB serial or ethernet connection. It also has a well-documented command interface supplied by the manufacturer. Once properly connected, the radio can be told to push data to the client as things change (think: knobs turned, buttons pushed, etc), as well as be completely remotely controlled via software.

Front display of the TS-890

Screenshot of the onboard display. The radio is tuned in to a pirate shortwave station playing music.

The radio can also send high-speed spectrum data to clients, as well as stream audio to and from the radio via PCM samples streamed over UDP. While not strictly open source, the radio’s programming interface is quite nicely documented, and almost every function of this radio is remotely controllable – this includes changing frequency, reading various meters in real-time, accessing menu items, etc.

My main goal with this project was to build a web-based UI that allowed me to use it as a companion on my desk while I operate the radio. The software provided by Kenwood does not provide a very good user experience, and left a lot of room for improvement.

General Architecture

open890 is a Phoenix application, with a small collection of supervisors and genservers for maintaining a TCP connection to the radio. There is a RadioConnectionSupervisor which uses DynamicSupervisor to maintain connections to radios.

From there, the RadioConnectionSupervisor starts and stops TCPClients which use :gen_tcp and handles various socket state management, and knows how to “sign in” to the radio and start the various data streams. Elixir (and :gen_tcp) are very well suited to this task, and the callback-oriented style of reacting to incoming messages has made it a very natural fit for interfacing with the radio.

Once the connection is properly started, individual messages from the radio are broadcast to the app’s endpoint and picked up on the other side in the main LiveView. On the LiveView side, broadcast messages are parsed out, funneled through a single Dispatch.dispatch function, albeit with many many function heads.

Following the Single-Responsibility Principle here has paid off, with perhaps 50-ish odd messages currently implemented, it’s very easy to add new messages as I build out features. Dispatch's single responsibility is to determine which messages are valid (i.e., the ones I have implemented), use the Extract module to get a usable data structure from the message in question, and assign the result to the socket. Extract's sole responsibility is to take a string message from the radio and turn it into a useful piece of data for the LiveView.

def dispatch("KS" <> _ = msg, socket) do
  key_speed = Extract.key_speed(msg)
  socket |> assign(:cw_key_speed, key_speed)
end

Example dispatching the “key speed” message that determines how fast the radio sends morse code.

High-speed spectrum scope via SVG & HTML Canvas

One of the challenges with the project was the high speed of data, particularly over the ethernet connection. The TS-890 has two different spectra streams, the main one being a list of 640 integer values representing the radio spectrum that the radio is currently receiving. At its highest data speed, the radio sends 30 updates per second. These 640 values can represent a span of between 5 and 500 kHz of radio bandwidth, and so at lower spans, you therefore see a more detailed picture of the radio signal.

Once parsed out into a usable list of values, the list of values for the spectrum are split off into two directions. One, they are simply set as an assign on the socket. In the LiveView template, I render an SVG, and use a couple of small helper functions to translate the values to SVG coordinates. Modern browsers are pretty good at drawing SVG quickly, and to make it work, you can simply render e.g. a <polygon> element and dynamically update the points inside it:

<polygon id="bandSpectrum" class="spectrum"
  points={RadioViewHelpers.scope_data_to_svg(@band_scope_data)} />

Here @band_scope_data contains that list of spectra values, and RadioViewHelpers.scope_data_to_svg just transforms it to the specific SVG coordinates. It’s that easy!

Secondly, to draw the “waterfall” display (the area under the spectrum), I create an HTML canvas element, and use LiveView hooks to subscribe to the spectrum data. For each new line of spectrum data, I shift the current set of pixels in the canvas down one line, and draw a new line of pixels at the top. Instead of something that looks like a chart, the waterfall simply maps the signal strength (the height in the spectrum display) to a color, using the HSL color space. “Low” signal levels are blue, around to green, then yellow, and red.

The end result is an animated display that updates quickly. The screenshot here doesn’t quite capture the full speed of updates, but here’s the general idea:

LiveView allows you to change any HTML element or attribute very quickly (and efficiently!), and with SVG this is extremely powerful. Browsers animate changes to coordinates, and so any bit of data that you can translate into screen coordinates, you can easily animate. I use basic SVG shapes to draw and animate grids, polygons representing the exact frequencies the radio is tuned to, audio filter characteristics, and so on.

The final result. Frequency readout, spectrum display, waterfall, and various meters all update in real time, thanks to Phoenix and LiveView.

Future updates

I have a laundry list of features to keep implementing before I release version 1.0, and I’ve been slowly working on the app in some fashion over the past year in my free time.

I’ve also managed to reverse engineer the audio data stream that the radio sends, and I’ve managed to get the audio playing in the browser - all using Phoenix.PubSub etc. I hope to eventually integrate that work into the main codebase and get it to a bunch of eager ham radio operators who are using the app :slight_smile:

Code

The code is available at GitHub - tonyc/open890: A web-based remote UI for the Kenwood TS-890S ham radio transceiver., although without a physical radio on your desk, you will probably not be able to start it up and really check it out.

There’s a lot more to the app (I could go on and on for pages, magazine-style), so if you’ve got any questions on general approach, lessons learned, etc, feel free to ask, and I’ll do my best to answer!

45 Likes

WOW! Thats amazing!

4 Likes

What @kip said:

Thank you for making this and sharing! :heart:

2 Likes

Thanks! This is an awesome project :slight_smile:

I imagine this app, or instances thereof, only have a relatively small number of concurrent users. I hope to try a similar dynamic visual UI for some of my own projects and test how well that works with lots of simultaneous users.

Yeah. In reality, it’s typically only one person at a time, but with multiple browser windows open, they all stay synced together, and one person making changes causes the other clients to see the updates as well.

The app also supports multiple connections to radios, so in theory if you had a bunch of them going at once, it would maintain all the connections.

2 Likes

Impressive. I’ve been dealing with Hamlib, flrig, and other stuff (albeit mostly in C++), but making the complicated stuff in Elixir/Liveview will make things more streamlined and better. 73 de Kenji Rikitake, JJ1BDX

Neat. I’m just starting to learn how to use Rust, and wrap C libraries using bindgen. I wonder about something like: Hamlib → Rust → Rustler → Elixir

Getting access via rigctld would be a good starting point.

Love this!!! I wanted to do something similar after reading Explore Software Defined Radio but I never got around to it. Great work!

Wow, thanks so much for sharing this Tony. Looks really cool and such a great use case for LV. I will definitely check out the code some more :nerd_face:

1 Like