Skip to content
HamRadioVillagePublic

About

[PUBLIC] Default fox firmware used for HRV DEF CON 34 Fox Hunt

Topics

Resources

Stars

6 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

DEF CON 34 Decoy Fox

So you've purchased a fox from the Ham Radio Village during DEF CON .... now what?

‼️ Read this first: you need a license to transmit

The radio module on this board (a NiceRF SA868) is a real transmitter on the amateur (ham) bands (specifically VHF) — rated for ~2 W, and shipped here on its low-power setting (~25 dBm, roughly 0.3 W). In the US and most countries, transmitting there requires an amateur radio license — in the US, an entry-level Technician license covers it. Identifying with your own callsign is also required by law, which is exactly what the callmessage setting is for.

The foxes come preprogrammed with a callsign just for the DEF CON 34 event so you can play along on location at the Las Vegas Convention Center with us. This applies to the Ham Radio Village fox hunt during posted contest hours from August 7–9, 2026 only.

Outside of this event, you are responsible for reprogramming the fox with either your own callsign or another event callsign.

Until you're licensed and have set your callsign, don't transmit into an antenna. You can still safely power the board and experiment if you transmit into a dummy load instead of an antenna (see Quick start).

This README is a full guide for what this piece of hardware is, what it does, how to put it on the air legally, how to change the callsign and message, and how to load your own song. We cover everything here.


Table of contents


What is a fox?

A fox is a small radio transmitter that beacons on a set schedule so that people carrying directional antennas and receivers can locate it. The sport of finding it goes by several names — fox hunting, transmitter hunting, radio direction finding (RDF), or the international name ARDF (Amateur Radio Direction Finding).

The idea is simple and old: a hidden transmitter identifies itself periodically, and hunters use the direction a signal comes from — plus how its strength changes as they move — to triangulate their way to it. It's part scavenger hunt, part orienteering, part practical radio skill (the same techniques are used to track down interference and stuck transmitters in the real world).

This fox does the classic thing: on each cycle it keys up the transmitter, sends its callsign in Morse code (so it's identifiable and legal), plays a short song (so it's fun and easy to recognize on the air), then goes quiet for a set delay before doing it again. One LED blink marks each cycle so you can see it's alive.

A typical hunt: several foxes are hidden around a park or venue, each on its own frequency or timing, and hunters race to find them all.


The hardware

A front view of the black and green shield design fox with dual band antenna

This piece of hardware is a tiny amateur-radio transmitter that plays a Morse-code ID and a short song on a loop. Built for fox hunting, it's the thing RF hunters are trying to find with their antennas.

The board supports both bands — but our default is VHF. The PCB design accommodates either band, and the antenna shipped with it is dual-band. But the SA868 radio module comes in band-specific versions, and one module is fitted per fox. Some of the newer designs are dual-band units, but versions previous to DC34 may be VHF only.

Our default build is VHF: the 2 m amateur band (144–148 MHz), and this README is written for that build. The DEF CON fox hunt runs 70 cm / UHF foxes too; if you have a UHF-populated board, everything here applies except the frequency — see Picking a frequency.

Your fox is built around two parts:

  • Seeed Studio XIAO ESP32-C3 — a thumbnail-sized microcontroller board. This is the "brain" that runs the fox code. It has a USB-C port you'll use for flashing and loading code.
  • NiceRF SA868 — a self-contained radio module. This is the "voice": it's the actual transmitter that puts the Morse and song on the air. The microcontroller talks to it over a serial (UART) connection. NiceRF sells the SA868 in band-specific versions — a VHF part covering 134–174 MHz and a UHF part covering 400–480 MHz — which are otherwise identical (same footprint, same pins, same AT commands). The module is rated for ~2 W, but code.py drives the HL pin low, which selects the module's low-power output (~25 dBm, roughly 0.3 W) — plenty for a venue-sized hunt and easier on the battery. Worth knowing if you're sanity-checking signal strength against distance while hunting.

Plus an antenna (connect it before powering on — see the warnings below) and a power source (USB, or a LiPo battery for field deployment).

The original design and software are from the C0ldbru / rot13labs fox at https://github.com/c0ldbru/fox (GPL-3.0). See Credits & license for the full attribution chain.

Pinout reference

