Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Build and Release

on:
release:
types: [created]

jobs:
build:
permissions:
packages: write
contents: write
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Cache PlatformIO
uses: actions/cache@v4
with:
path: |
~/.platformio
.pio
key: ${{ runner.os }}-pio-${{ hashFiles('**/platformio.ini') }}
restore-keys: |
${{ runner.os }}-pio-

- name: Install PlatformIO
run: |
python -m pip install --upgrade pip
pip install platformio

- name: Install library dependencies
run: pio pkg install

- name: Build firmware
run: pio run

- name: Rename firmware binary
run: |
cp .pio/build/m5stack-cardputer/firmware.bin flockyou-${{ github.event.release.tag_name }}-cardputer.bin
cp .pio/build/xiao_esp32s3/firmware.bin flockyou-${{ github.event.release.tag_name }}-xiao_esp32s3.bin

- name: Upload release asset
uses: softprops/action-gh-release@v2
with:
files: |
./flockyou-${{ github.event.release.tag_name }}-cardputer.bin
./flockyou-${{ github.event.release.tag_name }}-xiao_esp32s3.bin
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,21 @@ No buzzer on this env.

Boot sound (XIAO only): first 6 notes of Super Mario Bros. World 1-2 (underground).

### M5Stack Cardputer (`BOARD_M5STACK_CARDPUTER`, env: `m5stack-cardputer`)

| Peripheral | Function |
|-----|----------|
| ST7789 240×135 IPS display | Driven via M5Unified/M5GFX (`M5Cardputer.Display`) |
| QWERTY keyboard | Dismisses the detection screen — see below |
| I2S speaker | Detection tones and boot jingle via `M5.Speaker`; no piezo GPIO on this board |
| USB CDC | Serial JSON for Flask dashboard |

No addressable LED on this board — detections are visual (screen) and audible (speaker) only.

There's no dedicated `m5stack-cardputer` board definition in this platform version, so the env borrows `m5stack-stamps3` (same ESP32-S3 chip, same 8 MB flash layout as `partitions.csv`) and lets the `M5Cardputer`/`M5Unified` libraries own the actual display/keyboard/speaker bring-up at runtime.

**Display:** idle screen shows `SCANNING`, current channel, and unique hit count, same as the T-Dongle. On a detection it shows `DETECT`, the method (e.g. `wifi_oui_addr2`), MAC, RSSI, and channel — but unlike the T-Dongle's 5-second auto-return, **the alert screen stays up until you press any key**, since this board has a keyboard to dismiss it with. A new detection arriving while one is still on-screen redraws with the latest hit's info rather than queuing behind the dismiss.

### Detection audio

Each tier has its own tone (see the table above), so the detection method is clear without looking at the screen. Individual tiers can be muted from the dashboard's **Audio** panel; muting affects the buzzer only, and detections still log and export. The mask lives in NVS under `flockyou/beepmask` and survives a power cycle.
Expand Down Expand Up @@ -309,6 +324,7 @@ pio device monitor
| `AUTOSAVE_INTERVAL_MS` | 60000 | SPIFFS save cadence |
| `LED_PIN` | 21 | Onboard user LED |
| `BUZZER_PIN` | 3 | Piezo |
| `DEBUG_ALLOW_RANDOMIZED_MAC` | 0 | See `matchOuiRaw()`

---

Expand Down
116 changes: 116 additions & 0 deletions display_cardputer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#include "display_cardputer.h"

#ifdef USE_CARDPUTER_DISPLAY

#include <M5Cardputer.h>

// M5Stack Cardputer: 1.14" ST7789 240x135 IPS, driven by M5Unified/M5GFX.
// M5Cardputer.begin() brings up the display, keyboard and I2S speaker
// together — main.cpp's fyToneOn()/fyToneOff() rely on that having run
// before the first beep.

static uint8_t idleCh = 1;
static int idleDetCount = 0;
static bool inAlert = false;
static uint8_t lastDrawnCh = 0xFF;
static int lastDrawnHits = -1;

