Skip to content
Merged
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
2 changes: 2 additions & 0 deletions cyberpunkunicorn-os/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
30 changes: 27 additions & 3 deletions cyberpunkunicorn-os/apps/frequency/card.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,24 @@ String parseNdefUri(const std::vector<uint8_t> &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;
}

Expand Down Expand Up @@ -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";
Expand All @@ -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) {
Expand All @@ -211,6 +234,7 @@ CardStatus readerOpen() {
void readerClose() {
if (!held) return;
held = false;
hatBusGive();
xSemaphoreGive(readerMutex());
}

Expand Down
9 changes: 9 additions & 0 deletions cyberpunkunicorn-os/apps/frequency/card.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ CardStatus waitCard(
const std::function<void()> &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);
Expand Down
13 changes: 13 additions & 0 deletions cyberpunkunicorn-os/apps/hat/hat_probe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -232,13 +232,25 @@ 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;

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) {
Expand All @@ -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;
}
Expand Down
8 changes: 8 additions & 0 deletions cyberpunkunicorn-os/apps/hat/hat_probe.h
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
8 changes: 7 additions & 1 deletion cyberpunkunicorn-os/apps/remote/app_handlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions cyberpunkunicorn-os/apps/remote/remote.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion cyberpunkunicorn-os/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions cyberpunkunicorn-os/patches/0020-screen-stream-to-its-link.patch
Original file line number Diff line number Diff line change
@@ -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<tft_logger *>(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));
Loading