The fox code drives the SA868 and a status LED through these XIAO pins. You don't need this to use the fox, but it's handy if you're inspecting or repairing the board.

Function XIAO pin (code) GPIO Notes
PTT (push-to-talk) D3 GPIO05 Low = transmit, High = receive
PD (power-down) D4 GPIO06 Held high = module awake
HL (high/low power) D5 GPIO07 Low = ~25 dBm (low power); this is how the fox ships
UART RX (from radio) D7 GPIO20 Serial in
UART TX (to radio) D6 GPIO21 Serial out
MIC / audio out D1 GPIO03 Tone/song audio into the module
Status LED D10 GPIO10 One blink per cycle = alive

The LiPo battery, charging, and some safety tips

Many of these foxes run from a small LiPo (lithium-polymer) battery so they can be hidden away from a USB outlet. LiPos pack a lot of energy into a tiny package, which is exactly why they're handy — and exactly why they deserve respect. A little care keeps them safe and long-lived.

Charging. The XIAO ESP32-C3 has a built-in charger: plug the board in over USB-C and it charges the connected LiPo automatically. A charge LED on the board indicates charging in progress. Charge on a hard, non-flammable surface (not on a bed, couch, or pile of papers), and don't leave a charging battery unattended for long stretches — this is the single most common-sense habit with any lithium cell.

Caution

⚠ This firmware has no battery protection — it never reads the pack and will transmit until the LiPo is flat (see Battery & deployment notes). Running a LiPo all the way down and leaving it there is the main way to damage one. Recharge the fox after use; if storing, disconnect the battery and look up safe storage charges based on the information on the battery (voltage, amp hours).

A few habits that matter:

  • Watch the polarity. If you ever unplug and reconnect the battery, the + and – must go back the way they came. Reversing a LiPo can destroy the board and damage the cell. If your battery connector isn't keyed, mark it. Our foxes have a JST connector that only allows for one polarity connection... but it's possible to force it and break the connector.
  • Never puncture, crush, or bend the cell. The soft foil pouch is not armor. A swollen, puffy, or hissing battery is done — stop using it and dispose of it properly (see below).
  • Mind the heat. Don't leave the fox baking in a hot car or in direct sun for hours; heat is hard on lithium cells. If a battery gets hot to the touch while charging, unplug it.
  • Keep the terminals from shorting. A loose battery in a bag with keys or coins can short across its leads. While we've used a JST connector, it's possible to short across the mounting on the fox PCB. Liquids will also cause this issue.
  • Charge before you hide it, not after. Top it off, note your deploy time, and retrieve it promptly. These foxes aren't weather proof (enclosed designs can be found online or DIY).

Disposal. Don't put LiPos in household trash or curbside recycling — they can ignite when crushed. Most areas have battery-recycling drop-offs (many electronics and hardware stores host them). A swollen or damaged cell should be retired immediately and taken to one of these.

None of this is meant to scare you off — millions of devices use these cells safely every day. These warnings are meant as information for extended use and so there are no surprises as you use a new piece of hardware.

Charging without transmitting

To charge indoors, on a bench, or anywhere you shouldn't be on the air:

  1. Plug in USB-C (charging starts on its own).
  2. Hold B (BOOT) while plugging in the board, tap R (RESET), then release B.

No LED blink means it worked — the board is charging and will not transmit.

If that does not work with your board, try: Hold B (BOOT) while powering on the board for 3-4 seconds without tapping the RESET.

This is the same move as Get into bootloader mode, because it is that mode. Holding B pulls GPIO9 low, and tapping R resets the chip so it samples that pin — the ESP32-C3 comes up in its ROM serial-download mode, CircuitPython never starts, code.py never runs, and nothing keys the radio. The XIAO's battery charger is separate circuitry and runs either way.

Important

Use R — don't just plug in USB. The boot mode is sampled at reset. With a battery attached the board is already running, so plugging in USB may not reset it — and if it doesn't, the fox keeps transmitting while it charges. Tapping R is what makes this reliable.

Two more things worth knowing:

  • This is chip behavior, not a firmware feature. Nothing in code.py reads a button. It survives code edits and reflashes, but it is not a designed safety interlock — don't treat it as one.
  • It ends at the next reset. The boot mode is latched until the chip is reset or powered down. Tapping R on its own, or cycling the battery, boots normally and the fox is transmitting within seconds — so leave the antenna or dummy load attached while charging.