// Text size 1 (6px/char) keeps even the longest method string —
// "wifi_wildcard_probe_ie_sig", 27 chars — under the 240px screen width.
static void drawMethodLine(const char *method, int y)
{
auto &d = M5Cardputer.Display;
d.setTextColor(TFT_WHITE, TFT_BLACK);
d.setTextSize(2);
d.setCursor(4, y);
d.print(method);
}

void cardputerDisplayInit()
{
auto cfg = M5.config();
M5Cardputer.begin(cfg);
M5Cardputer.Display.setRotation(1);
M5Cardputer.Display.fillScreen(TFT_BLACK);
cardputerDisplayShowIdle(1, 0);
}

void cardputerDisplayShowIdle(uint8_t ch, int detCount)
{
idleCh = ch;
idleDetCount = detCount;
inAlert = false;

auto &d = M5Cardputer.Display;
d.fillScreen(TFT_BLACK);
d.setTextColor(TFT_GREEN, TFT_BLACK);
d.setTextSize(3);
d.setCursor(6, 8);
d.print("SCANNING");

d.setTextColor(TFT_WHITE, TFT_BLACK);
d.setTextSize(2);
d.setCursor(4, 72);
d.printf("Ch: %u", (unsigned)ch);
d.setCursor(4, 100);
d.printf("Hits: %d", detCount);

lastDrawnCh = ch;
lastDrawnHits = detCount;
}

void cardputerDisplayShowAlert(const char *method, const char *mac, int8_t rssi,
uint8_t ch, unsigned long alertMs)
{
(void)alertMs; // ignored — Cardputer latches on the alert until a key is pressed
idleCh = ch;
inAlert = true;

auto &d = M5Cardputer.Display;
d.fillScreen(TFT_BLACK);
d.setTextColor(TFT_RED, TFT_BLACK);
d.setTextSize(3);
d.setCursor(6, 4);
d.print("DETECT");

drawMethodLine(method ? method : "?", 40);

d.setTextColor(TFT_WHITE, TFT_BLACK);
d.setTextSize(2);
d.setCursor(4, 60);
d.print(mac ? mac : "");
d.setCursor(4, 80);
d.printf("RSSI %d CH %u", (int)rssi, (unsigned)ch);
}

bool cardputerDisplayInAlert(unsigned long now)
{
(void)now;
return inAlert;
}

// Cardputer has a keyboard, so a detection screen doesn't need to time out
// on its own — it stays up (even through further redraws from new hits)
// until you dismiss it with a key press, then returns to idle.
void cardputerDisplayTick(unsigned long now, uint8_t ch, int detCount)
{
(void)now;
M5Cardputer.update(); // polls hardware, keyboard included

idleCh = ch;
idleDetCount = detCount;
if (inAlert)
{
if (M5Cardputer.Keyboard.isChange() && M5Cardputer.Keyboard.isPressed())
{
cardputerDisplayShowIdle(ch, detCount);
}
return;
}
if (ch != lastDrawnCh || detCount != lastDrawnHits)
{
cardputerDisplayShowIdle(ch, detCount);
}
}

#endif
27 changes: 27 additions & 0 deletions display_cardputer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#pragma once

#include <stdint.h>

#ifdef BOARD_M5STACK_CARDPUTER
#define USE_CARDPUTER_DISPLAY
#endif

#ifdef USE_CARDPUTER_DISPLAY

void cardputerDisplayInit();
void cardputerDisplayShowIdle(uint8_t ch, int detCount);
void cardputerDisplayShowAlert(const char* method, const char* mac, int8_t rssi,
uint8_t ch, unsigned long alertMs);
void cardputerDisplayTick(unsigned long now, uint8_t ch, int detCount);
bool cardputerDisplayInAlert(unsigned long now);

#else

static inline void cardputerDisplayInit() {}
static inline void cardputerDisplayShowIdle(uint8_t, int) {}
static inline void cardputerDisplayShowAlert(const char*, const char*, int8_t, uint8_t,
unsigned long) {}
static inline void cardputerDisplayTick(unsigned long, uint8_t, int) {}
static inline bool cardputerDisplayInAlert(unsigned long) { return false; }

#endif
Loading