Programming Kenwood Radios with Bluetooth from Android

Recently I’ve been writing a programming tool for Kenwood ham radios for Java and Android, one which happily programs my favorite four hundred channels over RS232, TCP, and Bluetooth; in this friendly article, I’ll tell you a little bit about how to write your own.

It is available in two parts: CodePlug Tool is a portable library for Java with a minimal command-line frontend, while CAT Tool is an Android app that uses this library. I’ve been using it to sync hundreds of memory entries between the Yeasu FT-991A in my shack, the TM-D710G in my Studebaker, and the TH-D74 in my laptop bag.

If you’d rather just install the app, it’s available for free as Goodspeed’s CAT Tool in the Play Store.

Main menu on Android.

CAT Protocol Basics

Kenwood radios use an undocumented ASCII protocol that varies for each model, terminated by a newline. The best documentation available is in LA3QMA’s github repositories for the TH-D74, TH-D72, and TM-D710, which come from folklore and reverse engineering.

Each command consists of a two or three letter verb, sometimes followed by a space and parameters. Some of these are very short and sweet, such as sending CS to ask for a callsign and CS KK4VCZ to set the radio to my callsign. ME 042 reads memory entry 042, and writing that same

Complications arise when we try to write code that works on multiple radios. Each model adds additional fields for its features, so that respond differently even to commands that they have in common. The exact meanings of these fields can be found in the LA3QMA docs, but for now just note that they are different and that fields which have no meaning, such as the destination DSTAR callsign in this analog repeater configuration, always have some contents even if it isn’t very useful.

# TM-D710
ME 010,0145370000,0,2,0,1,0,0,12,12,000,00600000,0,0000000000,0,0
# TH-D72
ME 010,0145370000,0,2,0,1,0,0,0,12,12,000,0,00600000,0,0000000000,0,0
# TH-D74
ME 010,0145370000,0000600000,0,0,0,0,1,1,0,0,0,0,0,2,12,12,000,0,CQCQCQ,0,00,0

Another frustrating difference is that some commands don’t exist on all radios, so that we can use the MN command to read and write memory names on the TM-D710 and TH-D72, but there is no similar command for the TH-D74. I’ve confirmed the absence of this command by reverse engineering the firmware, although there are many new commands in the calibration mode.

A Portable Java Library

Now that we know the protocol, it’s tempting to jump straight to programming an Android app, but doing that will lead to frustration and heartache. Instead, it’s better to first design and test a standalone library for Java that runs on a desktop, so that we can know the library is stable and reliable before debugging in the phone’s GUI.

We begin with an interface named Radio to read, write and delete channels. This is extended by the CATRadio and ImageRadio interfaces to offer functions unique to radios which are CAT programmed, like these Kenwoods, and radios which are image programmed, such as the Baofengs and Tytera MD380. Never hurts to plan ahead.

package com.kk4vcz.codeplug;
import java.io.IOException;

public interface Radio {
  public void writeChannel(int index, Channel ch) throws IOException;
  public Channel readChannel(int index) throws IOException;
  public void deleteChannel(int index)  throws IOException;
	
  public String getVersion() throws IOException;
  public String getSerialNumber() throws IOException;
	
  public int getChannelMin() throws IOException;
  public int getChannelMax() throws IOException;
	
  public long peek32(long adr) throws IOException;
}

Radio classes are constructed around InputStream and OutputStream connections, so that connections can be made through a variety of sockets, rather than just serial ports.

package com.kk4vcz.codeplug.radios.kenwood;

public class THD74 implements CATRadio {
  public THD74(InputStream is, OutputStream os) throws IOException {
    ...
  }

In my shack, I run a TCP server for each radio’s serial port, so that my phone can reprogram them over the local network. The stty command sets the baud radio, which is necessary when socat has no concept of baud rates.

stty -F /dev/ttyS1 57600
socat tcp-l:54321,reuseaddr,fork file:/dev/ttyS1,nonblock,raw,echo=0

For the channels themselves, I have an interface named Channel that presents setter and getter functions that are common for all ham radio channels.

public interface Channel {
  //Channel number in memory.
  int getIndex();
  void setIndex(int i);
  
  //Name
  String getName();
  void setName(String n);
  
  //Frequency in Hz; split is figured out by the radio driver.
  void setRXFrequency(long freq);
  long getRXFrequency();
  long getTXFrequency();
  String getSplitDir();//+, -, "simplex", or "split"

Within a given radio, the functions that read and write channels do so by duplicating the Channel they are presented with into the local radio’s format, a class that implements the standard functions with the local radio’s own variable names. Seen here, the TMD710GChannel class implements the matching variable positions from the LA3QMA documentation of that radio’s ME Command. This makes it easy to render the write-back string that must be sent to the radio.

public class TMD710GChannel implements Channel {
  /* Like other Kenwoods, the LA3QMA page is the best source 
   * for the channel format.
   *
   * ME p1,p2,p3,p4,p5,p6,p7,p8,p9,p10,p11,p12,p13,p14,p15,p16
   */
  