For a fox that is unambiguously off, disconnect the battery.


Required Utilities

In order to flash/program your fox, you'll need the following items:

Quick start

For people who are already licensed and comfortable, and just want it on the air. Everyone else: skip to Step 1.

  1. Connect the antenna (or dummy load). Never key the SA868 without a load.
  2. Is the board already running CircuitPython? (A >>> prompt with a CircuitPython banner in Thonny confirms it.) If not, flash it.
  3. Open code.py, set your callmessage (callsign) and frequency.
  4. In Thonny: File → Save As → CircuitPython device, save as exactly code.py at the device root.
  5. Enter Ctrl+D in the shell to run. Healthy boot prints initializing system ... ready, no callsign warning, then Morse + song on a loop.

Field edit rhythm: Ctrl+C to stop, edit, save, Ctrl+D to run.


Step 1 — Flash CircuitPython

Flashing is a one-time job that installs the CircuitPython language onto the board. You only need it if the board isn't already running CircuitPython — a factory-fresh board, or one in an unknown state, needs it; a board that's already been flashed skips straight to Step 2.

Quick check for a mystery board: plug it in normally (no button), open Thonny, and connect. A >>> prompt with a CircuitPython banner means it's already flashed — go to Step 2.

Two things about this specific board that surprise people:

  • No USB drive ever appears — before or after flashing. That's a property of this chip, not a fault. You confirm success by connecting in Thonny and seeing the CircuitPython prompt, not by looking for a drive.
  • Thonny can't do the flash on this board (its installer expects a USB drive). Thonny handles Step 2 only. So Step 1 uses one of the two tools below.

