From c963fb26f0044752cfa861db2d595922d87c44af Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 02:39:12 +0000 Subject: [PATCH 1/2] cyberpunkunicorn-os: one owner for the hat bus, reader poll stats, 0.1.3 - The UI task and the remote command task could both drive pins 22/27: a menu's rescan (probeHat) ended the I2C bus under a tag being read for the website, and readerOpen saw 'no reader'. A recursive hat-bus mutex now covers readerOpen..readerClose and every probe; a probe from the other task returns the last result instead of touching the bus, and readerOpen waits up to 600 ms for a running probe. - A tap that times out now says how the reader's selects ended (no ack, no answer, no card, seen) in the reply and on serial, so the website's device log tells a dead reader from an empty field. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_015Aw32AU4vVqCSEoNA5aP3U --- cyberpunkunicorn-os/apps/frequency/card.cpp | 30 +++++++++++++++++-- cyberpunkunicorn-os/apps/frequency/card.h | 9 ++++++ cyberpunkunicorn-os/apps/hat/hat_probe.cpp | 13 ++++++++ cyberpunkunicorn-os/apps/hat/hat_probe.h | 8 +++++ .../apps/remote/app_handlers.cpp | 8 ++++- cyberpunkunicorn-os/build.sh | 2 +- 6 files changed, 65 insertions(+), 5 deletions(-) diff --git a/cyberpunkunicorn-os/apps/frequency/card.cpp b/cyberpunkunicorn-os/apps/frequency/card.cpp index 7027d6b..bb58e75 100644 --- a/cyberpunkunicorn-os/apps/frequency/card.cpp +++ b/cyberpunkunicorn-os/apps/frequency/card.cpp @@ -134,10 +134,24 @@ String parseNdefUri(const std::vector &mem) { // A reliable "is a card in the field" check. Bruce's read() reads the // InListPassiveTarget answer without waiting for the PN532 to be ready, so on // its own it misses cards now and then; waiting for RDY here first fixes that. +// Where each select stopped since the last resetPollStats() (pollStats()). +PollStats stats; + bool selectCard(uint16_t waitMs) { - if (!rfid->nfc.startPassiveTargetIDDetection(PN532_MIFARE_ISO14443A)) return false; - if (!rfid->nfc.waitready(waitMs)) return false; - if (!rfid->nfc.readDetectedPassiveTargetID()) return false; + stats.polls++; + if (!rfid->nfc.startPassiveTargetIDDetection(PN532_MIFARE_ISO14443A)) { + stats.noAck++; + return false; + } + if (!rfid->nfc.waitready(waitMs)) { + stats.noAnswer++; + return false; + } + if (!rfid->nfc.readDetectedPassiveTargetID()) { + stats.noCard++; + return false; + } + stats.selected++; return true; } @@ -173,6 +187,10 @@ void fill(CardInfo &out) { } // namespace +void resetPollStats() { stats = PollStats(); } + +PollStats pollStats() { return stats; } + const char *cardStatusText(CardStatus s) { switch (s) { case CARD_OK: return "OK"; @@ -190,6 +208,11 @@ const char *cardStatusText(CardStatus s) { CardStatus readerOpen() { if (xSemaphoreTake(readerMutex(), 0) != pdTRUE) return CARD_BUSY; + // Wait out a probe running on the other task (a menu's rescan, ~0.3 s). + if (!hatBusTake(600)) { + xSemaphoreGive(readerMutex()); + return CARD_BUSY; + } held = true; HatProbeResult hat = probeHat(); if (!hat.pn532) { @@ -211,6 +234,7 @@ CardStatus readerOpen() { void readerClose() { if (!held) return; held = false; + hatBusGive(); xSemaphoreGive(readerMutex()); } diff --git a/cyberpunkunicorn-os/apps/frequency/card.h b/cyberpunkunicorn-os/apps/frequency/card.h index 7d0e2f0..0e308fb 100644 --- a/cyberpunkunicorn-os/apps/frequency/card.h +++ b/cyberpunkunicorn-os/apps/frequency/card.h @@ -47,6 +47,15 @@ CardStatus waitCard( const std::function &idle = nullptr ); +// How the reader's selects ended since resetPollStats(), so a timeout can say +// whether the PN532 never answered (noAck / noAnswer: the reader or its bus) +// or answered "no card" every time (noCard: nothing in its field). +struct PollStats { + uint32_t polls = 0, noAck = 0, noAnswer = 0, noCard = 0, selected = 0; +}; +void resetPollStats(); +PollStats pollStats(); + // Write `uri` as the tag's only NDEF record, then read it back. `out` is the // verified tag. CardStatus writeCard(const String &uri, CardInfo &out); diff --git a/cyberpunkunicorn-os/apps/hat/hat_probe.cpp b/cyberpunkunicorn-os/apps/hat/hat_probe.cpp index 9bcffec..e4ce62d 100644 --- a/cyberpunkunicorn-os/apps/hat/hat_probe.cpp +++ b/cyberpunkunicorn-os/apps/hat/hat_probe.cpp @@ -232,6 +232,17 @@ String HatProbeResult::advice() const { return "Ready."; } +namespace { +SemaphoreHandle_t hatBus() { + static SemaphoreHandle_t m = xSemaphoreCreateRecursiveMutex(); + return m; +} +} // namespace + +bool hatBusTake(uint32_t waitMs) { return xSemaphoreTakeRecursive(hatBus(), pdMS_TO_TICKS(waitMs)) == pdTRUE; } + +void hatBusGive() { xSemaphoreGiveRecursive(hatBus()); } + static HatProbeResult lastResult; static uint32_t lastAt = 0; static bool haveLast = false; @@ -239,6 +250,7 @@ static bool haveLast = false; HatProbeResult probeHat() { return probeHat(true); } HatProbeResult probeHat(bool allowSpi) { + if (!hatBusTake(0)) return lastResult; // the reader is in use elsewhere: report what we last saw HatProbeResult r; probeI2C(r); if (!r.pn532 && !r.i2cStuck) { @@ -257,6 +269,7 @@ HatProbeResult probeHat(bool allowSpi) { lastResult = r; lastAt = millis(); haveLast = true; + hatBusGive(); hw::ingestProbe(r); // keeps the Library / main menu in step with what is plugged in return r; } diff --git a/cyberpunkunicorn-os/apps/hat/hat_probe.h b/cyberpunkunicorn-os/apps/hat/hat_probe.h index e0f0353..41cc1f7 100644 --- a/cyberpunkunicorn-os/apps/hat/hat_probe.h +++ b/cyberpunkunicorn-os/apps/hat/hat_probe.h @@ -38,6 +38,14 @@ struct HatProbeResult { String advice() const; // what to do next }; +// Pins 22/27 have one owner at a time: the card reader (frequency/card.cpp) from +// readerOpen() to readerClose(), or a probe for its scan. Recursive, so the owner +// may probe. The UI and the remote command task run on different cores; without +// this a menu's rescan tore the I2C bus down under a tag being read. +bool hatBusTake(uint32_t waitMs); +void hatBusGive(); + +// While another task owns the bus, returns the last result without touching it. HatProbeResult probeHat(); // allowSpi = false: I2C scan only, never drives 22/27 as SPI or reads the RX line // (the boot probe's early tries, while a PN532 may still be waking). diff --git a/cyberpunkunicorn-os/apps/remote/app_handlers.cpp b/cyberpunkunicorn-os/apps/remote/app_handlers.cpp index 6fad72c..9a7b51a 100644 --- a/cyberpunkunicorn-os/apps/remote/app_handlers.cpp +++ b/cyberpunkunicorn-os/apps/remote/app_handlers.cpp @@ -93,10 +93,16 @@ bool tap(remote::Request &r, freq::CardInfo &card) { uint32_t timeout = r.args["timeout_ms"] | TAP_TIMEOUT_MS; if (timeout > 30000) timeout = 30000; remote::emit("frequency.waiting_for_tag", JsonObjectConst()); + freq::resetPollStats(); s = freq::waitCard(card, timeout, [] { return false; }); if (s != freq::CARD_OK) { freq::readerClose(); - r.fail(s == freq::CARD_NONE ? "timeout" : "card_error", freq::cardStatusText(s)); + // What the reader did meanwhile, for the website's device log. + freq::PollStats p = freq::pollStats(); + String why = String(freq::cardStatusText(s)) + " (" + p.polls + " polls: " + p.noCard + " no card, " + + p.noAck + " no ack, " + p.noAnswer + " no answer, " + p.selected + " seen)"; + Serial.println("[freq] " + why); + r.fail(s == freq::CARD_NONE ? "timeout" : "card_error", why); return false; } return true; diff --git a/cyberpunkunicorn-os/build.sh b/cyberpunkunicorn-os/build.sh index 9608925..b7e06ac 100755 --- a/cyberpunkunicorn-os/build.sh +++ b/cyberpunkunicorn-os/build.sh @@ -9,7 +9,7 @@ PIO_ENV=CYD-2432S028 PLATFORMIO_VERSION=6.2.0 ESPTOOL_VERSION=5.4.0 OS_NAME="CyberUnicorn 5000" -OS_VERSION=0.1.2 +OS_VERSION=0.1.3 source ./boot.conf for var in BOOT_TITLE BOOT_TAGLINE; do From 973ce9b61d86bda07ed6a58a972e891ac5319c00 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 02:44:11 +0000 Subject: [PATCH 2/2] cyberpunkunicorn-os: screen stream stays on the link that started it (patch 0020) With the Bluetooth API on (Daniel's board has it on at boot), serialDevice is the BLE serial except while a USB command runs, so the screen stream went to the phone and reached the website only in torn pieces during commands: the garbled mirror and the binary in the device log. The stream now keeps the device that sent 'display start'. cli.run refuses 'display' so that device is never a capture that dies with the call. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_015Aw32AU4vVqCSEoNA5aP3U --- cyberpunkunicorn-os/CLAUDE.md | 2 + cyberpunkunicorn-os/apps/remote/remote.cpp | 3 ++ .../0020-screen-stream-to-its-link.patch | 46 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 cyberpunkunicorn-os/patches/0020-screen-stream-to-its-link.patch diff --git a/cyberpunkunicorn-os/CLAUDE.md b/cyberpunkunicorn-os/CLAUDE.md index 500f49c..5fdc121 100644 --- a/cyberpunkunicorn-os/CLAUDE.md +++ b/cyberpunkunicorn-os/CLAUDE.md @@ -77,6 +77,8 @@ User-tunable values go in `boot.conf` → `-D` macros via `PLATFORMIO_BUILD_FLAG - `0019` PN532 `readSelected()` / `writeNdefSelected()`: read or write the card the caller already selected (Frequency's card.cpp waits for RDY; Bruce's own read() re-selects without waiting and could miss a tag held still) +- `0020` the screen stream writes to the link that sent `display start`, not to + `serialDevice` (the BLE serial whenever Bluetooth is on), so the cable gets every frame (No 0002: the nm-rf-hat cat scene was dropped for the unicorn.) Macros in 0003–0004 keep their `NM_` prefix from `nm-rf-hat/`; renaming them is churn. diff --git a/cyberpunkunicorn-os/apps/remote/remote.cpp b/cyberpunkunicorn-os/apps/remote/remote.cpp index c380fbd..dbb1494 100644 --- a/cyberpunkunicorn-os/apps/remote/remote.cpp +++ b/cyberpunkunicorn-os/apps/remote/remote.cpp @@ -253,6 +253,9 @@ void cmdCliRun(Request &r) { line.trim(); if (line.isEmpty()) return r.fail("bad_args", "line is required"); if (line.startsWith("rpc")) return r.fail("bad_args", "cli.run can't run rpc"); + // The screen stream keeps the device that started it (patch 0020): a capture + // device lives only for this call. + if (line.startsWith("display")) return r.fail("bad_args", "send `display start` as a plain line, not cli.run"); CaptureDevice cap; SerialDevice *prev = serialDevice; diff --git a/cyberpunkunicorn-os/patches/0020-screen-stream-to-its-link.patch b/cyberpunkunicorn-os/patches/0020-screen-stream-to-its-link.patch new file mode 100644 index 0000000..77ec50b --- /dev/null +++ b/cyberpunkunicorn-os/patches/0020-screen-stream-to-its-link.patch @@ -0,0 +1,46 @@ +diff --git a/src/core/tftLogger/tftLogger.cpp b/src/core/tftLogger/tftLogger.cpp +index 21efac8..dd24085 100644 +--- a/src/core/tftLogger/tftLogger.cpp ++++ b/src/core/tftLogger/tftLogger.cpp +@@ -78,6 +78,11 @@ void tft_logger::setLogging(bool _log) { + if (log) memset(log, 0, sizeof(tftLog) * MAX_LOG_ENTRIES); + logCount = 0; + }; ++// CyberUnicorn: the link that sent `display start`. serialDevice is the BLE serial ++// whenever the Bluetooth API is on, and switches to USB only while a USB command ++// runs, so the stream went to the phone and reached the cable in torn pieces. ++static SerialDevice *asyncSerialDev = nullptr; ++ + void tft_logger::asyncSerialTaskFunc(void *pv) { + tft_logger *logger = static_cast(pv); + tftLog item; +@@ -88,7 +93,7 @@ void tft_logger::asyncSerialTaskFunc(void *pv) { + if (fn == DRAWIMAGE) { + if (!logger->images) { + uint8_t size = entry[1]; +- serialDevice->write(entry, size); ++ asyncSerialDev->write(entry, size); + continue; + } + uint8_t imageSlot = entry[12]; +@@ -100,10 +105,10 @@ void tft_logger::asyncSerialTaskFunc(void *pv) { + if (imgLen > MAX_LOG_SIZE - baseLen) imgLen = MAX_LOG_SIZE - baseLen; + memcpy(packet + baseLen, imgPath, imgLen); + packet[1] = baseLen + imgLen; +- serialDevice->write(packet, baseLen + imgLen); ++ asyncSerialDev->write(packet, baseLen + imgLen); + } else { + uint8_t size = entry[1]; +- serialDevice->write(entry, size); ++ asyncSerialDev->write(entry, size); + } + } + } +@@ -117,6 +122,7 @@ void tft_logger::asyncSerialTaskFunc(void *pv) { + + void tft_logger::startAsyncSerial() { + if (async_serial) return; ++ asyncSerialDev = serialDevice; + async_serial = true; + setLogging(true); + asyncSerialQueue = xQueueCreate(MAX_LOG_ENTRIES, sizeof(tftLog));