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
47 changes: 47 additions & 0 deletions cyd-port/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# nyanBOX — Cheap Yellow Display (ESP32‑2432S028) port

A port of nyanBOX to the **CYD** ("Cheap Yellow Display") ESP32‑2432S028 board — a
2.8" ILI9341 240×320 SPI TFT with an XPT2046 resistive touchscreen and a classic
ESP32‑WROOM (4 MB flash, no PSRAM). No physical buttons and no OLED, so this port
adapts the UI and input while keeping the original nyanBOX feature set intact
(all app modules build unchanged).

Built as a new PlatformIO env — the original `nyanbox-main` / `hardware-test` envs
are untouched. Build with `pio run -e nyanbox-cyd`.

## What changed vs. stock nyanBOX

- **Radio pin remap** (`include/pindefs.h`): the 3× nRF24 share the CYD's SD‑card
SPI bus (SCK18 / MISO19 / MOSI23, unchanged in code) and the CE/CSN pairs are
moved to free pins — **R1 22/5, R2 27/17, R3 4/16** — because the stock pins
collide with the TFT (15=CS, 2=DC). Sacrifices the microSD slot and the RGB LED.
NeoPixel moved 14→0 (14 = TFT_SCLK).
- **Display bridge** (`include/cyd_u8g2_bridge.h`, `src/cyd_u8g2_bridge.cpp`): a
`CydU8g2` subclass keeps the 128×64 U8g2 mono full‑buffer (zero I2C traffic) and
blits it, letterboxed, onto the ILI9341 via TFT_eSPI. A build‑flag macro rewrites
the SSD1306 type so all 42 UI files compile unchanged.
- **Touch input** (`src/touch_input.cpp`, `include/touch_input.h`): bit‑banged
XPT2046 (software SPI, so it doesn't contend for HSPI=TFT / VSPI=radios).
**Pressure‑gated** (not PENIRQ, which is unreliable on the CYD). **Self‑calibrating**
— a 4‑corner tap routine on first boot auto‑detects axis swap/direction/range and
stores it to EEPROM (hold a finger at power‑up to re‑run it). `nyanDigitalRead()`
shims the five logical buttons onto touch.
- **Touch‑native menus** (`src/nyanBOX.ino`): tap a row to select, drag a left‑edge
slider to scroll, a bottom BACK/LEVEL bar — no arrow reliance. Apps auto‑switch to
a 5‑zone on‑screen D‑pad (● center exits). Menu vs. app mode is an explicit flag
toggled at the `runApp()` boundary (can't use `currentState` — `runApp()` blocks).
- **Boot robustness**: the radio‑init loop no longer hangs forever when a radio is
absent (it skips missing radios), and the backlight (GPIO21) is driven explicitly —
both were causes of a dark screen on the CYD.

## Wiring notes (nRF24)

3× nRF24 at PA_MAX will brown out the CYD's onboard AMS1117‑3.3. Power the radios
from the **5V header through per‑module 3.3V regulation** (socket adapters or a
≥500 mA buck) with 10–100 µF + 100 nF decoupling per module, common ground. If
`isChipConnected()` is flaky, drop the RF24 SPI clock 16 MHz→8 MHz.

## Status

Builds green, boots, display + touch verified on hardware. Radios pending physical
wiring per the notes above.
274 changes: 274 additions & 0 deletions cyd-port/hardware-test/nyanbox_hardware_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
/*
nyanBOX by Nyan Devices
https://github.com/jbohack/nyanBOX
Copyright (c) 2025 jbohack

Licensed under the MIT License
https://opensource.org/licenses/MIT

SPDX-License-Identifier: MIT
*/

#include <Arduino.h>
#include <U8g2lib.h>
#include <SPI.h>
#include <RF24.h>
#include <Adafruit_NeoPixel.h>
#include "../include/pindefs.h"

const char* VERSION = "v1.1";

U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
Adafruit_NeoPixel pixels(1, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
RF24 radios[] = {
RF24(RADIO_CE_PIN_1, RADIO_CSN_PIN_1, 16000000),
RF24(RADIO_CE_PIN_2, RADIO_CSN_PIN_2, 16000000),
RF24(RADIO_CE_PIN_3, RADIO_CSN_PIN_3, 16000000)
};

bool radioResults[3] = {false, false, false};
bool testingComplete = false;
int testPhase = 0;

void displayTest() {
u8g2.clearBuffer();
u8g2.drawFrame(0, 0, 128, 64);
u8g2.drawFrame(2, 2, 124, 60);
u8g2.setFont(u8g2_font_helvB12_tr);
u8g2.drawStr(20, 22, "nyanBOX");
u8g2.setFont(u8g2_font_helvR08_tr);
u8g2.drawStr(20, 38, "HW TEST ");
u8g2.drawStr(70, 38, VERSION);
u8g2.sendBuffer();
delay(800);

// Full white screen
u8g2.clearBuffer();
u8g2.drawBox(0, 0, 128, 64);
u8g2.sendBuffer();
delay(500);

// Simple alternating pattern
u8g2.clearBuffer();
for (int x = 0; x < 128; x += 16) {
for (int y = 0; y < 64; y += 16) {
u8g2.drawBox(x, y, 8, 8);
u8g2.drawBox(x + 8, y + 8, 8, 8);
}
}
u8g2.sendBuffer();
delay(500);

// Lines test
u8g2.clearBuffer();
u8g2.drawHLine(0, 16, 128);
u8g2.drawHLine(0, 32, 128);
u8g2.drawHLine(0, 48, 128);
u8g2.drawVLine(32, 0, 64);
u8g2.drawVLine(64, 0, 64);
u8g2.drawVLine(96, 0, 64);
u8g2.sendBuffer();
delay(500);

// Border and corners
u8g2.clearBuffer();
u8g2.drawFrame(0, 0, 128, 64);
u8g2.drawFrame(2, 2, 124, 60);
u8g2.drawBox(0, 0, 10, 10);
u8g2.drawBox(118, 0, 10, 10);
u8g2.drawBox(0, 54, 10, 10);
u8g2.drawBox(118, 54, 10, 10);
u8g2.sendBuffer();
delay(500);
}

void setup() {
Serial.begin(115200);
delay(100);

Serial.println("");
Serial.println("========================================");
Serial.print("nyanBOX Hardware QC Test ");
Serial.println(VERSION);
Serial.println("Manufacturing Quality Control");
Serial.println("https://github.com/jbohack/nyanBOX");
Serial.println("========================================");

u8g2.begin();
pixels.begin();
pixels.clear();
pixels.show();

int buttonPins[] = {BUTTON_PIN_UP, BUTTON_PIN_DOWN, BUTTON_PIN_LEFT, BUTTON_PIN_RIGHT, BUTTON_PIN_CENTER};
for (int pin : buttonPins) pinMode(pin, INPUT_PULLUP);

Serial.println("Starting display test...");
displayTest();

SPI.begin();
int cePins[] = {RADIO_CE_PIN_1, RADIO_CE_PIN_2, RADIO_CE_PIN_3};
int csnPins[] = {RADIO_CSN_PIN_1, RADIO_CSN_PIN_2, RADIO_CSN_PIN_3};

for (int i = 0; i < 3; i++) {
pinMode(cePins[i], OUTPUT);
pinMode(csnPins[i], OUTPUT);
digitalWrite(csnPins[i], HIGH);
digitalWrite(cePins[i], LOW);
}
delay(100);

Serial.println("");
Serial.println("Enabling NeoPixel RGB cycle...");
Serial.println("");
testPhase = 1;
}


void loop() {
static unsigned long lastPhaseTime = 0;
static unsigned long lastButtonCheck = 0;
static unsigned long lastNeo = 0;
static int colorStep = 0;
if (millis() - lastNeo > 5) {
lastNeo = millis();
int r = 0, g = 0, b = 0;
int phase = (colorStep / 85) % 6;
int fade = colorStep % 85;
int intensity = (fade * 255) / 84;

switch(phase) {
case 0: r = 255; g = intensity; break;
case 1: r = 255 - intensity; g = 255; break;
case 2: g = 255; b = intensity; break;
case 3: g = 255 - intensity; b = 255; break;
case 4: b = 255; r = intensity; break;
case 5: b = 255 - intensity; r = 255; break;
}

pixels.setPixelColor(0, pixels.Color(r, g, b));
pixels.show();
colorStep = (colorStep + 1) % 510;
}

if (!testingComplete && millis() - lastPhaseTime > 250) {
lastPhaseTime = millis();

int radioIndex = testPhase - 1;
if (radioIndex < 3) {
Serial.print("Starting Radio ");
Serial.print(radioIndex + 1);
Serial.println(" test...");

u8g2.clearBuffer();
u8g2.setFont(u8g2_font_helvB12_tr);
u8g2.drawStr(10, 16, (String("RADIO ") + (radioIndex + 1) + " TEST").c_str());

const char* pinInfo[] = {"CE:5, CSN:17", "CE:16, CSN:4", "CE:15, CSN:2"};
u8g2.setFont(u8g2_font_helvR08_tr);
u8g2.drawStr(10, 32, pinInfo[radioIndex]);

radioResults[radioIndex] = radios[radioIndex].begin();
if (radioResults[radioIndex] && radios[radioIndex].isChipConnected()) {
radios[radioIndex].setChannel(radioIndex + 1);
radios[radioIndex].setPALevel(RF24_PA_LOW);
Serial.print("Radio");
Serial.print(radioIndex + 1);
Serial.println(": OK");
u8g2.drawStr(10, 48, "Status: OK");
} else {
radioResults[radioIndex] = false;
Serial.print("Radio");
Serial.print(radioIndex + 1);
Serial.println(": FAIL");
u8g2.drawStr(10, 48, "Status: FAIL");
}
u8g2.sendBuffer();
testPhase++;

} else {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_helvB12_tr);
u8g2.drawStr(10, 15, "RESULTS");
u8g2.setFont(u8g2_font_6x10_tr);

for (int i = 0; i < 3; i++) {
String result = "Radio" + String(i + 1) + ": " + (radioResults[i] ? "OK" : "FAIL");
u8g2.drawStr(10, 28 + i * 12, result.c_str());
}

u8g2.setFont(u8g2_font_5x8_tr);
u8g2.drawStr(10, 62, "Press any button");
u8g2.sendBuffer();
Serial.println("");

Serial.println("==== SUMMARY ====");
const char* pins[] = {"(CE:5, CSN:17)", "(CE:16, CSN:4)", "(CE:15, CSN:2)"};
for (int i = 0; i < 3; i++) {
Serial.print("Radio");
Serial.print(i + 1);
Serial.print(" ");
Serial.print(pins[i]);
Serial.println(radioResults[i] ? ": OK" : ": FAIL");
}
Serial.println("Button test ready...");
testingComplete = true;
}
}

if (millis() - lastButtonCheck > 100) {
lastButtonCheck = millis();
static bool lastButtons[5] = {false, false, false, false, false};
const char* buttonNames[] = {"UP", "DOWN", "LEFT", "RIGHT", "CENTER"};
const int buttonPins[] = {BUTTON_PIN_UP, BUTTON_PIN_DOWN, BUTTON_PIN_LEFT, BUTTON_PIN_RIGHT, BUTTON_PIN_CENTER};

for (int i = 0; i < 5; i++) {
bool pressed = !digitalRead(buttonPins[i]);
if (pressed && !lastButtons[i]) {
Serial.print("BUTTON: ");
Serial.println(buttonNames[i]);

if (testingComplete) {
u8g2.clearBuffer();

switch(i) {
case 0: // UP
u8g2.drawTriangle(64, 20, 54, 35, 74, 35);
u8g2.drawBox(59, 35, 10, 15);
break;
case 1: // DOWN
u8g2.drawBox(59, 15, 10, 15);
u8g2.drawTriangle(64, 45, 54, 30, 74, 30);
break;
case 2: // LEFT
u8g2.drawTriangle(35, 32, 50, 22, 50, 42);
u8g2.drawBox(50, 27, 15, 10);
break;
case 3: // RIGHT
u8g2.drawBox(63, 27, 15, 10);
u8g2.drawTriangle(93, 32, 78, 22, 78, 42);
break;
case 4: // CENTER
u8g2.drawCircle(64, 32, 12);
u8g2.drawDisc(64, 32, 6);
break;
}
u8g2.sendBuffer();
delay(300);

u8g2.clearBuffer();
u8g2.setFont(u8g2_font_helvB12_tr);
u8g2.drawStr(10, 15, "RESULTS");
u8g2.setFont(u8g2_font_6x10_tr);
for (int j = 0; j < 3; j++) {
String result = "Radio" + String(j + 1) + ": " + (radioResults[j] ? "OK" : "FAIL");
u8g2.drawStr(10, 28 + j * 12, result.c_str());
}
u8g2.setFont(u8g2_font_5x8_tr);
u8g2.drawStr(10, 62, "Press any button");
u8g2.sendBuffer();
}
}
lastButtons[i] = pressed;
}
}
}
25 changes: 25 additions & 0 deletions cyd-port/include/about.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
nyanBOX by Nyan Devices
https://github.com/jbohack/nyanBOX
Copyright (c) 2026 jbohack

Licensed under the MIT License
https://opensource.org/licenses/MIT

SPDX-License-Identifier: MIT
*/

#ifndef ABOUT_H
#define ABOUT_H

#include <U8g2lib.h>
#include "pindefs.h"

#define NYANBOX_VERSION "v4.27.11"
extern const char* nyanboxVersion;

void aboutSetup();
void aboutLoop();
void aboutCleanup();

#endif
35 changes: 35 additions & 0 deletions cyd-port/include/airtag_detector.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
nyanBOX by Nyan Devices
https://github.com/jbohack/nyanBOX
Copyright (c) 2025 jbohack

Licensed under the MIT License
https://opensource.org/licenses/MIT

SPDX-License-Identifier: MIT
*/

#ifndef airtag_H
#define airtag_H

#include <U8g2lib.h>
#include <vector>
#include "neopixel.h"
#include "pindefs.h"

struct AirTagDeviceData {
char name[32];
char address[18];
int8_t rssi;
unsigned long lastSeen;
uint8_t payload[64];
size_t payloadLength;
bool isAirTag;
};

extern std::vector<AirTagDeviceData> airtagDevices;

void airtagDetectorSetup();
void airtagDetectorLoop();

#endif
Loading