What you need

  • The fox board and a USB-C data cable (not charge-only — a charge-only cable is the #1 reason the board never shows up).
  • The CircuitPython firmware .bin file — download the latest stable .bin (not the .uf2) from https://circuitpython.org/board/seeed_xiao_esp32c3/. The filename looks like adafruit-circuitpython-seeed_xiao_esp32c3-en_US-<VERSION>.bin.
  • The antenna or a dummy load connected — the fox transmits within seconds of its code running later, and keying the SA868 without a load can damage it.

Get into bootloader mode (needed either way)

To receive firmware, the board has to be listening for it:

  1. Unplug the board.
  2. Find the tiny button labeled B (BOOT).
  3. Press and hold B, and while still holding, plug in the USB-C cable.
  4. Hold B one more second, then release.

Nothing visible happens — that's expected. If a later step can't find the board, redoing this is almost always the fix.

Already plugged in? Hold B, tap the R (RESET) button, then release B. Same result.

Now pick one of the two options below — they do exactly the same thing.

Option A — Browser tool (no installs)

Best if you'd rather not install Python or touch a terminal, and you have Chrome or Edge plus an internet connection.

  1. In Chrome or Edge, open https://adafruit.github.io/Adafruit_WebSerial_ESPTool/.
  2. With the board in bootloader mode, click Connect (top right) and pick your board's serial port. On Windows it looks like COM5 (USB Serial Device) or USB JTAG/serial debug unit. If you're not sure which entry it is: unplug, see which disappears, redo the full bootloader move (hold B while replugging), and pick the one that returns.
  3. Click Erase and confirm. Wait for it to report done.
  4. Find the "Choose a file…" row with an offset box. Choose your .bin, set the offset to 0x0 (that address matters), and click Program. A progress bar fills; success shows after a minute or two.
  5. Disconnect the browser tool (click disconnect or close the tab). This matters — if the browser keeps holding the port, Thonny can't connect in Step 2 and it looks exactly like a failed flash.
  6. Unplug and replug normally (no button). It's now running CircuitPython.

Option B — Command line with esptool

Best if you already have Python (or don't mind installing it), want it to work offline, or expect to flash lots of boards.

  1. Install Python from https://www.python.org/downloads/. On Windows, check "Add Python to PATH" during install. Then:
    pip install esptool
    
  2. Find your port: Windows — Device Manager → Ports (COM & LPT) → note COMx. macOS — ls /dev/cu.* (e.g. /dev/cu.usbmodem1101). Linux — ls /dev/ttyACM* (usually /dev/ttyACM0).
  3. With the board in bootloader mode, erase the flash with (swap in your port):
    python -m esptool --chip esp32c3 --port <PORT> erase-flash
    
    Success ends with Flash memory erased successfully… and a reset.
  4. The erase step will take the board out of bootloader mode, so redo the B-button move, then write (swap in your port and real filename):
    python -m esptool --chip esp32c3 --port <PORT> write-flash -z 0x0 adafruit-circuitpython-seeed_xiao_esp32c3-en_US-<VERSION>.bin
    
    Success shows Hash of data verified. then Hard resetting….
  5. Unplug and replug normally (no button).

Recent esptool spells the commands with hyphens (erase-flash / write-flash); older versions use underscores. Current versions accept both.

If it hangs on Connecting......, the board isn't in bootloader mode — redo the B move. If python isn't recognized, try py -m esptool.

Linux only: if the port shows "permission denied," run sudo usermod -a -G dialout $USER, then log out and back in. If the port appears then vanishes, a service called brltty may be grabbing it — sudo apt remove brltty fixes that.


Step 2 — Load the fox code with Thonny

Flashing installed the language; now you load the fox program (code.py). This step is repeatable — it's also how you change the callsign, frequency, or song later.

Point Thonny at the board

  1. Install and open Thonny (https://thonny.org).
  2. Run → Configure interpreter… → interpreter CircuitPython (generic) → set Port → OK.
  3. Confirm the shell banner names CircuitPython (something like Adafruit CircuitPython 10.x on … XIAO ESP32C3). Press Ctrl+D in the shell to reprint it if unsure.

A >>> prompt alone is not proof. Thonny shows the identical prompt when it's connected to your computer's Python. Check the bottom-right corner of the window — it names the current interpreter. If it says "Local Python 3," or if Save As never offers a "CircuitPython device" destination, redo Configure interpreter. (Empty Port list? Something may still hold the port — close the browser flashing tab — or the board needs a plain replug.)

Save the code

  1. Connect an antenna (or dummy load) — the fox goes on-air seconds after code.py runs.
  2. Open this repo's code.py.
  3. File → Save As… → CircuitPython device → save as exactly code.py at the device root. That exact name is what CircuitPython auto-runs at boot.
  4. Press Ctrl+D in the shell to run it. (While Thonny holds the >>> prompt, the board waits idle — Ctrl+D soft-reboots into your code with the shell still attached, so you see its output.)

This is a single-file fox — just code.py, nothing else. A healthy first boot prints:

initializing system ... ready
** NO BATTERY MONITORING ** deploy charged, retrieve promptly

…then the Morse and song transmit on a cycle, one LED blink each. If you see a callsign warning, you haven't set your callsign yet — see the next section.

Silence with no error usually means a misnamed file. View → Files shows the board's contents — the file must be at the root and spelled exactly code.py.


Your callsign and message

Note: your fox arrives with the DEF CON event callsign already loaded, so it's ready to hunt out of the box. The code.py in this repo keeps the neutral N0CALL placeholder shown below — so when you reflash or edit, you're setting your callsign, not transmitting someone else's. Once the event is over, put your own callsign here.

Open code.py. Near the top:

callmessage = "N0CALL DECOY DECOY N0CALL"  # your callsign goes here

Replace this with your own callsign plus whatever short message you want. The fox sends this whole string in Morse each cycle. Examples:

callmessage = "W12ABCD FOX"
callmessage = "W12ABCD FOXHUNT DE W12ABCD"

Note: If you have that many letters/numbers in your callsign, you've written something down wrong.

Rules and tips:

  • Use your real, licensed callsign. Identifying is legally required, and N0CALL is not a real callsign — it's a placeholder.
  • Leave CALLSIGN_PLACEHOLDER = "N0CALL" alone. That line is a safety sentinel: if N0CALL still appears in your callmessage, the board prints a boot warning so you don't accidentally transmit an unset callsign. Setting your real callsign makes the warning go away — that's how you know it took.
  • Only characters in the Morse table are sent. Letters A–Z, digits 0–9, and / are supported. Anything else (commas, dashes, etc.) is silently skipped.
  • Keep it reasonably short — the whole string is sent every cycle, so a long message means longer transmissions.

Save (code.py to the device) and Ctrl+D to apply. A clean boot with no callsign warning confirms it.


Picking a frequency

frequency = 146.565 # MHz -- see the README ("Picking a frequency")

(Yes, that comment points right here.)

The default, 146.565 MHz, is the customary "fox" frequency — commonly used for hidden-transmitter hunts and a good default when you're not at an event with assigned frequencies.

Two important cautions:

  • Stay within your license privileges and any local band plan. Which frequencies you may use depends on your license class and your country/region. In the US, a Technician license covers the 2 m band this board uses.
  • Keep it in-band for the module that's actually fitted. This applies for all hardware, not just ours. Our default build is a 2 m (VHF) fox — stay in 144–148 MHz for those. Some of our 2026 hardware is dual-band, which means it will transmit in both VHF and UHF with the same module. If you do not have a dual-band module, setting a 70 cm frequency on a VHF board won't produce a usable signal no matter what the antenna is: the module's synthesizer and power amplifier are built for one band. If you have a UHF-populated board instead, the 70 cm equivalent is 433–435 MHz (a common fox choice is 433.900 MHz) — check your event's assignment. Everything else in this README is unchanged; only frequency differs.

To change it: edit frequency, save code.py to the device, Ctrl+D in the shell to reboot.


The song

Each cycle, after the Morse ID, the fox plays a short tune. This makes a fox fun and easy to recognize by ear on a crowded band.

Three tunes are built in

code.py ships with three public-domain tunes, and you switch between them by commenting and uncommenting. Exactly one is active at a time:

  1. William Tell Overture finale (Rossini) — the "Lone Ranger" gallop. Active by default.
  2. Sailor's Hornpipe (traditional) — the "Popeye" tune.
  3. First Call — the racetrack "call to post" bugle, a fitting fanfare for the start of a fox hunt.

To switch, open code.py, find the song block near the top, comment out the active tune's SONG_TEMPO and SONG_NOTES (they come as a pair), and uncomment the one you want. In Thonny you can select the lines and press Ctrl+3 to toggle comments on or off. Save to the device and Ctrl+D.

Comment the pair together. Each tune has its own SONG_TEMPO and SONG_NOTES. If you uncomment one tune's notes but leave another's tempo active, the song will play at the wrong speed.

How a song is stored in this firmware

Most of these tunes started out as RTTTL ringtones of public-domain melodies. RTTTL isn't played directly, it's converted to this firmware's MIDI-note format ahead of time.

This firmware stores each melody as a list of (MIDI note, duration) pairs, with a tempo. A tune looks like this near the top of code.py:

SONG_TEMPO = 180
SONG_NOTES = [
    (67, 2), (67, 2), (67, 4), (67, 2), (67, 2), (67, 4), ...
]
  • The first number is a MIDI note number (60 = middle C, 69 = A above middle C, …). 0 means a rest (silence).
  • The second number is the note's length in 32nds of a whole note — so 8 is a quarter note, 4 an eighth, 16 a half, 2 a sixteenth. A dotted note is 1.5× (a dotted quarter = 12).
  • SONG_TEMPO is the tempo in BPM. Higher = faster.

You can hand-edit SONG_NOTES directly if you're comfortable with MIDI note numbers. Most people find it easier to start from an RTTTL tune of a public-domain melody and convert it to the note/duration format above.

Tuning the sound

A few related settings at the top of code.py:

WPM = 12            # Morse speed (words per minute)
TONE_HZ = 800       # Morse tone pitch, in Hz
ARTICULATION = 0.875 # fraction of each note slot that actually sounds

ARTICULATION controls how "detached" the song notes sound — lower values leave a gap between notes (more staccato); 1.0 runs them together.


Battery & deployment notes

⚠ This firmware has NO battery protection. It never reads the battery and will keep transmitting until the LiPo is flat — which is bad for the LiPo.

Because of that:

  • Deploy fully charged, note the time you deployed, and retrieve promptly. Don't leave it running for days.
  • The board blinks its LED once per cycle — that's your "still alive" signal.
  • transmit_delay = 30000 sets the gap between transmissions (in milliseconds; 30000 = 30 seconds). A longer delay means less airtime and longer battery life, but the fox is "findable" less often.

This is the deliberately simple firmware — no battery monitoring, one song, one file — chosen so it's easy to read and reliable. More advanced variants (with battery alerts, multiple songs, sleep modes) exist upstream if you want to grow into them (see Credits).


Troubleshooting

Situation What to do
Board / port never shows up Swap the USB cable first — charge-only cables won't enumerate.
Browser flash tool won't connect Redo bootloader mode (B while plugging in); use Chrome or Edge.
Connecting...... never ends (esptool) Same cause — board isn't in bootloader mode. Redo the B move.
'python' is not recognized Try py -m esptool, or reinstall Python with "Add Python to PATH" checked.
Flashed OK, but Thonny can't connect / no >>> The browser tool or another program may still hold the port — disconnect it / close the tab, replug, retry.
>>> prompt but Save As has no "CircuitPython device" Thonny is on your computer's Python, not the board — check the bottom-right corner; redo Configure interpreter.
Saved code.py but nothing runs, no output Press Ctrl+D in the shell — a held REPL blocks auto-run.
Ctrl+D but still silent, no error Misnamed/misplaced file — View → Files: file at device root, spelled exactly code.py.
Thonny: "device is busy" when saving Ctrl+C in the shell to stop the running code, then save.
No drive appeared after flashing Expected on this board — there is no drive. Use Thonny to confirm success.
Callsign warning at boot You haven't set your real callsign in callmessage (it still contains N0CALL).
Linux "permission denied" on the port sudo usermod -a -G dialout $USER, then log out/in. If the port vanishes, sudo apt remove brltty.

Configuration reference

Everything you'd normally change lives at the top of code.py:

Setting Default What it does
callmessage "N0CALL DECOY DECOY N0CALL" Your callsign + message, sent in Morse. Set this.
frequency 146.565 Transmit frequency in MHz. Must be legal for your license/region.
transmit_delay 30000 Milliseconds between transmissions.
bandwidth 1 0 = 12.5 kHz, 1 = 25 kHz channel spacing.
squelch 3 0–8; 0 = open. Receive-side — see note below.
volume 5 1–8 speaker output level. Receive-side — see note below.
WPM 12 Morse speed in words per minute.
TONE_HZ 800 Morse tone pitch in Hz.
ARTICULATION 0.875 How much of each song-note slot actually sounds.
STATUS_LED True One LED blink per cycle when enabled.
SONG_TEMPO / SONG_NOTES William Tell (of 3 built-in) The active tune. Three public-domain tunes ship in code.py; comment/uncomment to switch. See The song.

Leave CALLSIGN_PLACEHOLDER = "N0CALL" unchanged — this is the match that will trigger the unset-callsign boot warning.

A note on squelch and volume

These two are receive-side settings. The SA868 is a full transceiver, and squelch sets the threshold at which it opens its receiver, while volume sets the level of its speaker output. code.py sends both to the module at startup because they're part of the standard SA868 setup sequence — but this fox only ever transmits, and no speaker is connected, so changing them won't audibly change anything about the fox's signal.

They're documented here because they're real settings you'll see at the top of code.py, and because the SA868 is capable of more than this firmware asks of it. If you extend the fox to listen — a receive-triggered fox, a beacon that checks whether the frequency is busy before keying — these are the knobs you'd be reaching for.

If you want the transmitted audio louder or quieter, that's a different control: modulation level is set by the PWM signal code.py generates on the MIC pin, not by volume.


Credits & license

This fox is based on the rot13labs / C0ldbru fox (https://github.com/c0ldbru/fox, GPL-3.0), by way of Bradan Lane STUDIO's CircuitPython port (2024), which itself builds on Yet Another Foxbox (YAFB) by Gregory Stoike (KN4CK) (https://github.com/N8HR/YAFB). This "basic deployment" is functionally the C0ldbru design expressed in CircuitPython as a single file.

Distributed under GPL-3.0, WITHOUT ANY WARRANTY. See https://www.gnu.org/licenses/ for the full license text.

If you redistribute or publish this, keep the attribution above and include the GPL-3.0 license — that's a requirement of the license, not just courtesy.


Happy hunting — and 73!

About

[PUBLIC] Default fox firmware used for HRV DEF CON 34 Fox Hunt

Topics

Resources

Stars

6 stars

Watchers

0 watching

Forks

Contributors

Languages