Thursday, February 05, 2026

Xiegu X6200 review

Keen to do more field operating (and overnight camping). My rig of choice has been the Elecraft KX3 with it's excellent antenna tuner and easy to read LCD display. Since using rigs with a waterfall display of the band I find it hard to going back to tuning up and down trying to find stations. 

I purchased a Xiegu X6200 from AliExpress for AU$1200. 


It has some terrific features for portable operation:

  • Decent flat battery pack that clips on the back
  • Wonderful sharp and bright display
  • Excellent built-in antenna tuner
  • USB-C socket with interfaces for CAT control and audio
  • Handles to protect the knobs
The front facing speaker is small but quite effective. Surprisingly it is NOT a touch screen (although if you connect a mouse you get a mouse pointer). This means that to go through menus you must spin the outer lower left knob to move between settings and spin the inner knob to change a setting.

A waterfall with a touch screen is a wonderful thing and on a 7300 being able to tap a signal to tune there is missing here. Tuning with the knob is a little laggy and seems to have some inertia for some reason. (Unfortunately clicking on the waterfall with a mouse isn't supported).

The rig is certainly a quality build. All the controls feel excellent and the battery pack, sadly proprietary, clips on to the back. It's remarkably compact and solid feeling. The microphone has a full set of buttons including three configurable buttons for things like NR or NB. You can also direct enter frequencies. There is also a built-in microphone and PTT button so you could use it like a hand-held.

Because of my focus on digital modes, I wouldn't buy a radio without a direct USB connection and this radio does it all. I have found that it's rather sensitive to the cable being used and I've experienced the serial devices not appearing even though I was using a USB-C to USB-C cable that clearly carries data as it works with hard disks. The supplied USB-C to USB-A cable.

When plugged into a computer two serial devices appear. The second one is the CAT port (the other is a serial terminal).

Here's the settings that work with fldigi:


Here's the settings that work with FreeDV:


Rig: Xiegu X6200
Serial Port: /dev/ttyACM1
Baud Rate: 19200
Data bits, stop bits, handshake: Default
PTT Method: CAT
Mode: Data/Pkt
Split Operation: None.
Audio Input: alsa_input.usb-C-Media_Electronics_inc.USB_Audio_Device-00.mono-fallback
Audio Output: also_output.usb-C-Media_Electronics_inc._USB_Audio_Device-00.analog-stereo

The radio should switch to mode: U-DIG or L-DIG depending on band with FreeDV

I note that the audio level to WSJT-X is too high, 88dB. I needed to reduce "mic" gain in Linux sound settings.

Bugs

Xiegu has a history of software bugs in their radios. It seems some never get fixed. The most disappointing one for me is that I'm on the latest firmware 1.0.7 and I cannot get it to connect to Wifi which is a pity as there's a WFView server (Icom radio networking server) built in and it would be great to remote access the radio.

There is a discussion group at https://xiegu-x6200.groups.io/g/main/topics that looks to be a great resource.

Conclusion

I think this is quality radio for the money. I joined our club 80m net this week and got good reports from other stations. The radio puts out about 4W on the attached battery and about 9W with external power.

Wednesday, February 04, 2026

Tuesday, February 03, 2026

Very good morse decoder from 101 things

Prolific ham radio hacker Jonathan P Dawson has just shared a remarkable project he calls Hamfist that is a morse decode device running in the Arduino runtime for Pi PICO processors (including the original). 

Go and read his description but it's quite a sophisticated decoder, rather like a CW skimmer it can decode several morse signals in the audio passband. The PICO does the audio sampling with one of its A/D pins with just a simple circuit on the input.

"It combines adaptive signal processing, automatic speed estimation, probabilistic decoding, dictionary-based correction, and multi-channel decoding, all while keeping memory and CPU usage firmly under control."

I grabbed the source and built it in the Arduino IDE targeting the PICO. All very smooth. Unfortunately I don't have the display he uses, the ili9341, so I've ordered some. The code seems to run but of course I can't see the morse.

So. I pointed Claude Code at the Arduino files and simply asked it to create a C++ command line program that could read a WAV file with morse audio in it and print out the decoded morse. It wrote wav_decoder.cpp and a Makefile. Jonathan includes some sample WAV files so I tried some of these. Here's how that goes:

cw_decoder % ./cw_wav_decoder examples/cw2.wav

WAV file: examples/cw2.wav

  Sample rate: 12000 Hz

  Channels: 1

  Bits per sample: 16

  Target sample rate: 15000 Hz

CQ SP3VT SP3VT TEST E4EQ SP3VE E3VT TEST PA3A IN PA3AT 5NN E 27 MIKEEE EAT/ HI TU S E3VT CQ SP3VT SE 3VT TEST CZ G3VT SPSE UT TEST CQ SW3VT SP EVE TEST CQ SP3VT SAME3VT TEST DK3T DA3T 5NT 

I would add that the decode all happens in a fraction of a second.

I'm not sure if I'm more impressed with Jonathan's contribution or Claude Code's capabilities. Here's the file it produced and the Makefile.

#include <cstdio>
#include <cstdlib>
#include <cstdint>
#include <cstring>
#include <string>

#include "cw_dsp.h"
#include "fft.h"

// WAV file header structure
struct WavHeader {
char riff[4]; // "RIFF"
uint32_t file_size; // File size - 8
char wave[4]; // "WAVE"
char fmt[4]; // "fmt "
uint32_t fmt_size; // Format chunk size
uint16_t audio_format; // 1 = PCM
uint16_t num_channels; // Number of channels
uint32_t sample_rate; // Sample rate
uint32_t byte_rate; // Bytes per second
uint16_t block_align; // Bytes per sample * channels
uint16_t bits_per_sample; // Bits per sample
};

// Derived DSP class that outputs decoded text to stdout
class c_wav_decoder : public c_cw_dsp {
private:
std::string last_text[NUM_CHANNELS];

protected:
void decode(uint16_t channel, std::string text, std::string partial) override {
if (!text.empty() && text != last_text[channel]) {
printf("%s", text.c_str());
fflush(stdout);
last_text[channel] = text;
}
}

public:
void print_final() {
// Flush any remaining text
flush();
printf("\n");
}
};

// Simple linear interpolation resampler
class Resampler {
private:
double ratio;
double position;
int16_t last_sample;
bool first_sample;

public:
Resampler(uint32_t input_rate, uint32_t output_rate)
: ratio((double)input_rate / output_rate)
, position(0.0)
, last_sample(0)
, first_sample(true) {}

// Process one input sample, may produce 0 or more output samples
// Returns number of output samples produced
int process(int16_t input, int16_t* output, int max_output) {
int count = 0;

if (first_sample) {
last_sample = input;
first_sample = false;
}

while (position < 1.0 && count < max_output) {
// Linear interpolation
double frac = position;
int32_t interpolated = (int32_t)((1.0 - frac) * last_sample + frac * input);
output[count++] = (int16_t)interpolated;
position += ratio;
}

position -= 1.0;
last_sample = input;

return count;
}
};

void print_usage(const char* program_name) {
fprintf(stderr, "Usage: %s <wav_file> [channel]\n", program_name);
fprintf(stderr, "\n");
fprintf(stderr, "Decodes Morse code (CW) from a WAV audio file.\n");
fprintf(stderr, "\n");
fprintf(stderr, "Arguments:\n");
fprintf(stderr, " wav_file Path to the input WAV file (mono or stereo, any sample rate)\n");
fprintf(stderr, " channel Optional: frequency channel 0-%d (default: all channels)\n", NUM_CHANNELS - 1);
fprintf(stderr, "\n");
fprintf(stderr, "The decoder uses %d frequency channels spanning 0-%.0f Hz.\n",
NUM_CHANNELS, NUM_CHANNELS * CHANNEL_SIZE * (SAMPLE_FREQUENCY / 2.0) / (FRAME_SIZE / 2));
}

int main(int argc, char* argv[]) {
if (argc < 2 || argc > 3) {
print_usage(argv[0]);
return 1;
}

const char* filename = argv[1];
int selected_channel = -1; // -1 means all channels

if (argc == 3) {
selected_channel = atoi(argv[2]);
if (selected_channel < 0 || selected_channel >= NUM_CHANNELS) {
fprintf(stderr, "Error: channel must be between 0 and %d\n", NUM_CHANNELS - 1);
return 1;
}
}

// Open the WAV file
FILE* file = fopen(filename, "rb");
if (!file) {
fprintf(stderr, "Error: Cannot open file '%s'\n", filename);
return 1;
}

// Read WAV header
WavHeader header;
if (fread(&header, sizeof(WavHeader), 1, file) != 1) {
fprintf(stderr, "Error: Cannot read WAV header\n");
fclose(file);
return 1;
}

// Validate WAV format
if (strncmp(header.riff, "RIFF", 4) != 0 || strncmp(header.wave, "WAVE", 4) != 0) {
fprintf(stderr, "Error: Not a valid WAV file\n");
fclose(file);
return 1;
}

if (header.audio_format != 1) {
fprintf(stderr, "Error: Only PCM WAV files are supported (format=%d)\n", header.audio_format);
fclose(file);
return 1;
}

if (header.bits_per_sample != 16 && header.bits_per_sample != 8) {
fprintf(stderr, "Error: Only 8-bit or 16-bit WAV files are supported\n");
fclose(file);
return 1;
}

fprintf(stderr, "WAV file: %s\n", filename);
fprintf(stderr, " Sample rate: %u Hz\n", header.sample_rate);
fprintf(stderr, " Channels: %u\n", header.num_channels);
fprintf(stderr, " Bits per sample: %u\n", header.bits_per_sample);
fprintf(stderr, " Target sample rate: %.0f Hz\n", SAMPLE_FREQUENCY);
fprintf(stderr, "\n");

// Skip to data chunk
// The fmt chunk might be larger than our struct, and there might be other chunks
fseek(file, 12, SEEK_SET); // Skip RIFF header

char chunk_id[4];
uint32_t chunk_size;

while (fread(chunk_id, 4, 1, file) == 1) {
if (fread(&chunk_size, 4, 1, file) != 1) {
fprintf(stderr, "Error: Malformed WAV file\n");
fclose(file);
return 1;
}

if (strncmp(chunk_id, "data", 4) == 0) {
break; // Found data chunk
}

// Skip this chunk
fseek(file, chunk_size, SEEK_CUR);
}

if (strncmp(chunk_id, "data", 4) != 0) {
fprintf(stderr, "Error: Cannot find data chunk in WAV file\n");
fclose(file);
return 1;
}

// Initialize FFT and DSP
fft_initialise();
c_wav_decoder decoder;

// Create resampler
Resampler resampler(header.sample_rate, (uint32_t)SAMPLE_FREQUENCY);

// Process audio data
const int BUFFER_SIZE = 1024;
uint8_t buffer[BUFFER_SIZE * 4]; // Max size for stereo 16-bit
int16_t output_buffer[16]; // Resampler output buffer

int bytes_per_sample = header.bits_per_sample / 8;
int bytes_per_frame = bytes_per_sample * header.num_channels;
int samples_per_read = BUFFER_SIZE;

uint32_t total_samples = chunk_size / bytes_per_frame;
uint32_t samples_processed = 0;

while (samples_processed < total_samples) {
int samples_to_read = samples_per_read;
if (samples_processed + samples_to_read > total_samples) {
samples_to_read = total_samples - samples_processed;
}

size_t bytes_read = fread(buffer, bytes_per_frame, samples_to_read, file);
if (bytes_read == 0) break;

for (size_t i = 0; i < bytes_read; i++) {
int16_t sample;

if (header.bits_per_sample == 16) {
// 16-bit sample
int16_t* samples = (int16_t*)(buffer + i * bytes_per_frame);
if (header.num_channels == 1) {
sample = samples[0];
} else {
// Mix stereo to mono
sample = (samples[0] + samples[1]) / 2;
}
} else {
// 8-bit sample (unsigned)
uint8_t* samples = buffer + i * bytes_per_frame;
if (header.num_channels == 1) {
sample = ((int16_t)samples[0] - 128) * 256;
} else {
// Mix stereo to mono
sample = (((int16_t)samples[0] - 128) + ((int16_t)samples[1] - 128)) * 128;
}
}

// Resample to target rate
int num_outputs = resampler.process(sample, output_buffer, 16);

// Feed resampled samples to decoder
for (int j = 0; j < num_outputs; j++) {
decoder.process_sample(output_buffer[j]);
}
}

samples_processed += bytes_read;
}

// Flush remaining decoded text
decoder.print_final();

fclose(file);

return 0;
}

Makefile:

CXX = g++
CXXFLAGS = -std=c++17 -O2 -Wall

# Source files
SOURCES = wav_decoder.cpp \
cw_dsp.cpp \
cw_decode.cpp \
cw_classifier.cpp \
cw_data.cpp \
dictionary.cpp \
fft.cpp \
utils.cpp

# Object files
OBJECTS = $(SOURCES:.cpp=.o)

# Output binary
TARGET = cw_wav_decoder

.PHONY: all clean

all: $(TARGET)

$(TARGET): $(OBJECTS)
$(CXX) $(CXXFLAGS) -o $@ $^

%.o: %.cpp
$(CXX) $(CXXFLAGS) -c -o $@ $<

clean:
rm -f $(OBJECTS) $(TARGET)

# Dependencies
wav_decoder.o: wav_decoder.cpp cw_dsp.h fft.h
cw_dsp.o: cw_dsp.cpp cw_dsp.h cw_decode.h fft.h utils.h
cw_decode.o: cw_decode.cpp cw_decode.h cw_classifier.h cw_data.h dictionary.h
cw_classifier.o: cw_classifier.cpp cw_classifier.h
cw_data.o: cw_data.cpp cw_data.h
dictionary.o: dictionary.cpp cw_data.h
fft.o: fft.cpp fft.h
utils.o: utils.cpp utils.h

Thursday, January 29, 2026

Apple's Knowledge Navigator - are we there yet?

Like many Apple users I'm keen to see how an upgraded Siri, rumoured to be coming this year, works. Right now Siri is way behind competing speech handling agents. It can turn lights on and off just fine but ask it anything more and it either searches the Internet or asks ChatGPT.

Back in 1987, Apple published a ground breaking vision for how human computer interaction might work in the future. It was commissioned by then CEO, John Sculley, and it not only predicted AI agents but even the Internet.


This morning I was chatting with SmartFriend™ Tony about Clawdbot/Moltbot and he mentioned Knowledge Navigator. We are now very close to this futuristic vision. Not quite there, but it is within reach. ChatGPT helped me create this feature list.

Achieved:

  • Voice-activated assistants
  • Retrieving and summarising information
  • Video calls and collaboration
  • Making and managing calls/messages
  • Touchscreens and tablets
  • Text-to-speech and speech-to-text
Not as envisaged:
  • We don't use an animated person's head as our assistant - probably for the best
Not quite here yet:
  • Deep contextual long-term assistant memory - Apple announced this two years ago but didn't ship
  • Fully autonomous, multi-step tasks with actions - Agents promise this.
What a remarkable vision of the future Sculley and Apple published almost 40 years ago. I hope the Siri team is taking a look.


Wednesday, January 28, 2026

CYD Internet radio

Recently I've been playing with the Cheap Yellow Display boards. They are an ESP-32 on the back of a 2.8 inch LCD display with touch. One of the most interesting projects is an Internet Radio. Source code is here.

The boards have quite a range of devices already in place such as an RGB LED and an audio amplifier, other I/O is nicely taken out to sockets on the edge. (I've upgraded my 3D case design to add holes for these). I've just got it working:


You'd think it would be possible to use the on-board audio DAC with the speaker driver but unfortunately the I/O is shared with one of the touch screen lines. An external audio DAC should be used.

I changed a few of the constants to use the pins available on my version of the board (The USB-C one) as follows:

// On the USB-C CYD
const int I2S_BCKL = GPIO_NUM_18; // BLU --> BCKL | // 26 |
const int I2S_WSEL = GPIO_NUM_19; // YEL --> WSEL |UDA1334A // 25 | when using spkr output
const int I2S_DOUT = GPIO_NUM_23; // RED --> DIN | // 27 |
// also GPIO_NUM_27 BLK
// 3.3V RED VIN
// GND GRN GND

Other changes I made included the screen rotation and time zone for my area.

It's now playing lovely classical music, although it's a bit glitchy for some reason. My touch screen doesn't seem to be working so I plan to figure that out next. Thanks to John VK2ASU for the prompt to work on this.

Tech Talk on ABC Radio - Artificial Intelligence is the wrong name

Do you think the term Artificial Intelligence is being misused for things which aren't intelligent at all?  

Have we come to trust the output too much, partly because of the name? Could we have fallen for a marketing trick? 

Prediction market odds are increasingly being reported as news, what are they and can they be gamed?

Communication failures during the recent Victorian fires, why the big ABC AM stations are an important backup in times of emergency.

Peter Marks, mobile software developer and technology commentator from Access Informatics, joined Philip Clark and listeners to Nightlife with the latest tech news. https://www.abc.net.au/listen/programs/nightlife/nightlife-tech-talk-with-peter-marks/106276236 

Monday, January 26, 2026

Starlink residential lite is enough for me

I've been very happy with Starlink after NBN dropped the ball here and drove me away. Until now I've been on the Residential Full plan at AU$139 / month. Most of my computers are connected over Wifi and here's the speed test I was getting:


I decided to try Residential Lite which is said to be a lower speed and lower priority in busy times. Here's what I get now:


Still pretty good. My guess is that the busy times when I might see reduced speed due to congestion would be in the evenings when people are streaming video but so far I haven't noticed it.

I do see a bit of buffering just after a YouTube video starts playing on the Apple TV box but I saw that before and assume it's some networking mis-match. Lengthy multi-party Zoom calls work as well as before.

Residential Lite is AU$99 / month so that's $40 a month freed up for ham radio bits.



Sunday, January 25, 2026

FreeDV net - 12 on frequency but poor conditions

A good rollup this morning but few stations could actually hear many people. Very warm here in south eastern Australia. Jack, VK5KVA said he had 42.7C yesterday (108.86F) and the only cooling was a ceiling fan. We're expecting about 34C today.

Stations on frequency: VK2AWJ, VK2GMH, VK3JCO, VK3KEZ, VK3PCC, VK3SRC, VK3XCI, VK5KVA, VK5MH, VK7DMH, VK4IKZ, VK5RA and me, VK3TPM.