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.
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.
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.
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.
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.
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.