Development and Testing - oshri-almog/esp32c5-kismet-wifi-interface GitHub Wiki

This page is for contributors: how the repository is laid out, which tests exist, what each one needs and checks, and how to test on real boards. How to report a problem or send a change is on Contributing.

Repository layout

firmware/                      ESP-IDF 5.5 project: the board's firmware
  main/esp32c5_sniffer.c       the whole firmware, in one file
  main/Kconfig.projbuild       the menuconfig options ("Packet Sniffer Configuration")
  sdkconfig.defaults           the build settings a fresh clone starts from
  partitions.csv               the partition table (needs 2 MB of flash or more)
kismet/                        the Kismet side, GPL-2.0-or-later (COPYING.md says why)
  datasource_esp32c5.h         the server-side source type "esp32c5"
  capture_esp32c5/             the C helper kismet_cap_esp32c5: capture_esp32c5.c, Makefile.in
  add-to-kismet.sh             copies both into a Kismet source tree and wires them into its build
esp32c5_kismet/                the Python remote helper (python -m esp32c5_kismet.remote)
  board.py                     the serial link to a board: port lock, handshake, stream parser
  kismet_v3.py                 Kismet's external capture protocol, version 3
  remote.py                    the command line, source definitions and connections to Kismet
docker/                        Dockerfile, entrypoint.sh, kismet_site.conf
compose.yaml                   the kismet, demo and helper services
tools/fake_board.py            a pseudo-terminal that behaves like a board
tests/
  test_board.py                offline: the Python remote helper's link to a board
  test_kismet_v3.py            offline: the Python remote helper's Kismet side
  c/run.sh, c/test_parser.c    the C parser harness for the C helper
  kismet_e2e.sh                fake board -> C helper -> a real Kismet
  remote_e2e.sh                fake board -> Python remote helper -> a real Kismet
  docker_smoke.sh              the demo image, no hardware
web/                           the web flasher: its page and manifest, with placeholders that CI fills in
docs/wiki/                     the pages of this wiki
.github/workflows/docker.yml   CI: builds, smoke-tests and publishes the Docker image
.github/workflows/firmware.yml CI: builds the firmware and the flasher site, deploys the site, attaches the image to releases
.github/workflows/wiki.yml     copies docs/wiki/ to the GitHub wiki
.github/dependabot.yml         monthly updates of the pinned GitHub Actions
requirements.txt               the Python remote helper's packages
README.md, CREDITS.md, LICENSE the overview, credits and prior art, the MIT licence
  • There is no Python package to install: no setup.py, no pyproject.toml. The Python remote helper and its tests run from the repository root.
  • firmware/build/, firmware/sdkconfig, .venv/, capture files (*.pcap, *.pcapng, *.kismet) and a compose .env file, which can hold a Kismet login, are git-ignored.
  • .gitattributes keeps every text file LF on checkout, whatever core.autocrlf says. The scripts run on Linux and inside the Docker image, and a script with CRLF line endings does not start there.

The tests at a glance

Test What it checks Needs Runs on Last recorded result
tests/test_board.py The Python remote helper's link to a board: stream framing, channel specs, port names, the port lock (and on Linux the tty's exclusive mode and /proc/locks), the handshake against a fake board inside the test, finding a board by its MAC Python and pyserial Windows, Linux ALL OK on WSL2, the Pi and Windows (counts)
tests/test_kismet_v3.py The Python remote helper's Kismet side: protocol bytes, UUIDs compared with the C helper's, source definitions, one board per source, the 802.15.4 rewrap, the BTLE CRC fix-up, hopping and refused channel sets, the command line, where the login and the API key go, a redirect that must not be followed, the HTTP proxy from the environment and no_proxy, a refused login said by its status on one line, a stop signal that arrives while the helper waits, whole sessions against a fake Kismet over TCP and over a websocket, and over TLS when openssl is available Python and requirements.txt; openssl on the PATH for the TLS session Windows, Linux ALL OK on WSL2, the Pi and Windows (counts)
tests/c/run.sh The C helper: stream parser, "capturing", 802.15.4 and BTLE handling, source names, channel= and refused channels, the probe, --list, finding a board by MAC, the port lock and exclusive mode, the reason told to Kismet, the PING watchdog, the signals that end a remote helper, the capability drop, the remote login, --ssl-certificate turning on TLS, the port in the Host header gcc, and a Kismet tree patched with add-to-kismet.sh and built Linux ALL OK on WSL2 and the Pi (counts)
tests/kismet_e2e.sh The fake board, through the C helper, into a real Kismet, as a local source and over remote capture Kismet built with the esp32c5 source, python3, curl, port 2501 free; openssl for the TLS cases Linux ALL OK on WSL2 and the Pi (counts)
tests/remote_e2e.sh The fake board, through the Python remote helper, into a real Kismet, over the websocket and over legacy TCP The same, plus a Python with requirements.txt, ss and ip; ports 2511 and 3511 free Linux ALL OK on WSL2 and the Pi (counts)
tests/docker_smoke.sh The demo image: each radio, then the helper role feeding Kismet in a second container Docker, curl, Python Linux, Windows (Git Bash) 22 checks, ALL OK, in CI on amd64 and arm64
Real boards What no fake can show: the radios, the USB port, TXTEST between boards Two or more flashed boards See Testing on real hardware See that section

Only the last row needs a board. Of these tests CI runs only the Docker smoke test; it also builds the firmware, but never runs it (CI). Run the others yourself.

Last recorded runs

These are the last runs of the Python, C and end-to-end tests, all with the current code. Each number is the count of PASS lines; every run ended with ALL OK. A blank cell was not run that way. The counts grow as tests are added.

Platform Run as tests/c/run.sh kismet_e2e.sh remote_e2e.sh test_board.py test_kismet_v3.py
WSL2: Ubuntu 24.04, libwebsockets 4.3.3, Python 3.12.3 root 342 108 120 254 460
an unprivileged user (nobody) 318 + 3 SKIP
Raspberry Pi 4: Debian 13 arm64, libwebsockets 4.3.5, Python 3.13.5, websocket-client 1.9.2 a normal user 318 + 3 SKIP 104 + 1 SKIP 120 253 + 1 SKIP 460
root 337 + 1 SKIP 254
Windows 11: Python 3.13.2, websocket-client 1.9.2, pyserial 3.5 from PowerShell Linux only Linux only Linux only 238 + 2 SKIP 458 + 2 SKIP

What the SKIP lines are (the tables under the Python and C tests below give their exact text):

  • tests/c/run.sh, not as root: the 3 checks that need root: the exclusive mode, setuid root, and the capabilities. As root on the Pi: the exclusive mode across device nodes, no /dev/ttyS* without hardware to try it on.
  • kismet_e2e.sh on the Pi: case 16, a bare esp32c5 with no board, which is skipped while Espressif boards are plugged in, as they were on the Pi.
  • test_board.py on the Pi, not as root: the open refused with EBUSY, which needs root and capsh.
  • Windows: test_board.py skips the port key through a symbolic link (no permission to make one) and the POSIX-only pseudo-terminal and lock tests. test_kismet_v3.py skips the real symbolic link, for the same reason, and the TLS session, as there was no openssl on the PATH. With Git's openssl on the PATH the TLS session runs: 460 plus 1 SKIP.