  int p1=0; //Memory channel number.
  long p2=146520000; //RX Frequency
  int p3=0; //rx step size.
  int p4=0; //shift direction
  int p5=0; //reverse
  int p6=0; //tone status
  int p7=0; //ctcss status
  int p8=0; //dcs status
  int p9=8; //tone frequency
  int p10=8; //CTCSS frequency
  int p11=47; //DCS frequency
  long p12=600000; //offset frequency in Hz, 8 digits
  int p13=0; //mode
  long p14=0; //TX freq in Hz, 10 digits
  int p15=0; //TX step size;
  int p16=0; //lock out
  
  String name="";

Now that we have drivers for a few radios, and classes to manage their channels, we also need a convenient source of memories to be uploaded or downloaded into the radio. For this, I wrote a driver to match CHIRP’s CSV format, as it seemed a reasonable standard between many different radios.

% java -jar CodePlugTool.jar
Usage: 
cpt [driver] [device/hostname:port] [verbs]

Drivers:
        Kenwood
                d72  -- TH-D72 Dual-Band HT
                d74  -- TH-D74 Tri-Band HT
                d710 -- TM-D710 Mobile
        Yaesu
                991a -- Yaesu FT-991A
        Others
                csv  -- Chirp's CSV format.
Ports:
        ttyS0   -- Physical Port S0
        ttyS2   -- Physical Port S2
        ttyS1   -- Physical Port S1
        ttyUSB1 -- USB-to-Serial Port (cp210x)
        ttyUSB0 -- USB-to-Serial Port (cp210x)
Verbs:
        info             -- Prints the radio's info.
        dump             -- Dumps the radio's channels to the console.
        upload foo.csv   -- Uploads a CSV file from CHIRP to the radio.
        download foo.csv -- Downloads a CSV file from the radio.
        raw 'ME 000'     -- Runs a raw command and prints the result.
Examples:
        java -jar CodePlugTool.jar d710 ttyS1 info
        java -jar CodePlugTool.jar d74 localhost:54321 info

At this point, we’ve got a functioning library that runs standalone from a .jar file, and it becomes easy to write regression tests that copy memories back and forth between radios.

A Polished Android App

Because we were careful not to use any features introduced after Java 8, or to depend upon any classes not available in Android, it can be easily imported as a dependent library into our app.

The Android interface works much like our CLI, but there are some complications of GUI programming that we must reckon with. First, all networking must happen outside of the main GUI thread, displaying results and progress by posting events back to the main thread. I was a little lazy with this, using public static methods to keep a global state of the current working radio.

Second, it’s necessary to declare our permission requirements in AndroidManifest.xml, so that we can use the network and open sockets to Bluetooth devices.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
   android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission
   android:name="android.permission.BLUETOOTH" />
<uses-permission
   android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission
   android:name="android.permission.ACCESS_COURSE_LOCATION" />

Because we were careful to have our radio drivers work around InputStream and OutputStream, we can use the android.bluetooth package for connecting to the TH-D74’s internal bluetooth modem directly, without having to pretend that it’s a serial port. See BTConnection for the connection and SettingsFragment for the user’s selection of an actively paired Bluetooth RFCOMM device.

Selecting a radio.

Now that we have our networking in its proper thread, and we have permission to use it, and we have a GUI for selecting the target address or Bluetooth device, it’s finally time to yank the frequencies out of the radio and display them in an Android RecyclerView, which allows for smooth scrolling without having to display every frequency at once. This is implemented in CodeplugFragment, with the view adapter as CodeplugViewAdapter.

Downloading a codeplug.

We need to be able to edit a channel. This part gets a bit ugly in code, but in general, an EditFragment is created as a Dialog whenever a channel is clicked.

Channel editing dialog for W4KEV repeater.

And finally we need a database source. Since we already support import and export from CHIRP’s format, we can politely re-use its API servers.

Querying RepeaterBook.

Next Steps

So as of early September 2020, I have a portable library for programming Kenwood radios with both a CLI interface that runs on my desktop and a GUI interface for Android, but plenty of work is left to be done.

First, the TH-D74 lacks the MN command that reads and writes a memory’s name in older Kenwoods. I’ve been reverse engineering the TH-D74 firmware in hopes of finding an equivalent, but without much luck. The remaining options are to use the memory image style of editing the settings as commercial tools do or to patch that firmware, as we did on the MD380.

Second, documentation is needed for wiring Bluetooth adapters into radios which lack them, such as the TM-D710 and TH-D72. A few short wires and a bluetooth module could retrofit these radios to be programmable by phone.

And third, it would be handy to have support for all those other radios out in the world, but except for the ones in shack, I don’t have much motivation. Perhaps you do? Pull requests, forks and rewrites are welcome.