From 03d6c3e7f00c4285cd295ebd18cb6498409dd154 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 14:54:52 +0000 Subject: [PATCH] cyberpunkunicorn-os 0.1.4: a reader that takes no commands is woken or named Daniel's 0.1.3 logs: the PN532 ACKed its I2C address but every one of 43 polls in 30 s ended 'no ack', about 700 ms each, i.e. every transfer ran into Wire's timeout, even on a fresh power-up. readerOpen now asks for the firmware version at 100 kHz and logs the answer; if none comes it frees the bus (9-clock recovery) and starts the chip again once. A wait that sees 4 'no ack' polls in a row does the same once, then ends with CARD_READER_STUCK ('reader_stuck' over the remote protocol) instead of spinning to the timeout. Timeout replies also carry the ms. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_015Aw32AU4vVqCSEoNA5aP3U --- .../apps/frequency/FrequencyMenu.cpp | 23 +++++++- cyberpunkunicorn-os/apps/frequency/card.cpp | 57 +++++++++++++++++-- cyberpunkunicorn-os/apps/frequency/card.h | 1 + cyberpunkunicorn-os/apps/frequency/cli.cpp | 5 +- cyberpunkunicorn-os/apps/hat/hat_probe.cpp | 5 ++ cyberpunkunicorn-os/apps/hat/hat_probe.h | 4 ++ .../apps/remote/app_handlers.cpp | 18 +++++- cyberpunkunicorn-os/build.sh | 2 +- 8 files changed, 102 insertions(+), 13 deletions(-) diff --git a/cyberpunkunicorn-os/apps/frequency/FrequencyMenu.cpp b/cyberpunkunicorn-os/apps/frequency/FrequencyMenu.cpp index b47b4a3..20b4627 100644 --- a/cyberpunkunicorn-os/apps/frequency/FrequencyMenu.cpp +++ b/cyberpunkunicorn-os/apps/frequency/FrequencyMenu.cpp @@ -110,6 +110,10 @@ bool openReader(bool &anyway) { for (;;) { CardStatus s = readerOpen(); if (s == CARD_OK) return true; + if (s == CARD_READER_STUCK) { + notice("FREQUENCY", "READER STUCK", cardStatusText(s), "Unplug the board for 10 s, DIP 3 only."); + return false; + } if (s != CARD_NO_READER) { notice("FREQUENCY", "READER BUSY", cardStatusText(s), "Close the other screen or serial command."); return false; @@ -120,6 +124,12 @@ bool openReader(bool &anyway) { } } +// A wait that ended because the reader stopped taking commands says so. +void stuckNotice(CardStatus s) { + if (s == CARD_READER_STUCK) + notice("FREQUENCY", "READER STUCK", cardStatusText(s), "Unplug the board for 10 s, DIP 3 only."); +} + bool openReader() { bool anyway; return openReader(anyway); @@ -285,7 +295,10 @@ int writeLoop(const String &label, const UrlCheck &u) { cyberAnimate(); } ); - if (s != CARD_OK) return written; + if (s != CARD_OK) { + stuckNotice(s); + return written; + } CardInfo done; s = writeCard(u.fixed, done); @@ -520,7 +533,8 @@ void home() { ); if (s != CARD_OK) { readerClose(); - if (k == K_ESC || returnToMenu) return; + stuckNotice(s); + if (k == K_ESC || returnToMenu || s == CARD_READER_STUCK) return; if (!menu()) return; continue; } @@ -597,7 +611,10 @@ void issueMemberCard() { cyberAnimate(); } ); - if (s != CARD_OK) break; + if (s != CARD_OK) { + stuckNotice(s); + break; + } s = writeCard(u.fixed.length() ? u.fixed : r.url, done); if (s != CARD_OK) { notice("ISSUE CARD", "TRY AGAIN", writeProblem(s), "", "any key: try again", 4000); diff --git a/cyberpunkunicorn-os/apps/frequency/card.cpp b/cyberpunkunicorn-os/apps/frequency/card.cpp index bb58e75..b483906 100644 --- a/cyberpunkunicorn-os/apps/frequency/card.cpp +++ b/cyberpunkunicorn-os/apps/frequency/card.cpp @@ -4,6 +4,7 @@ #include "modules/rfid/PN532.h" #include "../hat/hat_probe.h" +#include "core/bus_HAL.h" #include "card.h" #include "flash.h" #include @@ -185,6 +186,44 @@ void fill(CardInfo &out) { out.uri = parseNdefUri(mem); } +// A PN532 can ACK its I2C address and still never take a command: every +// transfer then runs into Wire's timeout (0.1.3 logs: 43 polls in 30 s, all +// "no ack"). Ask for the firmware version, the one command with a fixed answer; +// if it doesn't come, free the bus (9-clock recovery), start it again and ask +// once more. +bool commandsAnswered() { + uint32_t v = rfid->nfc.getFirmwareVersion(); + // On serial, so the website's device log shows whether the chip took a command at all. + Serial.printf("[freq] PN532 firmware %s (0x%08lX)\n", v ? "answered" : "did not answer", (unsigned long)v); + return v != 0; +} + +bool startReader() { + for (int attempt = 0; attempt < 2; attempt++) { + if (attempt) { + Serial.println("[freq] reader not answering commands; recovering the I2C bus"); + releaseI2CBus(); // Wire must be off 22/27 while the lines are clocked by hand + hatI2CRecover(); + delay(50); + if (!rfid->begin()) continue; + } + // The PN532 is rated to 400 kHz but clock-stretches; 100 kHz is what Bruce's own NFC tools use. + acquireI2CBus()->setClock(100000); + if (!commandsAnswered()) continue; + rfid->nfc.SAMConfig(); + // Answer "no tag" after a few tries instead of waiting forever, so a poll + // never leaves a command pending in the chip. + rfid->nfc.setPassiveActivationRetries(0x05); + return true; + } + return false; +} + +// Polls in a row that got no ACK. A few of them means the reader stopped taking +// commands mid-wait; one recovery, then give up instead of spinning to the timeout. +constexpr int STUCK_AFTER = 4; +int noAckRun = 0; + } // namespace void resetPollStats() { stats = PollStats(); } @@ -202,6 +241,7 @@ const char *cardStatusText(CardStatus s) { case CARD_LOCKED: return "Tag is write-locked"; case CARD_WRITE_FAILED: return "Write failed - hold still"; case CARD_VERIFY_FAILED: return "Verify failed"; + case CARD_READER_STUCK: return "Reader not answering - unplug 10 s"; } return "?"; } @@ -224,10 +264,10 @@ CardStatus readerOpen() { readerClose(); return CARD_NO_READER; } - rfid->nfc.SAMConfig(); - // Answer "no tag" after a few tries instead of waiting forever, so a poll - // never leaves a command pending in the chip. - rfid->nfc.setPassiveActivationRetries(0x05); + if (!startReader()) { + readerClose(); + return CARD_READER_STUCK; + } return CARD_OK; } @@ -252,9 +292,18 @@ CardStatus waitCard( CardInfo &out, uint32_t timeoutMs, const std::function &cancel, const std::function &idle ) { uint32_t start = millis(); + noAckRun = 0; + bool recovered = false; for (;;) { + uint32_t acksBefore = stats.noAck; CardStatus s = pollCard(out); if (s != CARD_NONE) return s; + noAckRun = stats.noAck != acksBefore ? noAckRun + 1 : 0; + if (noAckRun >= STUCK_AFTER) { + if (recovered || !startReader()) return CARD_READER_STUCK; + recovered = true; + noAckRun = 0; + } if (cancel && cancel()) return CARD_NONE; if (timeoutMs && millis() - start > timeoutMs) return CARD_NONE; if (idle) idle(); diff --git a/cyberpunkunicorn-os/apps/frequency/card.h b/cyberpunkunicorn-os/apps/frequency/card.h index 0e308fb..33ca50e 100644 --- a/cyberpunkunicorn-os/apps/frequency/card.h +++ b/cyberpunkunicorn-os/apps/frequency/card.h @@ -20,6 +20,7 @@ enum CardStatus { CARD_LOCKED, // already write-locked CARD_WRITE_FAILED, CARD_VERIFY_FAILED, // wrote, but read back something else + CARD_READER_STUCK, // PN532 answers its address but not commands, even after a bus recovery }; const char *cardStatusText(CardStatus s); diff --git a/cyberpunkunicorn-os/apps/frequency/cli.cpp b/cyberpunkunicorn-os/apps/frequency/cli.cpp index fa9ea44..f2c91bc 100644 --- a/cyberpunkunicorn-os/apps/frequency/cli.cpp +++ b/cyberpunkunicorn-os/apps/frequency/cli.cpp @@ -44,8 +44,9 @@ void printTag(const CardInfo &c) { bool waitTag(CardInfo &c, uint32_t timeout) { out("Tap a tag (" + String(timeout) + " ms)..."); - if (waitCard(c, timeout, nullptr) == CARD_OK) return true; - out("No tag."); + CardStatus s = waitCard(c, timeout, nullptr); + if (s == CARD_OK) return true; + out(s == CARD_READER_STUCK ? String("ERROR: ") + cardStatusText(s) : String("No tag.")); return false; } diff --git a/cyberpunkunicorn-os/apps/hat/hat_probe.cpp b/cyberpunkunicorn-os/apps/hat/hat_probe.cpp index e4ce62d..d856075 100644 --- a/cyberpunkunicorn-os/apps/hat/hat_probe.cpp +++ b/cyberpunkunicorn-os/apps/hat/hat_probe.cpp @@ -239,6 +239,11 @@ SemaphoreHandle_t hatBus() { } } // namespace +bool hatI2CRecover() { + recoverI2C(); + return linesIdle(); +} + bool hatBusTake(uint32_t waitMs) { return xSemaphoreTakeRecursive(hatBus(), pdMS_TO_TICKS(waitMs)) == pdTRUE; } void hatBusGive() { xSemaphoreGiveRecursive(hatBus()); } diff --git a/cyberpunkunicorn-os/apps/hat/hat_probe.h b/cyberpunkunicorn-os/apps/hat/hat_probe.h index 41cc1f7..367ee79 100644 --- a/cyberpunkunicorn-os/apps/hat/hat_probe.h +++ b/cyberpunkunicorn-os/apps/hat/hat_probe.h @@ -45,6 +45,10 @@ struct HatProbeResult { bool hatBusTake(uint32_t waitMs); void hatBusGive(); +// Clocks a chip stuck mid-byte off SDA (the standard 9-clock recovery) and says +// whether both lines then idle high. Call with no Wire instance running on 22/27. +bool hatI2CRecover(); + // 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 diff --git a/cyberpunkunicorn-os/apps/remote/app_handlers.cpp b/cyberpunkunicorn-os/apps/remote/app_handlers.cpp index 9a7b51a..df8d873 100644 --- a/cyberpunkunicorn-os/apps/remote/app_handlers.cpp +++ b/cyberpunkunicorn-os/apps/remote/app_handlers.cpp @@ -87,22 +87,34 @@ void putCard(JsonObject o, const freq::CardInfo &c) { bool tap(remote::Request &r, freq::CardInfo &card) { freq::CardStatus s = freq::readerOpen(); if (s != freq::CARD_OK) { - r.fail(s == freq::CARD_NO_READER ? "no_reader" : "busy", freq::cardStatusText(s)); + r.fail( + s == freq::CARD_NO_READER ? "no_reader" + : s == freq::CARD_READER_STUCK ? "reader_stuck" + : "busy", + freq::cardStatusText(s) + ); return false; } uint32_t timeout = r.args["timeout_ms"] | TAP_TIMEOUT_MS; if (timeout > 30000) timeout = 30000; remote::emit("frequency.waiting_for_tag", JsonObjectConst()); freq::resetPollStats(); + uint32_t started = millis(); s = freq::waitCard(card, timeout, [] { return false; }); if (s != freq::CARD_OK) { freq::readerClose(); // 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)"; + p.noAck + " no ack, " + p.noAnswer + " no answer, " + p.selected + " seen, " + + (millis() - started) + " ms)"; Serial.println("[freq] " + why); - r.fail(s == freq::CARD_NONE ? "timeout" : "card_error", why); + r.fail( + s == freq::CARD_NONE ? "timeout" + : s == freq::CARD_READER_STUCK ? "reader_stuck" + : "card_error", + why + ); return false; } return true; diff --git a/cyberpunkunicorn-os/build.sh b/cyberpunkunicorn-os/build.sh index b7e06ac..328b8cb 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.3 +OS_VERSION=0.1.4 source ./boot.conf for var in BOOT_TITLE BOOT_TAGLINE; do