Guide Windows Boards to a Pi - oshri-almog/esp32c5-kismet-wifi-interface GitHub Wiki
This guide connects ESP32-C5 boards plugged into a Windows laptop to a Kismet server on a Raspberry Pi on the same network, through the Python remote helper. It goes from setting up the server to a clean stop, and is for anyone who wants the boards next to them and Kismet somewhere else. The same steps work with any Linux server, and with the project's Docker image as the server.
Note: Only capture on networks and devices you own or are authorised to test.
flowchart LR
subgraph laptop["Windows laptop"]
B1["Board on COM14<br/>Wi-Fi"] -- USB --> H["Python remote helper<br/>python -m esp32c5_kismet.remote"]
B2["Board on COM15<br/>BLE"] -- USB --> H
end
subgraph pi["Raspberry Pi, 192.168.1.50"]
K["Kismet server<br/>web port 2501"]
end
H -- "remote capture: websocket on TCP 2501<br/>one connection per source, API key" --> K
K --> UI["Web UI http://192.168.1.50:2501<br/>logs on the Pi"]
Kismet does not run on Windows. The Python remote helper does: it opens each board on its COM port, sets the radio and the channel, and passes every frame to Kismet over Kismet's remote capture, a websocket on the web port 2501. Kismet on the Pi does the rest, and the logs are written there. Remote Capture explains the mechanism.
What has been tested: this guide was run end to end on 2026-10-02, from a Windows 11 Pro PC on Wi-Fi to Kismet on a Raspberry Pi 4 (Debian 13) across the LAN, with one board (firmware image 01a50bd6). Wi-Fi, 802.15.4 and Bluetooth LE sources each ran, started from PowerShell, cmd and Git Bash, with an API key, with a login, and with the environment variables. Two boards from one helper, as in part 4, and Kismet splitting the channels of two Wi-Fi boards from the laptop (part 5) were not run that way.
The examples use the Pi at 192.168.1.50, boards on COM14 and COM15, and the API key 3F9A6C1E07B24D58A1C9E2F4608B7D35. Change all three to yours.
The Pi needs a Kismet that knows the esp32c5 source type. A Kismet from a distribution package does not; it would log Kismet could not find a datasource driver for incoming remote source 'esp32c5' ... when the helper connects. Pick one:
- Native build: follow Install on Raspberry Pi, or steps 3 and 4 of Guide: First Capture. About 78 minutes of unattended compiling on a Pi 4.
-
Docker: follow Install with Docker. The image already has the source.
docker compose updownloads the published arm64 image, about 48 MB, so nothing is compiled on the Pi.
The Pi needs no boards of its own for this guide.
-
Set Kismet's login before the first start. Kismet's web UI listens on port 2501 on all network interfaces, and until a login exists, the first person to open it chooses the admin login. For the native build, as the user that will run Kismet:
mkdir -p ~/.kismet printf 'httpd_username=%s\nhttpd_password=%s\n' admin 'choose-a-long-password' > ~/.kismet/kismet_httpd.conf chmod 600 ~/.kismet/kismet_httpd.conf
For Docker, set
KISMET_USERandKISMET_PASSWORDin the.envfile (Install with Docker). -
Start Kismet without any sources. Native build:
mkdir -p ~/kismet-logs cd ~/kismet-logs ~/kismet-install/bin/kismet --no-ncurses
Kismet logs
No data sources defined; Kismet will not capture anything until a source is added.That is expected: the sources come from the laptop. It also logsHTTP server listening on 0.0.0.0:2501, which means it accepts connections on every interface.Docker, from the project folder:
sudo docker compose up -d
On Debian,
docker composecomes from thedocker-composepackage, notdocker.io; Install with Docker lists the packages, and gives the equivalentdocker runcommand. With no board on the Pi, the container waits up to 30 s for one, then starts Kismet without sources. A Kismet container that only receives remote sources needs no device rules;compose.yamlsets them anyway, for boards plugged into the Pi, and they do no harm. No capability is added: the container needs noNET_ADMIN.To check that this Kismet knows the
esp32c5source type, which a Kismet from a distribution package does not, run the check in Step 0 on Remote Capture on the Pi, withlocalhostas the address. -
Note the Pi's address:
hostname -I
-
Open the port if the Pi runs a firewall. The laptop connects to TCP 2501 on the Pi. If you run a firewall on the Pi, allow TCP 2501 from the laptop. Port 2501 also carries the web UI and the REST API, which is why the login in step 1 comes first.
With the Docker server, do not count on the firewall to close the port: Docker's own rules let connections through to a published port, and a host firewall such as ufw may never see them. There, the login from step 1 is what protects port 2501.
For the Pi to start Kismet at boot, see Guide: Running as a Service.
The helper needs a Kismet login or an API key. Use an API key with the datasource role:
- it can feed sources and nothing else: with it, a request such as the source list gets HTTP 401;
- the admin password never goes to the laptop;
- Kismet keeps it across restarts, and in the Kismet version this project builds keys do not expire.
A login works too, whatever its password holds: the helper sends it in an Authorization header. Only a login whose user name contains :, with an & in the user name or the password, cannot log in; the helper warns about it at start (Install on Windows).
Create the key on the Pi, in the web UI or with curl.
In the web UI: open http://192.168.1.50:2501, log in, then Settings → API Keys → Create API Key. Give it a name, pick the role datasource, and copy the key.
With curl, in a terminal on the Pi:
curl -u admin:choose-a-long-password --data-urlencode 'json={"name": "windows-laptop", "role": "datasource", "duration": 0}' http://localhost:2501/auth/apikey/generate.cmdIt prints the key, 32 hex characters. The name must be unique on that Kismet. This call was tested against Kismet on the Pi and in Docker Desktop.
To withdraw the key later, delete it under Settings → API Keys, or call /auth/apikey/revoke.cmd with json={"name": "windows-laptop"}.
-
Install Python 3.10 or newer from python.org, then check it in a new PowerShell window:
python --version -
Get the project and its three packages (pyserial, msgpack and websocket-client). With Git for Windows installed:
git clone https://github.com/oshri-almog/esp32c5-kismet-wifi-interface cd esp32c5-kismet-wifi-interface python -m pip install -r requirements.txt
Without Git, download the repository as a ZIP from its GitHub page, unpack it,
cdinto the unpacked folder, and run thepipline there.There is nothing to install beyond that: run the helper as
python -m esp32c5_kismet.remotefrom this folder. -
Plug in the boards and list them:
python -m esp32c5_kismet.remote --list
COM14 F0:F5:BD:01:02:03 --source esp32c5-COM14 --source esp32c5zigbee-COM14 --source esp32c5btle-COM14 COM15 F0:F5:BD:01:02:04 --source esp32c5-COM15 --source esp32c5zigbee-COM15 --source esp32c5btle-COM15 One source per board: it captures with one radio at a time. Every ESP32 on its native USB port has this USB ID, so a board listed here need not be an ESP32-C5 sniffer.Each board is listed with its MAC and its three source names, one per radio. Pick one per board.
--listnever opens a port, so it is safe to run at any time.The boards need this project's firmware. If they do not have it yet, flash them from this laptop with the web flasher in Chrome or Edge (Flashing the Firmware).
Install on Windows covers all of this in more detail, including a board that Windows has wedged.
Keep the key off the command line, where it shows in the process list to administrators and to other programs running as you: put it in the environment variable KISMET_CAP_APIKEY, which the helper reads when no --apikey, --user or --password is given. In PowerShell:
$env:KISMET_CAP_APIKEY = "3F9A6C1E07B24D58A1C9E2F4608B7D35"
python -m esp32c5_kismet.remote --connect 192.168.1.50:2501 --source esp32c5-COM14:name=win-wifi --source esp32c5btle-COM15:name=win-btleIn cmd:
set KISMET_CAP_APIKEY=3F9A6C1E07B24D58A1C9E2F4608B7D35
python -m esp32c5_kismet.remote --connect 192.168.1.50:2501 --source esp32c5-COM14:name=win-wifi --source esp32c5btle-COM15:name=win-btle
-
--connecttakes the Pi's address and Kismet's web port, 2501. - Each
--sourceis one board on one radio, with its own connection.name=is what Kismet shows. - The variable lasts as long as the window. For a login instead of a key, set
KISMET_CAP_USERandKISMET_CAP_PASSWORD; the key wins if all three are set.
The helper logs one line per event:
14:02:11 INFO: esp32c5-COM14:name=win-wifi: connected, offering it to Kismet as E5C50001-0000-0000-0000-F0F5BD010203
14:02:11 INFO: esp32c5-COM14:name=win-wifi: opening COM14 for wifi
14:02:11 INFO: win-wifi: COM14 opened
14:02:12 INFO: win-wifi capturing (wifi)
The BLE source logs the same four lines, ending win-btle capturing (btle), mixed in with these.
If a board last used another radio, it reboots into the one asked for first. On Windows that took 1.2 to 1.5 s from opening the board to capturing, against about 0.8 s without a switch.
On the Pi, Kismet logs each new source:
INFO: New remote source win-wifi (E5C50001-0000-0000-0000-F0F5BD010203) connected
In the web UI, Data Sources lists win-wifi and win-btle as remote sources, running, with climbing packet counts, and devices appear in the device list. The project's tests checked this through the REST API below, not in a browser. The ID Kismet shows is built from the board's MAC and the radio (E5C50001 Wi-Fi, E5C50002 802.15.4, E5C50003 BLE), so the same board on the same radio is always the same source, whatever COM port it is on.
Over the REST API, in a terminal on the Pi, with the admin login (the datasource key is not allowed to read the source list). The command uses bash quoting and python3, so it does not work in PowerShell on the laptop:
curl -s -u admin:choose-a-long-password http://localhost:2501/datasource/all_sources.json \
| python3 -c 'import json,sys; [print(s["kismet.datasource.name"], "remote" if s["kismet.datasource.remote"] else "local", "running" if s["kismet.datasource.running"] else "stopped", s["kismet.datasource.num_packets"]) for s in json.load(sys.stdin)]'It prints one line per source, such as win-wifi remote running 186.
Channel control works as for local sources. Kismet sends the helper its channel list and hop rate, and the helper retunes the board. The web UI's channel buttons and the REST calls on Channel Control apply; through Docker Desktop, locking a remote Wi-Fi source on channel 48 put all its new packets on 5240 MHz. Two Wi-Fi boards on the laptop are split by Kismet like two on the Pi: Kismet logs Splitting channels for interfaces using 'esp32c5' among 2 interfaces, sends each source its starting point (21 and 0 of the 42 channels), and the helper hops from there.
What this guide's run measured, one source at a time: the Wi-Fi source found 41 Wi-Fi devices and delivered 18,488 packets in 90 s, hopping all 42 channels at 5 per second; the BLE source found 6 BLE devices in 60 s (791 packets); the 802.15.4 source ran and hopped, but with no Zigbee or Thread devices nearby it saw no packets. The first packets reached Kismet 1.3 to 2.0 s after the helper started. What you see depends on what is around you.
Run the helper in an ordinary console window (Windows Terminal, PowerShell or cmd) and leave it open. It keeps every source trying until you stop it:
| What happens | What the helper does |
|---|---|
| Kismet is not up yet, or restarts | Logs the failed connection ([WinError 10061] No connection could be made because the target machine actively refused it when nothing listens) and tries again 5 s later. Windows takes about 2 s to report a refused connection, so the line comes about every 7 s. When Kismet on the Pi restarted, the helper was capturing again 4.6 to 4.9 s after Kismet's port answered |
| Kismet comes back | Offers each source again under the same ID. A restarted Kismet logs New remote source win-wifi (...) connected again. A Kismet that kept running while the network or the helper was away logs Remote source win-wifi (...) reconnected and reuses the source it has |
| A board reboots to change radio | Reads through the reboot and asks again |
| A board is unplugged | Retries the port about once a second; after 15 s without capture it gives the source up, tells Kismet, and waits for the board: COM14 is not there; is the board plugged in? (waiting for it). When the board is back it offers the same source |
| The laptop's network drops | Keeps trying, 5 s after each failed attempt, until the Pi is reachable again |
While a board is missing or the Pi is unreachable, the helper logs the same ERROR line at every try, every 5 to 7 s. That is noisy but harmless.
To start the helper when you log on to the laptop, see Guide: Running as a Service. Sleep and wake of the laptop have not been tested.
Note: The websocket is plain
ws://, not encrypted: Kismet has no TLS of its own. The API key travels in a request header (a cookie) and the packets travel in the clear across your network. Use this on a network you trust. Across one you do not, put a TLS reverse proxy in front of Kismet and give the helper--ssl, with--ssl-certificatefor your own CA (Remote Capture).
-
Stop the helper first. Press Ctrl+C, or Ctrl+Break, in its window. It logs
INFO: stopping, closes its connections, releases the COM ports and exits with status 0; pressing it again while it stops changes nothing. On the test PC this took 0.06 to 0.55 s with the helper as of commitf8e6792, and about 0.2 s at most with the current one, which was stopped while it fed Kismet in WSL2. -
Kismet on the Pi then shows the sources as stopped, with the error
websocket connection closed. That is expected, and the C helper gives the same. The sources stay listed; when the helper connects again, Kismet reuses them and keeps what it knew of them: the name, and any option the new definition leaves out. A source once locked withchannel_hop=falsestays locked when it is started again without it; writechannel_hop=true, or restart Kismet. Kismet never reopens a remote source itself, and closing one from Kismet's side (itsclose_source.cmdcall) lasts only until the helper connects again, about 5 s later: to stop a source, stop the helper. -
Stop Kismet on the Pi, if you want to: Ctrl+C in its terminal,
sudo docker compose stopfor Docker, orsudo systemctl stop kismetfor a service.
If you stop Kismet first instead, the helper keeps retrying, about every 7 s, until you stop it too.
If the helper will not stop, find its process ID in Task Manager (Details tab) and force it: taskkill /F /PID 12345 with that number (without /F, Windows refuses). A forced stop still releases the COM ports at once, and Kismet shows the same websocket connection closed. A helper started as a background job from Git Bash (python ... &) stops with Ctrl+C in that window or with kill -INT $!: Windows gives such jobs an "ignore Ctrl+C" flag, and the helper clears it when it starts.
| What you see | Where | What to do |
|---|---|---|
[WinError 10061] No connection could be made because the target machine actively refused it |
Helper | Kismet is not running on the Pi, or not on that address and port. Check --connect and that Kismet logged HTTP server listening on 0.0.0.0:2501
|
| Connection attempts time out | Helper | The laptop cannot reach the Pi: a different network, or a firewall on the Pi blocking TCP 2501 |
Kismet refused the websocket: 401 Unauthorized (check the login -- --user/--password or KISMET_CAP_USER/KISMET_CAP_PASSWORD -- or the API key -- --apikey or KISMET_CAP_APIKEY; the key needs the datasource role) |
Helper | Wrong key, or a key without the datasource role. Create one as in part 2 |
a user and password, or an API key, are required for the websocket protocol ... |
Helper |
KISMET_CAP_APIKEY is not set in this window. Set it again, or pass --apikey
|
port 3501 is Kismet's legacy TCP port; did you mean --tcp, or port 2501? |
Helper | Use port 2501 |
COM14 is already in use by another capture (an esp32c5 source or another program holds it); a board captures with one radio at a time |
Helper | Another program has the COM port: a second helper, esptool, a serial terminal. Close it |
win-wifi: no capture from the board on COM14 for 15 seconds (last: ...); is it flashed with the esp32c5 sniffer firmware, and is nothing else holding the port? |
Helper, and in Kismet as the source's error | The board does not answer: not flashed with this firmware, wedged, or hung in a radio switch. Replug it; see Flashing the Firmware |
Kismet could not find a datasource driver for incoming remote source 'esp32c5' ... |
Kismet | The Pi's Kismet was built without the ESP32-C5 source. Use one built as in part 1 |
More on Install on Windows and Troubleshooting.
- Install on Windows: the Python remote helper on Windows in full
- Remote Capture: the protocol, logins and API keys, and the legacy TCP port
- Command-Line Reference: every option of the Python remote helper
- Choosing a Setup: other ways to combine Windows and Kismet