In case 10 of kismet_e2e.sh the first packet reached Kismet over --connect 939 ms after the helper started in WSL2, and 910 ms after it on the Pi.

Which tests to run for a change

You changed Run
esp32c5_kismet/ test_board.py and test_kismet_v3.py; on Linux also remote_e2e.sh
kismet/capture_esp32c5/capture_esp32c5.c tests/c/run.sh, kismet_e2e.sh, remote_e2e.sh (it checks that the C helper is refused a board the Python remote helper holds), the Docker smoke test
kismet/datasource_esp32c5.h, kismet/add-to-kismet.sh, kismet/capture_esp32c5/Makefile.in Rebuild Kismet with the script, then everything in the row above
tools/fake_board.py kismet_e2e.sh, remote_e2e.sh, the Docker smoke test
docker/, compose.yaml, .dockerignore The Docker smoke test, and a check of each compose profile: docker compose config -q, docker compose --profile demo config -q and docker compose --profile helper config -q
firmware/ Build it, then the hardware checks. CI builds it too, but nothing automated runs the firmware: every other test uses a fake board. Whatever reaches main is what the web flasher installs.
web/ Look at the page that CI builds for the pull request: download the run's site artifact, unpack it, serve it with python -m http.server in that folder and open http://localhost:8000 in Chrome or Edge. Web Serial works only on HTTPS or localhost, so opening the file straight from disk is no test of the Install button.
The line protocol or the stream format All of the above, with the change made in both helpers and in the fake board

Setting up

Python

On Linux, use a virtual environment in the repository root (.venv is git-ignored):

cd esp32c5-kismet-wifi-interface
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt

On Windows, in PowerShell:

cd esp32c5-kismet-wifi-interface
python -m pip install -r requirements.txt
  • requirements.txt asks for pyserial, msgpack and websocket-client 1.9.1 or newer. websocket-client 1.9.1 needs Python 3.10 or newer, so requirements.txt needs 3.10. The helper has run with real boards on Python 3.13 (3.13.2 on Windows 11, 3.13.5 on Raspberry Pi OS with Debian 13), and its tests also on 3.12.3 (Ubuntu 24.04 in WSL2). On the Pi an earlier version of it also passed its tests and captured from a board on 3.10, 3.11 and 3.12. Python 3.9 cannot install requirements.txt; with websocket-client 1.8.0 instead, that earlier version's tests passed and a capture ran there too.
  • On Debian and Ubuntu, install with pip into a virtual environment (sudo apt install python3-venv if venv is missing), not from apt. The packaged python3-websocket is 1.7.0 on Ubuntu 24.04 and 1.8.0 on Debian 13. Versions before 1.9.1 raise an error on Linux when Kismet resets the connection. The helper recovers from that error (on the Pi it reconnected after Kismet was killed, with 1.7.0, 1.8.0 and 1.9.2 alike), but install with pip so that you get 1.9.1 or newer.

