Intro

There's something slightly absurd about turning a tiny retro handheld into both an app development platform and a weak-signal radio decoder. The R36S - cheap, hackable, and rough around the edges - wasn't built for any of this.

That's exactly why it works.

The Constraint Advantage

Modern development hides inefficiency. The R36S exposes it.

With limited CPU, RAM, and screen space, you're forced to:

  • Write tighter code
  • Design simpler interfaces
  • Think in constraints, not abstractions

That same constraint-driven thinking applies perfectly to signal processing tasks like FT8.

Start with the Right Foundation: ROCKNIX

Firmware matters more than hardware here.

ROCKNIX stands out as a sanely maintained base:

  • Consistent, purposeful updates
  • Clean design philosophy
  • Predictable behavior
  • Modern and maintained Linux kernel

Compared to more fragmented alternatives, ROCKNIX gives you a stable environment - critical when you're experimenting with both development and DSP workloads.

A Playground for Real Systems Learning

Underneath it all, the R36S runs Linux. That opens the door to:

  • Writing small C or Python programs
  • Building terminal-based tools
  • Experimenting with audio pipelines
  • Understanding how software meets hardware

This isn't abstracted development. It's hands-on and honest.

Enter FT8: Where Things Get Interesting

FT8 decoding sounds simple - but it isn't.

It involves:

  • Continuous audio sampling (~12 kHz)
  • FFT-heavy signal processing
  • Strict 15-second decode cycles

On a modern PC, this is trivial. On the R36S, it's a stress test.

Can It Actually Do It?

Yes - but barely, and that's the point.

  • Sparse bands → decent decoding
  • Busy bands → slow decodes (?)
  • Full GUI apps → too heavy
  • Minimal CLI decoders → surprisingly workable

You quickly learn:

  • What "CPU-bound" really means
  • How algorithm efficiency affects outcomes
  • Why timing and synchronization matter

Learning by Breaking (and Dropping Frames)

The R36S gives immediate feedback:

  • Inefficient code → lag
  • Bad DSP assumptions → missed signals
  • Timing drift → failed decodes

FT8 makes this brutally obvious because it's time-sensitive. You're not just writing code - you're aligning with physics and timing.

Micro-App Thinking Meets Signal Processing

You won't build a full-featured radio suite here.

Instead, you'll build:

  • Minimal FT8 decoders
  • Audio capture pipelines
  • Small utilities for filtering or logging

This constraint teaches modularity in a very real way.

The Hidden Challenge: I/O and Time

FT8 isn't just CPU-bound - it's time-bound.

You'll need:

  • Reliable audio input (USB sound card, etc.)
  • Accurate system time (NTP or GPS)

Solving these on a constrained device teaches more than any tutorial.

Portable, Hackable, Slightly Unreasonable

There's something uniquely satisfying about decoding real radio signals on a device that fits in your pocket.

With ROCKNIX, that experience becomes:

  • Less chaotic
  • More predictable
  • Still deeply hackable

While we have an Android app that decodes and transmits FT8, it is NOT nearly as fun as this one ;)

Not Efficient - But Deeply Educational

Using the R36S for development and FT8 decoding is inefficient.

That's the entire point.

You gain:

  • Real understanding of system limits
  • Intuition for performance and timing
  • Respect for efficient design

The Strange Advantage

When you return to a powerful machine, everything feels easier - but you think differently.

You:

  • Write leaner code
  • Design simpler systems
  • Understand performance, not just assume it

And maybe most importantly - you'll know that even a tiny, underpowered device can do surprisingly complex things…

…if you meet it halfway.

That's Pocket Chaos.

Initial Forays

Start by installing the stable ROCKNIX release and then upgrade to the nightly builds.

R36S clones need the RK3326 -B variant of the images.

RK3326:~ # uname -a
Linux RK3326 6.12.79 #1 SMP Sat May  2 06:28:30 UTC 2026 aarch64 GNU/Linux

RK3326:~ # mkdir /storage/experiments
RK3326:~ # cd /storage/experiments/

# curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh  # OOMs!

# git clone https://github.com/bodiya/wsjtr

<Let the adventures begin!>
// We cross-compiled "./wsjtr" on a different system
RK3326:~/release # time ./wsjtr --wav test_01.wav --passes 1 --keep-wav
032817   7   0.8 1513 ~  JO1COV DL4SBF 73
032817  10   0.8 2138 ~  LZ365BM <...> 73
032817  17   1.2 2279 ~  PY2DPM ON6UF RR73
032817  17   1.7 2390 ~  CQ E75C JN93
032817   3   1.0 1292 ~  EA9ACD HA5LGO -13
032817   4   0.9  823 ~  LY2EW DL1KDA RR73
032817  -1   0.6  955 ~  CQ IU8DMZ JN70
032817  19   0.8 1123 ~  CQ HB9CUZ JN47
032817  -2   1.0 1564 ~  JI1TYA DH1NAS 73
032817  -7   0.8  338 ~  JO1COV PE1OYB JO21
032817   9   0.8 2327 ~  CQ R8AU MO05
032817 -19   1.7 1450 ~  CQ RX3ASQ KO95
032817 -10   0.8  559 ~  OE3MLC G3ZQQ 73
032817   6   0.8 1369 ~  CQ OK6LZ JN99
032817  21  -1.1 2378 ~  R1CBP SP9LKP RR73
032817  -4   0.1 1285 ~  MM0IMC 4U1A -06
032817   5   0.8 1158 ~  CQ HA1BF JN86
032817   3   1.9  771 ~  JA1FWS OK2BV JN89
032817  -4   0.1 1345 ~  CQ 4U1A JN88