Linux tools for the C harness and the end-to-end tests

  • A Kismet source tree at commit cfe427074, patched with kismet/add-to-kismet.sh, configured, built and installed. Follow Building Kismet with ESP32-C5 Support. The examples on this page use the layout of the Raspberry Pi that was tested: the tree in ~/src/kismet, Kismet installed to ~/kismet-install.
  • gcc (Debian's build-essential), python3 and curl.
  • WSL2 works for all of it: the C harness and both end-to-end tests have run on WSL2. See Install on WSL2.

Warning: Build Kismet in WSL2 with make -j4 at most. A make -j20 there used up the host's memory, and the WSL distribution had to be terminated. On a Raspberry Pi 4 with 8 GB, make -j4 took about 78 minutes.

The offline Python tests

From the repository root, on Linux:

.venv/bin/python tests/test_board.py
.venv/bin/python tests/test_kismet_v3.py

On Windows:

python tests/test_board.py
python tests/test_kismet_v3.py
  • Neither file opens a serial port or needs Kismet. test_board.py has a fake board of its own inside the test. test_kismet_v3.py also plays Kismet, over TCP and over a websocket, and over a websocket with TLS when openssl is on the PATH to make a test certificate.
  • test_board.py feeds the Python remote helper the same streams that tests/c/test_parser.c feeds the C helper, so both helpers are held to one parser rule.
  • test_kismet_v3.py compares the Python remote helper's UUIDs with values compiled from the C helper's code.
  • Each check prints a PASS line. The first FAIL stops the file with exit status 1. A clean run ends with ALL OK and exit status 0.
  • Both files remove the temporary directories they make when they exit.

These SKIP lines are expected:

Line File Why
SKIP pseudo-terminal and lock tests (POSIX only) test_board.py, on Windows Pseudo-terminals and flock exist only on POSIX
SKIP the helper stopped with Ctrl+Break twice (no console to send a console event in) test_kismet_v3.py, on Windows The test runs without a console window. Ctrl+Break reaches the helper the check starts only through a console the two share
SKIP port key through a symbolic link (no permission to make one here) test_board.py Windows without the right to make symbolic links
SKIP a real symbolic link (no permission to make one here) test_kismet_v3.py The same
SKIP an open refused with EBUSY (needs root and capsh, to try it without CAP_SYS_ADMIN) test_board.py, on Linux Not run as root, or no capsh. The check opens a port held in exclusive mode from a process without CAP_SYS_ADMIN, since the exclusive mode lets a process with it through
SKIP wss to localhost with a certificate for localhost (no openssl to make one) test_kismet_v3.py No openssl on the PATH to make a test certificate

Before you send a change to board.py, also run both files on Linux: the pseudo-terminal and lock cases run only on POSIX, and the exclusive-mode and /proc/locks cases only on Linux (on other POSIX systems they are left out without a word). The counts of the last runs are under Last recorded runs. An earlier version of test_kismet_v3.py also passed on Linux with websocket-client 1.7.0, 1.8.0 and 1.9.1.

Several cases in test_kismet_v3.py make the helper log errors on purpose, so its log is hidden. TEST_DEBUG=1 shows it:

TEST_DEBUG=1 .venv/bin/python tests/test_kismet_v3.py

The C parser harness

tests/c/run.sh compiles tests/c/test_parser.c, which includes this repository's kismet/capture_esp32c5/capture_esp32c5.c as it is. It builds against a Kismet tree's config.h, capture_framework.h and libkismetdatasource.a. The calls that would reach a Kismet server are replaced by stubs that record what they are given; the rest of the capture framework is Kismet's own. Linux only.

  1. Build Kismet with the source once: the tree must have been through add-to-kismet.sh, ./configure and make.

  2. Run the harness against that tree:

    sh tests/c/run.sh ~/src/kismet

    Without an argument it uses $KISMET_SRC, then ~/src/kismet. Set CC to use a compiler other than gcc.

It compiles the repository's copy of the helper, not the tree's, so you can re-run it after every edit without copying anything into the tree or rebuilding Kismet.

Part What it checks
test_framing The stream parser: start markers with and without the helper's nonce, the PCAP header, records, damaged records; fed in chunks of 1, 3, 7, 11, 64 and 4096 bytes and all at once
test_capturing "capturing" is said only once the PCAP header has the radio's link type
test_injection Frames whose payload holds the whole restart signature (<<START>> and a PCAP header) are passed on as data
test_154, test_wifi_freq The 802.15.4 TAP header rewrapped as link type 230, with channel and signal; the frequency in every radio's signal block
test_btle The CRC fix-up for older firmware (flags 0x0013 against 0x0C13), with a CRC vector that Wireshark's BTLE dissector accepts
test_definitions Source names, mode=, channel=
test_sysfs --list and finding a board by its MAC
test_probe, test_remote_busy Which definitions the helper claims when Kismet probes; a remote helper that does not offer a port another process holds
test_open, test_exclusive Opening a port, the port lock, and the tty's exclusive mode; channel sets, BTLE 38 and 39 reported as 37, and a refused channel
test_say_why, test_ping_watchdog The reason told to Kismet, as a message and as an error; the websocket PING watchdog
test_end_on_signal, test_end_with_parent The signals that end a remote helper (not one it was started ignoring, as under nohup), and its capture process ending with its parent
test_capabilities The helper dropping every capability, as root, with Docker's default set and installed setuid root; setuid root still opening a user's own pseudo-terminal
test_login The remote login from KISMET_CAP_APIKEY, KISMET_CAP_USER and KISMET_CAP_PASSWORD, read as the framework reads it; the Basic Authorization header and the KISMET cookie; a user name with ':' in the URI; a login too long refused; none of it over --tcp; the warning for a login that cannot log in
test_ssl_and_host --ssl-certificate in each spelling turning on TLS, but not over --tcp; the Host header with the port, without it for the scheme's own port (80, or 443 with --ssl), and the length limit of libwebsockets before 4.2
  • The boards it lists and finds sit on a sysfs tree the test makes up under /tmp, and their ports are pseudo-terminals. Boards plugged into the machine make no difference.
  • Run it as root to include the checks that need root: the exclusive mode (on a /dev/ttyS* with no hardware behind it, when the machine has one; a port with hardware is not touched), the capabilities, and setuid root. The login checks test the framework as add-to-kismet.sh patches it, so the tree must have been patched by this repository's script.
  • Each check prints PASS or FAIL. The harness runs every check, then prints ALL OK (exit status 0) or <n> FAILED (exit status 1). A build that fails also gives exit status 1.
  • A tree without the files it needs gives <tree>/<file> is missing: give a Kismet tree patched with kismet/add-to-kismet.sh and built and exit status 2.
  • The counts of the last runs, as root and as another user, are under Last recorded runs. In the helper's reviews, deliberate bugs (mutations) were put into it one at a time to check that these tests catch them.

These SKIP lines are expected:

Line Why
SKIP exclusive mode: needs root, to make device nodes and to open as a process without CAP_SYS_ADMIN Not run as root
SKIP exclusive mode across device nodes: no /dev/ttyS* without hardware to try it on Run as root on a machine with no spare /dev/ttyS*
SKIP setuid root: the capture process ends with its parent after opening a port of the user's: needs root Not run as root
SKIP capabilities as root, in Docker and setuid root: needs root Not run as root
SKIP exclusive mode: built without libcap, or no TIOCGEXCL, SKIP capabilities: built without libcap, the helper keeps what it is given A Kismet configured without libcap: the helper then drops no capability

The fake board

tools/fake_board.py makes a pseudo-terminal that speaks the firmware's line protocol and streams made-up traffic that depends on the channel it is tuned to. The end-to-end tests, the Docker demo image and Try It Without Hardware all use it instead of a board. It needs only Python's standard library, and it runs on POSIX systems only: Linux, WSL2.

python3 tools/fake_board.py /tmp/esp32c5-fake

It puts a link to a new /dev/pts/N at the path you give, starts in Wi-Fi mode and prints what it does on its standard output:

[fake] booted in wifi mode on /tmp/esp32c5-fake -> /dev/pts/3

In a second terminal, give it to Kismet as a source. /tmp/esp32c5-fake is the path from the first command:

kismet -c esp32c5:device=/tmp/esp32c5-fake

The C helper also accepts the pseudo-terminal by name, for example esp32c5-pts/3.

Run the fake board as the same user as Kismet, or as the helper that opens it: the pseudo-terminal belongs to whoever starts the fake board (mode 0620), and kismet_cap_esp32c5 drops every capability, root's power to open other users' files included. For a Kismet started with sudo, start the fake board with sudo too.

Options

python3 tools/fake_board.py PATH [MODE] [--garble N] [--inject N] [--restart-every N] [--vanish] [--old-firmware] [--lacks RADIO] [--silent]
Option What it does
PATH Where the port appears. It is a symbolic link to the pseudo-terminal.
MODE The radio it boots with: WIFI (the default), 802154 (also ZIGBEE, THREAD) or BLE (also BT, BLUETOOTH)
--garble N Every Nth record gets a header no stream can contain, so the helper has to resynchronise
--inject N Every Nth record is a frame whose payload holds the whole restart signature, <<START>> and a PCAP header, as anyone on the air could send
--restart-every N Every Nth record the board restarts its stream in place, as after a reset that keeps the port: alternately right after a record and after a record cut short
--vanish On a radio change the port goes away for 2 s and comes back as a new pseudo-terminal behind the same path. Without it the port stays open through the reboot, as it did on the real boards.
--old-firmware BTLE records the way older firmware sends them: no CRC flags, CRC zeroed
--lacks RADIO Firmware without that radio, BLE or 802154, as the sibling project's 1.0.0 and 1.1.0 are: MODE for it is ignored, and START is answered in the link type of the radio the board is on
--silent Says nothing at all and ignores what it is sent, as a board with other firmware, or stuck in its ROM download mode, does

The Nth record is counted from the last START the fake answered, so each of these lands in a stream a helper asked for and is reading.

What it sends

Radio Traffic What Kismet shows
Wi-Fi Beacons from three access points: ESP32C5-FAKE-24 on channel 6, ESP32C5-FAKE-5LOW on 36, ESP32C5-FAKE-5HIGH on 149 The three access points, once hopping reaches their channels
802.15.4 Data frames to broadcast from node 0x1001 on channel 15 and node 0x2501 on channel 25 Devices 10:01, 25:01 and FF:FF
BTLE Advertisements from C6:00:00:C5:E5:5A, named ESP32C5-FAKE The advertiser ESP32C5-FAKE

It sends about 100 records a second while tuned to a channel with traffic, and nothing on other channels.

How it differs from a real board

  • A radio change reboots it in 0.53 s, the time measured on the real board from MODE to the boot marker. Anything sent during the reboot is lost, and the fake logs it as [fake] rebooting, lost: <line>. The end-to-end tests use that line to check that no START is lost.
  • It remembers its radio only while it runs. Each start boots in the MODE on its command line.
  • It reads command words in any case. The firmware's are case-sensitive: it refuses start.
  • It ignores TXTEST and any command it does not know, and it has no UART log and no drop counters.
  • It refuses CHANNELS 0 and CHANNELS AUTO, which make the firmware go back to its built-in list, and in 802.15.4 it starts locked on channel 15, where the firmware hops 11 to 26. Under Kismet neither matters: the helpers always send a channel.
  • Its timestamps are wall-clock time whatever START says.
  • It prints ESP-ROM:esp32c5-fake before its first boot marker, so the helpers' handling of boot text gets exercised.

The Kismet end-to-end tests

Both scripts start their own Kismet with a login in a temporary --homedir and logging off, feed it from the fake board, and check what Kismet reports through its REST API.

flowchart LR
    F["tools/fake_board.py<br/>pseudo-terminal"] -->|"line protocol,<br/>PCAP stream"| C["kismet_cap_esp32c5<br/>(kismet_e2e.sh)"]
    F -->|"line protocol,<br/>PCAP stream"| P["python -m esp32c5_kismet.remote<br/>(remote_e2e.sh)"]
    C -->|"IPC: packets"| K["Kismet<br/>temporary --homedir, --no-logging"]
    K -.->|"starts"| C
    P -->|"websocket or --tcp"| K
    T["the test script"] -->|"REST: sources, devices"| K
Loading
  • Both need a Kismet built with the esp32c5 source and installed, with kismet_cap_esp32c5 next to kismet. Kismet starts helpers only from the bin directory it was configured with (--prefix or --bindir), not from the directory the kismet binary runs from. Point KISMET at an installed kismet: one left in the source tree runs whatever kismet_cap_esp32c5 is installed under its prefix.
  • Both print a == <case> heading, a few lines of what Kismet reported, PASS or FAIL for each check, and ALL OK at the end.
  • On a failure they exit with status 1, keep their logs in a temporary directory and print where it is (--- logs kept in <dir>).
  • Each case runs for 15 to 40 seconds, so a full run takes several minutes.

tests/kismet_e2e.sh: the C helper

KISMET=~/kismet-install/bin/kismet sh tests/kismet_e2e.sh

Without KISMET it runs the kismet on the PATH. It starts Kismet on port 2501, so stop any other Kismet first. It needs python3 and curl, but no Python packages, and openssl to make certificates for the TLS cases, which print SKIP without it. The remote cases run the kismet_cap_esp32c5 next to KISMET, or the one HELPER names, and reach Kismet through a relay of the script's own that logs the head of every request. Every Kismet it starts has the login e2e, with a password that holds '&', a space and %41. The cases:

  1. Wi-Fi on 2.4 and 5 GHz with Kismet hopping, and a damaged record every 50.
  2. Frames whose payload holds the whole restart signature.
  3. A board that restarts its stream in place every 40 records.
  4. 802.15.4 as esp32c5zigbee-<port>, with the board booting in Wi-Fi and rebooting, its port staying open.
  5. 802.15.4 with --vanish: the port goes away in the reboot and comes back as another.
  6. Wi-Fi locked with channel=36,channel_hop=false, and a second source on the same board, which must be refused as in use.
  7. BTLE, set to channel 38 and shown as 37, then set to 40, which the helper refuses while the source goes on capturing.
  8. BTLE from older firmware, which the helper fixes up.
  9. BTLE on firmware without it (--lacks BLE): one lost sync per attempt, never "capturing", and the helper's 15 s reason in Kismet's log.
  10. Remote capture over the websocket (--connect, with retry, started as nohup starts it): the login in an Authorization header and nowhere in the request line, the port in the Host and Origin headers, the first packet within 3 s and more every second after, no empty INFO: line after a channel set, no libwebsockets notices, a second remote helper for the same board refused, close_source.cmd followed by a reconnect, and kill -TERM ending the capture process with the helper.
  11. An API key, which must go in Kismet's session cookie and not in the request line. The script prints only the key's length, a datasource key from Kismet (<n> characters), so the key stays out of saved test logs.
  12. A login, and then an API key, too long for the request's headers: each refused with a reason, nothing sent.
  13. A websocket answered with a redirect, eight ways: to another host, to another port, to https://, to a path on the same server, with no Location, echoing the request's query with the login in it, with control bytes in the Location, and over TLS. In each, the helper must make exactly one request and nothing may connect to where the redirect points; the helper must say so with the status and where it pointed, without the query and with control bytes percent-encoded, and end.
  14. Remote capture over TLS, through a relay of the script's own, with --ssl-certificate and no --ssl: the helper must speak TLS, check the relay's certificate against that CA, and capture, with the bare host as TLS's server name, the port in Host and Origin, and the login in the Basic header.
  15. No libwebsockets rejecting message on queue depth warnings from a helper in a network namespace of its own with 200 routes. This needs unshare and ip, and root or user namespaces; the case prints SKIP without them.
  16. A bare esp32c5 with no board plugged in. Kismet has to hand it to the helper and keep retrying, instead of giving up with Unable to find driver. This case is skipped when an Espressif USB-Serial-JTAG device (USB ID 303a:1001) is plugged in.

The counts of the last runs, and how soon the first packet came in case 10, are under Last recorded runs.

tests/remote_e2e.sh: the Python remote helper

KISMET=~/kismet-install/bin/kismet PYTHON="$PWD/.venv/bin/python" sh tests/remote_e2e.sh
  • PYTHON must have pyserial, msgpack and websocket-client. Otherwise the script stops with the helper needs pyserial, msgpack and websocket-client for <python> (PYTHONPATH=<path>). Give it as an absolute path: the script starts the helper from a temporary directory.
  • It also needs python3 on the PATH for its own checks, curl, ss and ip. ip finds this machine's own address for the proxy case; without it, or on a machine with only loopback addresses, that part prints SKIP.
  • Its Kismet listens on port 2511 for the web and on 3511 for legacy TCP remote capture, so a Kismet on 2501 and 3501 is left alone. Set HTTP_PORT and TCP_PORT to use other ports.
  • It stops only the processes it started.

The cases:

  1. Wi-Fi over the websocket, the helper stopped with SIGTERM. Every websocket case logs in with a password that holds '&', which the helper sends in an Authorization header.
  2. Wi-Fi over legacy TCP as esp32c5-<port>, stopped with SIGINT.
  3. 802.15.4 as esp32c5zigbee-<port>, with the board rebooting in place.
  4. BTLE, with the login from KISMET_CAP_USER and KISMET_CAP_PASSWORD; the password must not appear on the helper's command line. A channel set to 38 must show as 37, and one to 40 must be refused as the C helper refuses it: HTTP 200, the source going on on 37, and the refusal in Kismet's log.
  5. BTLE from older firmware, with an API key (role datasource) from KISMET_CAP_APIKEY, through a proxy that logs each request: the key must go in the KISMET cookie, and no request line may hold it.
  6. Frames whose payload holds the restart signature.
  7. A board that restarts its stream in place.
  8. channel=36,channel_hop=false on a source Kismet already knows as hopping. Then a second source on the same board: from a second Python remote helper, on another radio or as the very same source, which must not be offered to Kismet at all; from kismet_cap_esp32c5; and twice in one helper, which must refuse to start.
  9. A board another process holds: not offered until it is free, while the helper's other source captures, and taken within about 5 s of its release.
  10. A wrong API key: Kismet's refusal must be one log line, 401 Unauthorized and the hint, at every attempt, with nothing of Kismet's page and not the key.
  11. An HTTP proxy in http_proxy: --connect 127.0.0.1 must not use it; this machine's own address must go through it, with CONNECT, and capture; and with no_proxy covering that address the helper must go direct and capture.
  12. Kismet killed and started again: the helper must reconnect with the same UUID.

Every time the helper is stopped it must exit with status 0, within 10 s, and nothing may be left running at the end. The counts of the last runs are under Last recorded runs.

The Docker smoke test

tests/docker_smoke.sh runs the demo image with its fake board in each radio. Then it runs the helper role in one container, feeding Kismet in a second one over remote capture. No hardware is needed.

  1. Build the demo image from the repository root:

    docker build -f docker/Dockerfile --target demo -t esp32c5-kismet:demo .
  2. Run the test on Linux:

    sh tests/docker_smoke.sh esp32c5-kismet:demo

    On Windows, in Git Bash, where Python is python:

    PYTHON=python sh tests/docker_smoke.sh esp32c5-kismet:demo

    Where your user is not in the docker group, as on the tested Pi, both commands fail with a permission error on /var/run/docker.sock. Run both with sudo there:

    sudo docker build -f docker/Dockerfile --target demo -t esp32c5-kismet:demo .
    sudo sh tests/docker_smoke.sh esp32c5-kismet:demo

    The Pi built its images this way; the smoke test itself has not been run on the Pi. Its recorded runs are on Docker Desktop and in CI.

  • It needs Docker, curl and Python on the host (PYTHON, default python3).
  • It uses host port 2599 and containers and a network named esp32c5-smoke*, and removes them afterwards with their volumes. Set SMOKE_PORT and SMOKE_NAME to run it beside another copy.
  • Every container runs with Docker's default capabilities and no more, so the test also shows that nothing in the image needs NET_ADMIN. It also checks that Kismet's list of interfaces (the web UI's Data Sources window) answers, which it never does when one of Kismet's own capture helpers crashes on start.
  • The helper role logs in with a password that holds '&', a space and %41. An image built before the login moved into HTTP headers fails that part: its entrypoint refuses such a password.
  • It prints PASS or FAIL for each check and ALL OK at the end; the exit status is 0 or 1.
  • The first build of the demo image took about 18.5 minutes on a fast Windows PC, and rebuilds from cache took seconds. On GitHub's runners it takes about 25 to 30 minutes. On a Raspberry Pi 4 (8 GB), building both images took about 80 minutes, almost all of it the Kismet compile.
  • CI ran it on the current code, on amd64 and on arm64: all 22 checks passed, in about 2 minutes each.

CI

There are three workflows: .github/workflows/docker.yml, named "Docker image", .github/workflows/firmware.yml, named "Firmware and web flasher", and .github/workflows/wiki.yml, named "Wiki", which copies docs/wiki/ to the GitHub wiki on every change to it on main, or when run by hand.

The Docker image

When Pushes to main and pull requests that change kismet/**, docker/**, .dockerignore, tools/fake_board.py, tests/docker_smoke.sh or the workflow itself; version tags such as v1.2.3 and v1.2.3-rc.1, whatever they change; manual runs
Where Each architecture on its own native runner: linux/amd64 on ubuntu-24.04 (120-minute limit), linux/arm64 on ubuntu-24.04-arm (180-minute limit). Under emulation the Kismet build would take hours.
What Build the demo image with the GitHub Actions cache, run tests/docker_smoke.sh on it, then build the Kismet image
Publishing Only for version tags and manual runs: both architectures are pushed by digest, then joined under the image tags listed on Docker Reference
Actions Pinned to commit SHAs; .github/dependabot.yml updates them monthly

CI has run on GitHub for pushes to main, for pull requests and for the version tag v0.1.0: each time both architectures built the demo image and passed the smoke test. The v0.1.0 run published the images to ghcr.io/oshri-almog/esp32c5-kismet, as 0.1.0, 0.1, latest and sha-b5fa22a, and the demo as 0.1.0-demo, demo and sha-b5fa22a-demo, each tag for amd64 and arm64. The package is public, so pulls need no login. No manual run has been made, so that way of publishing has not run.

The firmware and the web flasher

When Pull requests and pushes to main that change firmware/**, web/** or the workflow itself; version tags such as v1.2.3 and v1.2.3-rc.1, whatever they change; manual runs
What Build the firmware for esp32c5 in the espressif/idf:v5.5.5 container and make the merged image as on Flashing the Firmware. Then build the site from web/: the image as firmware/esp32c5-kismet-merged.bin, and the version (git describe), the commit and the image's SHA-256 filled into the page and the manifest. The SHA-256 also goes on the manifest's and the image's URLs as ?sha256=, so a browser holding Pages' 10-minute cache of an older build fetches the new files. Both are kept as artifacts of the run, firmware and site. The build fails if the firmware's version is not what git describe gives, or ends in -dirty
Pull requests Build only: nothing is deployed or released
Pages Pushes to main and manual runs on main deploy the site to GitHub Pages, https://oshri-almog.github.io/esp32c5-kismet-wifi-interface/. Runs on the same branch or tag take turns, so two deploys never run at once and an older build never replaces a newer site; a newer push to a pull request cancels its older run. Version tags do not deploy it
Releases A version tag attaches the merged image to the GitHub Release of that tag, as esp32c5-kismet-<tag>-merged.bin with a .sha256 file. If there is no release, it creates one as a draft and publishes it once the files are on it, since a published release takes no more files when the repository has immutable releases turned on; a draft that exists is published the same way. A tag on a commit that does not have this workflow, such as one made before it was added, starts no run and gets no image. A failed upload is retried by re-running the failed job; a manual run on a tag only builds. Only this job may write to the repository's contents
Actions Pinned to commit SHAs; .github/dependabot.yml updates them monthly. The ESP-IDF container is pinned by its digest too, which Dependabot does not update: change its tag and digest together

In the artifacts of other runs the image is named esp32c5-kismet-<version>-merged.bin after git describe; on the site it is always firmware/esp32c5-kismet-merged.bin. For the deploy to work, the repository's Pages source has to be "GitHub Actions" (Settings → Pages → Build and deployment); the github-pages environment lets only the default branch deploy.

The tag v0.1.0 ran the build and the release job: its release has esp32c5-kismet-v0.1.0-merged.bin and esp32c5-kismet-v0.1.0-merged.bin.sha256. The site that offers v0.1.0 was deployed from the same commit by a manual run on main, which built the firmware again. Both files are the same firmware, version v0.1.0, but from separate runs, so their SHA-256 differ.

What CI does not run

CI does not run the Python offline tests, the C harness or the end-to-end tests, and it never runs the firmware on a board. A pull request that only touches esp32c5_kismet/, compose.yaml, requirements.txt, or files in tests/ other than docker_smoke.sh runs no CI at all. Run the tests for your change yourself, and say in the pull request which ones you ran.

Working on the C helper

  1. Build Kismet with the source once (Building Kismet with ESP32-C5 Support).

  2. Edit kismet/capture_esp32c5/capture_esp32c5.c, and run the harness after each change:

    sh tests/c/run.sh ~/src/kismet
  3. Get the changed files into the tree and build it. From the repository root, run the script again, then make:

    sh kismet/add-to-kismet.sh ~/src/kismet
    make -C ~/src/kismet

    The script copies a file into the tree only when it differs from the tree's copy (it prints copied <file> for each one), and an unchanged file keeps its time. So after a change to capture_esp32c5.c alone, make rebuilds only the helper. After a change to datasource_esp32c5.h, which kismet_server.cc includes, it also recompiles kismet_server.cc and relinks kismet (about 490 MB with its debug information): on the Raspberry Pi 4 a build that did so took about 3 minutes. A run that adds one of the script's capture framework fixes recompiles every capture helper once, but not kismet: on the Raspberry Pi 4 that took 8.5 s.

    After a change to capture_esp32c5/Makefile.in, run ./configure in the tree again, with the flags you used the first time, before make: the helper's Makefile is made from it by configure.

  4. Install it next to kismet, the way you installed Kismet. The example is for a Kismet installed under your home directory without sudo, as on the tested Pi:

    make -C ~/src/kismet install INSTUSR=$(id -un) INSTGRP=$(id -gn) SUIDGROUP=$(id -gn)

    make install never replaces configuration files that are already installed.

  5. Run the end-to-end tests:

    KISMET=~/kismet-install/bin/kismet sh tests/kismet_e2e.sh

Note: After a run of the script that edits Kismet's Makefile.in or regenerates configure, as the first run on a tree does, make prints 'Makefile.in' or 'configure' are more current than this Makefile. You should re-run 'configure'. on every run until ./configure runs again. It is only a notice, and make carries on. A later run that finds everything in place changes neither file, and the notice does not come back. Run ./configure again, with the flags you used the first time, after a change to capture_esp32c5/Makefile.in (step 3) and on a tree configured before the script's first run: its Makefile was made from Kismet's own Makefile.in, which has no esp32c5 helper, so it builds without it.

For the Docker image, rebuild the demo target and run the smoke test. The image builds Kismet with a stand-in for the helper's source and copies the real capture_esp32c5.c in only afterwards, so a change to that file alone leaves the Kismet build cached and rebuilds only the helper and the layers after it: about a minute on the Raspberry Pi 4. A change to datasource_esp32c5.h, add-to-kismet.sh or Makefile.in rebuilds Kismet.

Moving to a newer Kismet commit

No Kismet release has the esp32c5 source yet, so the project pins Kismet commit cfe427074. On the Pi, kismet --version printed Kismet 2026.09.0-cfe427074. To try another commit (change <commit> to the one you want):

  1. Clone Kismet into a fresh directory, check out the commit and add the source. Run the last command from the repository root:

    git clone https://github.com/kismetwireless/kismet.git ~/src/kismet-next
    git -C ~/src/kismet-next checkout <commit>
    sh kismet/add-to-kismet.sh ~/src/kismet-next
  2. If the script stops with anchor not found, Kismet has changed: <anchor>, Kismet has moved the CatSniffer helper's lines that the script anchors its edits on. Update the anchors in add-to-kismet.sh.

  3. The script's seven fixes to capture_framework.c (the metadata leak, the websocket's 5 s bursts, a closed websocket's missed wake-up, the login in HTTP headers with redirects refused, libwebsockets' queue warnings, the empty INFO: line, the port in the Host header) are each skipped once Kismet has the fix itself; the login and Host header fixes are recognised only in this script's own version. If the code a fix replaces has changed shape, the script prints a note naming it and carries on, for example capture_framework.c: cf_commit_packet has changed, its metadata leak not fixed, capture_framework.c: the websocket login has changed, it still goes in the URI or capture_framework.c: the websocket's Host header has changed, its port not added. Check each note: the helper still builds without the fix, but keeps the bug it fixed. Building Kismet with ESP32-C5 Support lists every note.

  4. Configure the tree with a prefix of its own, build it and install it there. Kismet starts helpers from the bin directory it was configured with, so the new kismet has to be installed before it runs its own kismet_cap_esp32c5. With the same prefix as your working Kismet, make install would overwrite that install. The configure flags are the ones from Building Kismet with ESP32-C5 Support with another --prefix. The example installs into ~/kismet-next-install without sudo:

    cd ~/src/kismet-next
    ./configure --prefix=$HOME/kismet-next-install --disable-python-tools --disable-librtlsdr \
        --disable-ubertooth --disable-bladerf --disable-btgeiger
    make -j4
    make install INSTUSR=$(id -un) INSTGRP=$(id -gn) SUIDGROUP=$(id -gn)
  5. From the repository root, run the C harness and both end-to-end tests against the new tree and its install:

    sh tests/c/run.sh ~/src/kismet-next
    KISMET=~/kismet-next-install/bin/kismet sh tests/kismet_e2e.sh
    KISMET=~/kismet-next-install/bin/kismet PYTHON="$PWD/.venv/bin/python" sh tests/remote_e2e.sh

    Each test runs the helper installed next to the kismet that KISMET names, so these use the new install, not your working one. This layout, a second install beside the first, has not been tried yet.

  6. For the image, pass the commit as a build argument and run the smoke test. The default is KISMET_REF in docker/Dockerfile.

    docker build -f docker/Dockerfile --target demo --build-arg KISMET_REF=<commit> -t esp32c5-kismet:demo .

Building the firmware

From an ESP-IDF 5.5 shell:

cd firmware
idf.py set-target esp32c5
idf.py build
idf.py merge-bin -o esp32c5-kismet-merged.bin
  • set-target is needed once. merge-bin builds first and writes firmware/build/esp32c5-kismet-merged.bin, which flashes at offset 0x0. Flashing, backups and board quirks are on Flashing the Firmware.
  • warning: ignoring malformed line is harmless: sdkconfig.defaults starts with a UTF-8 byte order mark, and the line it names is a comment.
  • The app version is what git describe prints. In a tree with no commits it is 1, with a Could not use 'git describe' to determine PROJECT_VER warning. In a tree with uncommitted changes it ends in -dirty. Build any firmware you publish from a clean, committed tree.
  • CI builds the firmware the same way, in Espressif's espressif/idf:v5.5.5 container, from a checkout with the whole history and the tags, so that git describe gives the same version as in a clone. The web flasher and the releases offer merged images built this way (CI). Each run builds its own, and the build writes its time into the bootloader and the app, so two builds of the same commit differ in a few bytes and have different SHA-256. For v0.1.0, the release's file and the flasher's differ in 108 bytes: the build times, and the hashes and checksums that follow from them.
  • The build options are under idf.py menuconfig → Packet Sniffer Configuration.

Testing on real hardware

Only capture on networks and devices you own or are authorised to test.

What has been tested

Setup What ran
Raspberry Pi 4, 8 GB, Debian 13 (trixie) arm64; four ESP32-C5 boards on a powered USB hub, as /dev/ttyACM0 to /dev/ttyACM3 The native Kismet build; backups and flashing of all four boards; each radio into Kismet through the C helper; TXTEST between every pair of boards; the C helper and the Python remote helper as remote sources; four boards as sources at once; the sibling project's firmware 1.0.0, 1.1.0 and 1.2.0 on one board; the Docker build of both images (about 80 minutes) and the Kismet image with the four boards; on 2026-10-02, the current helpers with the boards: their latest changes to remote capture, and four sources at once; and the current Docker image, built on the Pi, with the boards: the kismet service, the helper role and the demo
Windows 11; two of the same boards, as COM30 and COM32 The Python remote helper feeding Kismet in WSL2 and in Docker Desktop, with an early version of the helper. On 2026-10-02, with the helper as of commit f8e6792 and one board on COM32: feeding Kismet on the Pi across the local network (Wi-Fi and BLE captured; the 802.15.4 source ran but received nothing) and Kismet in WSL2 (Wi-Fi), from PowerShell, cmd and Git Bash, with every login form, and stopping with Ctrl+C, Ctrl+Break and kill -INT. Later that day, the current helper with the same board, feeding Kismet in WSL2 (not the Pi): Wi-Fi, BLE and 802.15.4; a Kismet restart; a refused login on one line; two helpers on one board; http_proxy set with --connect 127.0.0.1; and stopping with one, two or three presses of Ctrl+C or Ctrl+Break
GitHub Actions, Ubuntu 24.04 runners, amd64 and arm64 The Docker image build and the smoke test (CI)
Not run macOS, the BSDs, Fedora and Arch; a setuid-root install on real boards; Kismet's web UI in a browser (the tests read Kismet's REST API)

The hardware results on this wiki come from runs on the Pi, the latest on 2026-10-02 with the boards on firmware image 01a50bd6 (the start of the merged image's SHA-256). One run that day used the helpers as they were just before the latest changes to remote capture, and put the previous round of changes on real boards: logins and API keys through a logging proxy and through TLS, a user name with ':', a refused BTLE channel 40 with both helpers, the helpers' stderr, a missing named port, four sources at once, a four-board Wi-Fi survey and 30 minutes of Kismet logging from two boards. A later one ran the code with those changes, with libwebsockets 4.3.5, Python 3.13.5 and websocket-client 1.9.2: add-to-kismet.sh brought the tree that the script's previous version had patched up to date in place (a second run changed nothing, and the result matched a fresh cfe427074 tree patched by the same script), the rebuilt helper was installed without sudo, the test suites passed, and the latest changes ran with the boards: redirects refused by both helpers without connecting where they pointed, the port in Host and Origin for both, the C helper's TLS with --ssl-certificate alone, the Python remote helper's proxy rules and its one-line refusal, its corrected status texts, and 200 of 200 TXTEST frames through the C helper and through the Python remote helper. The last, with the helpers as they are now, passed the test suites again and ran four sources at once, through the C helper started by Kismet and through one Python remote helper process, without an error while the BTLE board kept its radio; starts that switched it to BLE together with two other boards still made it drop off USB, and sometimes hang (Troubleshooting). The Docker image's run with the boards is on Install with Docker. Still to check on hardware lists what needs a new run.

Seeing what the firmware does

The board's native USB port carries only the capture stream and your command lines. The firmware answers nothing on USB except START: whether a command was accepted or refused shows only in its log on UART0.

  • UART0 runs at 115200 baud, 8N1, TX on GPIO11 and RX on GPIO12. On a devkit it is the USB connector marked UART. A board with only the native USB-C port, such as the Seeed Studio XIAO ESP32C5, needs a USB-UART adapter on those two pins.

  • A status line comes every 10 s, with the drop counters that are reported nowhere else:

    I (123456) sniffer: Wi-Fi ch   6 of 1 (250 ms) | captured 1234 | dropped: buffer 0, usb 0, oversize 0
    
  • The boot log has an App version: line. It is the only way to read the firmware version.

The commands and every log message are listed on Firmware Protocol.

Talking to a board by hand

Do not open a board's native USB port with a serial terminal. On this port DTR and RTS drive reset and boot mode, and a program that raises them can reboot the board or leave it in its ROM download mode.

Use open_serial() from the Python remote helper's board.py instead. It clears DTR and RTS in the order that does not reset the chip: on Windows before the port opens, elsewhere RTS and then DTR right after it opens (opening the port raises both lines there). It also takes the same port lock as the helpers. When a source holds the board, it is refused with ... is already in use by another capture .... The next section uses it.

TXTEST between two boards

TXTEST [n] makes a board send n IEEE 802.15.4 test frames, 20 ms apart. It works only in 802.15.4 mode, on the channel the board is on at that moment. A second board then proves the receive path without any Zigbee or Thread equipment nearby.

Warning: TXTEST is the only command that makes a board transmit. Use it only where you are allowed to transmit on 2.4 GHz, and on an 802.15.4 channel that none of your own Zigbee or Thread networks uses.

The example uses board A on /dev/ttyACM0 as the receiver (a Kismet source) and board B on /dev/ttyACM1 as the sender (not a source). Change the ports to yours.

  1. Start Kismet with board A on 802.15.4, locked to channel 20:

    ~/kismet-install/bin/kismet --no-ncurses --no-logging -c 'esp32c5:device=/dev/ttyACM0,mode=zigbee,channel=20,channel_hop=false,name=c5-rx'

    On the Pi this kept the source on channel 20, not hopping, with the C helper and with the Python remote helper. A source that is already running can be locked through Kismet's REST API instead. Take the source's UUID from /datasource/all_sources.json: it starts with E5C50002 for 802.15.4 and ends with the board's MAC. In the command, replace the UUID with yours and admin:PASSWORD with your Kismet login:

    curl -s -u admin:PASSWORD --data-urlencode 'json={"channel":"20"}' http://localhost:2501/datasource/by-uuid/E5C50002-0000-0000-0000-F0F5BD010203/set_channel.cmd
  2. From the repository root, send board B its commands:

    .venv/bin/python - /dev/ttyACM1 <<'EOF'
    import sys
    import time
    from esp32c5_kismet import board
    
    ser = board.open_serial(sys.argv[1])  # clears DTR and RTS without a reset; refused if a source holds the board
    ser.write(b"MODE 802154\n")           # reboots the board if it is on another radio
    time.sleep(1.5)                       # the reboot takes about 0.5 s; anything sent meanwhile is lost
    ser.write(b"CHANNELS 20\n")           # one channel is a lock; the default 802.15.4 list hops 11-26
    time.sleep(0.5)                       # let the board retune before it transmits
    ser.write(b"TXTEST 200\n")            # 200 frames, 20 ms apart: about 4 s
    time.sleep(5)
    board.close_serial(ser)
    EOF

    On the Pi this script ran as written, with the virtual environment's Python and with Debian's python3 and its python3-serial package, and 200 of 200 frames reached Kismet. The pause after CHANNELS matters: the firmware handles commands in a task with a higher priority than the one that changes channel, so without it TXTEST starts before the board has left the channel it was hopping on, and the first frame goes out there.

    The USB port usually stays open through the reboot, but on the test boards some radio switches made a board drop off USB and come back. Then the write after MODE fails. Run the script again: the board is now in 802.15.4 mode and does not reboot.

  3. Check what board A received:

    curl -s -u admin:PASSWORD http://localhost:2501/datasource/all_sources.json | python3 -m json.tool | grep -E '"kismet.datasource.(name|channel|hopping|num_packets)"'

    c5-rx should show channel "20", hopping 0 and num_packets 200, plus anything else that transmits on channel 20. Kismet's device list then has the 802.15.4 devices 00:01 (the sender) and FF:FF (the broadcast address).

  4. Board B now remembers 802.15.4. The next time a helper opens it for another radio, it reboots once. Put it back on Wi-Fi before you flash it with the merged image: a board flashed while it is on 802.15.4 can come up deaf to Wi-Fi (Flashing the Firmware).

Results on the Pi: TXTEST 200 into Kismet gave 200 of 200 packets, with device 00:01 at 200 packets on channel 20 and +9 dBm at close range. Board to board, all 12 ordered pairs of the four boards received 50 of 50 frames on channel 20. On channel 15, with no Zigbee or Thread equipment nearby, every board received nothing, as expected.

Other checks worth repeating

Check How Result on the Pi
Time to capture Kismet's log: Data source '...' launched successfully, then <name> capturing (<radio>) With the 0.8 s wait after MODE: 1.0 to 1.5 s when the board was already on the radio; about 1.5 s when it had to switch, 2.5 s when it dropped off USB and came back. Again on 2026-10-02: 0.99 s, 1.50 to 1.64 s, and 2.5 to 3 s
Recovery from a killed local helper Take the source's kismet.datasource.ipc_pid from /datasource/all_sources.json and kill -9 it. A helper's command line holds no port name, so pkill -f /dev/ttyACM0 finds nothing. Error at once, Attempting to re-open source after 5.8 s, capturing after 6.4 s (earlier code)
Several Wi-Fi boards Two or more Wi-Fi sources in one Kismet Splitting channels for interfaces using 'esp32c5' among 2 interfaces; the two boards were never on the same channel at once. On 2026-10-02 four boards: ... among 4 interfaces, and no two on one channel in 1,986 samples over 20 s
BLE records Records read from the boards directly 757 of 757 with a valid CRC and the "CRC checked" and "CRC valid" flags (earlier code)
Remote capture start Poll /datasource/all_sources.json from starting the helper to the first packet C helper over the websocket: 1.2 to 1.4 s (5 to 6 s before the framework fix in add-to-kismet.sh); over --tcp: 1.2 to 1.6 s; the Python remote helper: 1.4 to 1.8 s (earlier code). On 2026-10-02 the Python remote helper on Windows, feeding the Pi across the local network: 1.3 to 1.8 s

Still to check on hardware

These are fixed or changed in the code but have not run on a board since:

  • The Python remote helper's could not open <port>: <error> text, for an open error that does not name the port. On the Pi the open errors named the port themselves, so this form has run only in the unit tests.
  • Python's --list on Linux leaving out a board another capture holds (the C helper's --list does, on the Pi).
  • Finding a board again by its MAC when two boards swap tty names, and a board unplugged for more than 20 s. On 2026-10-02 one board dropped off USB and came back many times, and both helpers reopened its port each time, but it always came back under the same name.
  • Why one test board drops off USB at a Kismet start where several boards switch radio at once, and sometimes then stops answering (what was seen): the firmware, or the power on its hub port. Moving it to another port, or swapping it with another board, would tell them apart.
  • That a board a helper on the host holds is refused inside a container. The other direction passed on the Pi: the host could not open a board the container was capturing from.

Results that look like failures but are not

Scripts should check the text of these, not the exit status.

What you see Why
kismet --version exits with status 1 Kismet does that by design
kismet_cap_esp32c5 --help exits with status 255 The capture framework does that
kismet_cap_esp32c5 --list prints to stderr and exits with status 2 The capture framework does that; use kismet_cap_esp32c5 --list 2>&1
python -m esp32c5_kismet.remote --list exits with status 1 No board was found, or on Linux every board is in use by another capture
ERROR: Tried to re-register duplicate alert FLIPPERZERO at every Kismet start An upstream quirk of Kismet cfe427074; harmless. Do not treat ERROR lines in Kismet's log as failures by themselves.
A source shows an old error text while it runs Kismet keeps the last error_reason after a reconnect or re-open
A local source's error_reason says only IPC connection closed after the C helper gave up Kismet ignores the error frame a local helper sends; the helper's reason is in Kismet's log, as an ERROR: line just before. Kismet also sometimes starts two re-opens of a failed local source at once, and the second ends with IPC connection closed too.
packets.frequency in the kismetdb log is 0 for every 802.15.4 and BTLE packet A Kismet limit: its 802.15.4 and BTLE code never sets a packet's frequency, whatever the helper sends. The device records have the right frequency.
A remote source closed with close_source.cmd runs again 5 s later The helper reconnects, and Kismet runs the source again. To stop a remote source, stop its helper.
A remote source keeps an option its new definition no longer has Kismet keeps, until it restarts, every option of a UUID it knows that the new definition leaves out: write the option out (channel_hop=true, say) or restart Kismet
⚠️ **GitHub.com Fallback** ⚠️