real    0m21.356s
user    0m25.235s
sys     0m1.536s

Heh ;)

user@zion:~/repos/ft8_lib_upstream$ cat Makefile
BUILD_DIR = .build

# Cross-compilation support
CROSS_COMPILE ?=
CC = $(CROSS_COMPILE)gcc
AR = $(CROSS_COMPILE)ar

FT8_SRC  = $(wildcard ft8/*.c)
FT8_OBJ  = $(patsubst %.c,$(BUILD_DIR)/%.o,$(FT8_SRC))

COMMON_SRC = $(wildcard common/*.c)
COMMON_OBJ = $(patsubst %.c,$(BUILD_DIR)/%.o,$(COMMON_SRC))

FFT_SRC  = $(wildcard fft/*.c)
FFT_OBJ  = $(patsubst %.c,$(BUILD_DIR)/%.o,$(FFT_SRC))

TARGETS  = libft8.a gen_ft8 decode_ft8 test_ft8

# Base CFLAGS and LDFLAGS
CFLAGS   = -O3 -DHAVE_STPCPY -I.
LDFLAGS  = -lm

ifdef OPTIMIZE
CFLAGS   += -Ofast -mcpu=cortex-a35+crypto+crc -flto
LDFLAGS  += -flto
endif

ifdef FT8_DEBUG
CFLAGS   += -fsanitize=address -ggdb3 -DFTX_DEBUG_PRINT
LDFLAGS  += -fsanitize=address
endif

ifdef STATIC
CFLAGS   += -static
LDFLAGS  += -static
endif

# Optionally, use Portaudio for live audio input
# Portaudio is a C++ library, so then you need to set CC=clang++ or CC=g++
ifdef PORTAUDIO_PREFIX
CFLAGS   += -DUSE_PORTAUDIO -I$(PORTAUDIO_PREFIX)/include
LDFLAGS  += -lportaudio -L$(PORTAUDIO_PREFIX)/lib
endif

.PHONY: all clean run_tests install

all: $(TARGETS)

clean:
        rm -rf $(BUILD_DIR) $(TARGETS)

run_tests: test_ft8
        @./test_ft8

install: libft8.a
        install libft8.a /usr/lib/libft8.a

gen_ft8: $(BUILD_DIR)/demo/gen_ft8.o libft8.a
        $(CC) $(CFLAGS) -o $@ .build/demo/gen_ft8.o -lft8 -L. $(LDFLAGS)

decode_ft8: $(BUILD_DIR)/demo/decode_ft8.o libft8.a $(FFT_OBJ)
        $(CC) $(CFLAGS) -o $@ $(BUILD_DIR)/demo/decode_ft8.o $(FFT_OBJ) -lft8 -L. $(LDFLAGS)

test_ft8: $(BUILD_DIR)/test/test.o libft8.a
        $(CC) $(CFLAGS) -o $@ .build/test/test.o -lft8 -L. $(LDFLAGS)

$(BUILD_DIR)/%.o: %.c
        @mkdir -p $(dir $@)
        $(CC) $(CFLAGS) -o $@ -c $^

lib: libft8.a

libft8.a: $(FT8_OBJ) $(COMMON_OBJ)
        $(AR) rc libft8.a $(FT8_OBJ) $(COMMON_OBJ)
make clean && make STATIC=1 OPTIMIZE=1 CROSS_COMPILE=aarch64-linux-gnu-
RK3326:~/release # time ./decode_ft8 test_01.wav
Sample rate 12000 Hz, 180000 samples, 15.000 seconds
Block size = 1920
Subblock size = 960
N_FFT = 3840
#############################################################################################
Max magnitude: -19.1 dB
000000 +18.0 +1.44 1369 ~  CQ OK6LZ JN99
000000 +15.5 +1.52  709 ~  CQ IK4LZH JN54
000000 +15.5 +1.44 2328 ~  CQ R8AU MO05
000000 +15.0 +1.44  891 ~  SA5QED IQ5PJ 73
000000 +14.5 +1.68 1291 ~  EA9ACD HA5LGO -13
000000 +14.0 +1.52  559 ~  OE3MLC G3ZQQ 73
000000 +14.0 +1.44  338 ~  JO1COV PE1OYB JO21
000000 +13.5 +1.52 1125 ~  CQ HB9CUZ JN47
000000 +13.0 +1.28  956 ~  CQ IU8DMZ JN70
000000 +13.0 +2.56  772 ~  JA1FWS OK2BV JN89
000000 +13.0 +1.52  822 ~  LY2EW DL1KDA RR73
000000 +13.0 +1.68 1566 ~  JI1TYA DH1NAS 73
000000 +12.5 +1.52 2138 ~  LZ365BM <...> 73
000000 +12.5 +1.52 1512 ~  JO1COV DL4SBF 73
000000 +12.5 +2.40 1450 ~  CQ RX3ASQ KO95
000000 +11.5 +1.36 2691 ~  CQ OE8GMQ JN66
000000 +10.5 +1.84 2278 ~  PY2DPM ON6UF RR73
000000 +09.5 +1.36 1616 ~  JO1COV PA0CAH JO21
Decoded 18 messages, callsign hashtable size 26

real    0m0.473s
user    0m0.449s
sys     0m0.013s

Yes - this is more like it!

Bonus console image

Tips

The original microSD card that came with the R36S clone was NOT mounting on Linux automatically.

Trying to mount it explicitly resulted in:

$ sudo kpartx -a /dev/sda
Warning: Disk has a valid GPT signature but invalid PMBR.
Assuming this is *not* a GPT disk anymore.

Quick-fix:

$ sudo kpartx -a -g /dev/sda

This forces GPT interpretation for this affected disk.

References