From b41050c2c6d911a3bab91faa5a17c65b12762079 Mon Sep 17 00:00:00 2001 From: shinya Date: Thu, 21 May 2026 14:11:06 +0200 Subject: [PATCH] readmes and gps changes --- README.md | 21 ++++- camera_example/README.md | 34 +++---- chip_test_example/README.md | 120 ++++++++++++++++++++++++ chip_test_example/gps.hpp | 162 +++++++++++++++++++++++---------- chip_test_example/gps_test.cpp | 93 ++++++++++++++----- math_example/README.md | 38 ++++++++ shader_example/README.md | 10 ++ 7 files changed, 387 insertions(+), 91 deletions(-) create mode 100644 chip_test_example/README.md create mode 100644 math_example/README.md create mode 100644 shader_example/README.md diff --git a/README.md b/README.md index aa0ed8e..18ca29e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,22 @@ # SkyLok +Pi Zero W 2 captures frames, runs a motion shader, sends diffs over LoRa. + +- `camera_example/` — libcamera + libjpeg-turbo, streams MJPEG over TCP. Works. +- `shader_example/` — offscreen GL motion diff between two frames. Works on desktop. +- `math_example/` — pixel → 3D vertex projection (depth map to OBJ mesh). +- `chip_test_example/` — LR1121 LoRa + ICM-20948 IMU + u-blox GPS drivers/tests. +- `main.cpp` — prototype that wires camera + shader + TCP together. + +References: - https://www.youtube.com/watch?v=zFiubdrJqqI -- Client: libcamera::DirectLowResVid -> EGL::MotionShader -> MATH::Vortexes -> SPI/RadioHead::LoRa -> Server -- Server: eats all incoming and cooks +- https://github.com/ConsistentlyInconsistentYT/Pixeltovoxelprojector/ + +## Build main.cpp + +```sh +g++ main.cpp -o main \ + $(pkg-config --cflags --libs libcamera libevent) \ + -levent -levent_pthreads -lpthread \ + -lturbojpeg -lglfw -lGLEW -lGL -lEGL -lGLESv2 +``` diff --git a/camera_example/README.md b/camera_example/README.md index 3e9fb41..76dc0eb 100644 --- a/camera_example/README.md +++ b/camera_example/README.md @@ -1,27 +1,29 @@ # camera_example -libcamera viewfinder (XRGB8888) -> libjpeg-turbo -> TCP. One client at a time. +libcamera viewfinder (XRGB8888) → libjpeg-turbo → TCP. One client at a time. -Wire format: `[uint32 BE jpeg_size][jpeg_bytes]` repeating. +Wire format: `[uint32 BE size][jpeg_bytes]` repeating. -Knobs are constants at the top of `camera.cpp` (fps, quality, AF mode, lens -position, port, resolution). +Knobs (fps, quality, AF, lens, port, resolution) are `constexpr` at the top of `camera.cpp`. ## Build on the Pi - apt install g++ pkg-config libcamera-dev libevent-dev libjpeg-turbo8-dev - make - -## Cross-build (x86 -> armv6l) on a Nix host - - nix build - -The resulting binary links against `/nix/store`, so it only runs on a Pi that -has those libs available (i.e. nix installed). For a vanilla Pi OS target, -use the Makefile on the device, or `./deploy.sh user@host` to rsync+build. +```sh +apt install g++ pkg-config libcamera-dev libevent-dev libjpeg-turbo8-dev +make +``` ## Receive -Any TCP client that reads the framed JPEGs. Quick check: +```sh +nc 5000 | ffplay -f mjpeg - +``` - nc 5000 | ffplay -f mjpeg - +## Cross-build (Nix, x86 → armv6l) + +```sh +nix build +``` + +Binary links against `/nix/store` so it only runs on a Pi with Nix installed. +For plain Pi OS: use `make` on the device, or `./deploy.sh user@host` to rsync+build. diff --git a/chip_test_example/README.md b/chip_test_example/README.md new file mode 100644 index 0000000..79de62d --- /dev/null +++ b/chip_test_example/README.md @@ -0,0 +1,120 @@ +# chip_test_example + +Tests for LR1121 LoRa, ICM-20948 IMU, u-blox GPS on Raspberry Pi Zero W 2. +Header-only drivers, kernel ioctls, no external libs. + +--- + +## Wiring + +### LR1121 (SPI0) + +| Module | Pi GPIO | Pi pin | +|--------|---------|--------| +| SCK | GPIO11 | 23 | +| MOSI | GPIO10 | 19 | +| MISO | GPIO9 | 21 | +| NSS | GPIO8 | 24 | +| BUSY | GPIO24 | 18 | +| NRESET | GPIO25 | 22 | +| DIO9 | GPIO4 | 7 | +| DIO8 | GPIO23 | 16 | + +Enable: `sudo raspi-config` → Interfaces → SPI → Yes → reboot + +### ICM-20948 (I2C1) + +| Module | Pi GPIO | Pi pin | +|--------|---------|--------| +| SDA | GPIO2 | 3 | +| SCL | GPIO3 | 5 | +| AD0 | GND → addr 0x68 / VCC → addr 0x69 | + +Enable: `sudo raspi-config` → Interfaces → I2C → Yes → reboot + +### u-blox GPS (UART0) + +| Module | Pi GPIO | Pi pin | +|--------|---------|--------| +| TX | GPIO15 | 10 | +| RX | GPIO14 | 8 | + +Enable: `sudo raspi-config` → Interfaces → Serial Port → login shell: No, hardware: Yes → reboot + +--- + +## Build + +```sh +sudo apt install g++ make +make +``` + +Run verbose first so you see exactly where it fails: + +```sh +sudo ./lora_rx -v --433 +sudo ./imu_test -v +./gps_test -v +``` + +--- + +## LoRa debug + +```sh +ls /dev/spidev0.0 # SPI on? +sudo ./lora_rx -v --433 # step labels show exactly which command hangs +``` + +If it hangs at `Calibrate` — that's a TCXO config issue. Try in order: + +```sh +sudo ./lora_rx -v --433 --tcxo-none # skip TCXO entirely (crystal mode) +sudo ./lora_rx -v --433 --tcxo-27 # TCXO 2.7V +sudo ./lora_rx -v --433 --tcxo-33 # TCXO 3.3V (default) +``` + +The one that gets past "Calibrate done" is your module's config. +Use the same TCXO flag on TX and RX. + +If using the 2.4 GHz antenna instead: + +```sh +sudo ./lora_rx -v --24 +sudo ./lora_tx -v --24 +``` + +--- + +## IMU debug + +```sh +sudo i2cdetect -y 1 # 0x68 or 0x69 must show +sudo ./imu_test -v # auto-detects both addresses +sudo ./imu_test -v -r # raw 16-bit values +``` + +Nothing on i2cdetect: I2C not enabled, wrong wiring, or no pull-ups on SDA/SCL. + +--- + +## GPS debug + +```sh +ls -la /dev/serial0 +stty -F /dev/serial0 38400 raw && cat /dev/serial0 # raw NMEA bytes +./gps_test -v -a # all sentences +./gps_test -b 9600 # try different baud +``` + +--- + +## TCXO voltage reference + +| Flag | Value | Voltage | +|------|-------|---------| +| `--tcxo-none` | 0xFF | no TCXO (crystal) | +| `--tcxo-27` | 0x05 | 2.7V | +| `--tcxo-33` | 0x07 | 3.3V | +| `--tcxo-v N` | 0x00–0x07 | raw byte | diff --git a/chip_test_example/gps.hpp b/chip_test_example/gps.hpp index 6042067..fef202e 100644 --- a/chip_test_example/gps.hpp +++ b/chip_test_example/gps.hpp @@ -1,19 +1,21 @@ // gps.hpp — u-blox NMEA GPS reader over UART // No external libs; uses POSIX termios + read(). // -// Hardware pins (THE TRUTH): -// GPIO14/TXD0 → UBLOX_TX line (/dev/serial0) -// GPIO15/RXD0 → UBLOX_RX line -// Default baud: 9600 +// Hardware (THE TRUTH): +// GPIO14/TXD0 → UBLOX_TX (/dev/serial0) +// GPIO15/RXD0 → UBLOX_RX +// Default baud: 38400 (factory default on this module) // // Enable UART: sudo raspi-config → Interfaces → Serial Port // "login shell over serial" = No, "serial port hardware" = Yes +// Then reboot. Check: ls -la /dev/serial0 #pragma once -#include -#include +#include #include +#include #include +#include #include #include @@ -22,54 +24,93 @@ namespace gps { struct Fix { - bool valid = false; - double lat = 0.0; // degrees, positive=N - double lon = 0.0; // degrees, positive=E + bool valid = false; + double lat = 0.0; // degrees, positive=N + double lon = 0.0; // degrees, positive=E float speed_knots = 0.0f; - char time[12]{}; // HHMMSS.ss (raw NMEA) - char date[7]{}; // DDMMYY + char time[12]{}; // HHMMSS.ss (raw NMEA) + char date[7]{}; // DDMMYY +}; + +struct Config { + const char *dev = "/dev/serial0"; + int baud = 38400; // confirmed working baud for this module + bool verbose = false; }; class Reader { public: - bool begin(const char *dev = "/dev/serial0", int baud = 9600); + bool begin(const Config &cfg); + bool begin(const char *dev = "/dev/serial0", int baud = 38400); void end(); - // Read one NMEA sentence into buf (null-terminated). Returns false on error. - bool readLine(char *buf, size_t cap); + // Read one NMEA sentence (null-terminated, includes '\n'). + // timeout_ms: 0 = block forever, >0 = return false after timeout. + bool readLine(char *buf, size_t cap, uint32_t timeout_ms = 0); - // Parse $GPRMC or $GNRMC from a sentence. Returns false if sentence is - // not RMC or position is invalid. + // Parse $GPRMC or $GNRMC. Returns false if wrong sentence type or fix void. static bool parseRmc(const char *sentence, Fix &fix); + // Verify NMEA checksum: XOR of bytes between '$' and '*'. + static bool verifyChecksum(const char *sentence); + private: - int fd_ = -1; + int fd_ = -1; + bool verbose_ = false; }; +// ---- Implementation --------------------------------------------------------- + +inline bool Reader::begin(const Config &c) +{ + verbose_ = c.verbose; + return begin(c.dev, c.baud); +} + inline bool Reader::begin(const char *dev, int baud) { fd_ = ::open(dev, O_RDWR | O_NOCTTY | O_NDELAY); - if (fd_ < 0) return false; - ::fcntl(fd_, F_SETFL, 0); // blocking + if (fd_ < 0) { + if (verbose_) + std::fprintf(stderr, "[gps] cannot open %s: %m\n", dev); + return false; + } + ::fcntl(fd_, F_SETFL, 0); // blocking reads termios t{}; - if (tcgetattr(fd_, &t) < 0) return false; + if (tcgetattr(fd_, &t) < 0) { + if (verbose_) std::fprintf(stderr, "[gps] tcgetattr failed: %m\n"); + ::close(fd_); fd_ = -1; + return false; + } speed_t spd; switch (baud) { - case 4800: spd = B4800; break; - case 9600: spd = B9600; break; - case 19200: spd = B19200; break; - case 38400: spd = B38400; break; - case 57600: spd = B57600; break; - default: spd = B9600; + case 4800: spd = B4800; break; + case 9600: spd = B9600; break; + case 19200: spd = B19200; break; + case 38400: spd = B38400; break; + case 57600: spd = B57600; break; + case 115200: spd = B115200; break; + default: + if (verbose_) + std::fprintf(stderr, "[gps] unknown baud %d, defaulting to 38400\n", baud); + spd = B38400; } cfsetispeed(&t, spd); cfsetospeed(&t, spd); cfmakeraw(&t); t.c_cc[VMIN] = 1; t.c_cc[VTIME] = 0; - tcsetattr(fd_, TCSANOW, &t); + if (tcsetattr(fd_, TCSANOW, &t) < 0) { + if (verbose_) std::fprintf(stderr, "[gps] tcsetattr failed: %m\n"); + ::close(fd_); fd_ = -1; + return false; + } + tcflush(fd_, TCIOFLUSH); + + if (verbose_) + std::fprintf(stderr, "[gps] opened %s at %d baud\n", dev, baud); return true; } @@ -78,18 +119,37 @@ inline void Reader::end() if (fd_ >= 0) { ::close(fd_); fd_ = -1; } } -inline bool Reader::readLine(char *buf, size_t cap) +inline bool Reader::readLine(char *buf, size_t cap, uint32_t timeout_ms) { + using clock = std::chrono::steady_clock; + auto deadline = clock::now() + std::chrono::milliseconds(timeout_ms); + + auto timed_read = [&](char &c) -> bool { + if (timeout_ms == 0) { + return ::read(fd_, &c, 1) == 1; + } + // Non-blocking check with timeout. + while (true) { + ssize_t r = ::read(fd_, &c, 1); + if (r == 1) return true; + if (clock::now() >= deadline) return false; + // Brief yield to avoid spinning. + struct timespec ts = { 0, 1'000'000 }; // 1 ms + nanosleep(&ts, nullptr); + } + }; + size_t n = 0; char c; - // Wait for '$' (start of NMEA sentence) + + // Wait for '$' (start of sentence). do { - if (::read(fd_, &c, 1) != 1) return false; + if (!timed_read(c)) return false; } while (c != '$'); buf[n++] = '$'; while (n < cap - 1) { - if (::read(fd_, &c, 1) != 1) return false; + if (!timed_read(c)) return false; buf[n++] = c; if (c == '\n') break; } @@ -97,23 +157,38 @@ inline bool Reader::readLine(char *buf, size_t cap) return true; } -// Parse $GPRMC or $GNRMC -// Format: $GNRMC,HHMMSS.ss,A,DDMM.MMMM,N,DDDMM.MMMM,E,spd,crs,DDMMYY,... +inline bool Reader::verifyChecksum(const char *sentence) +{ + // Checksum is XOR of all bytes between '$' (exclusive) and '*' (exclusive). + const char *p = sentence; + if (*p == '$') ++p; + + uint8_t calc = 0; + while (*p && *p != '*') calc ^= (uint8_t)*p++; + if (*p != '*') return false; // no checksum present + + unsigned int given = 0; + if (std::sscanf(p + 1, "%02X", &given) != 1) return false; + return (uint8_t)given == calc; +} + +// Parse $GPRMC or $GNRMC. +// Format: $GNRMC,HHMMSS.ss,A,DDMM.MMMM,N,DDDMM.MMMM,E,spd,crs,DDMMYY,...*CS inline bool Reader::parseRmc(const char *sentence, Fix &fix) { if (!sentence) return false; - - // Check sentence type if (std::strncmp(sentence, "$GPRMC", 6) != 0 && std::strncmp(sentence, "$GNRMC", 6) != 0) return false; - // Make a mutable copy + // Make a mutable copy for tokenization (strip the *CS checksum tail). char buf[128]; std::strncpy(buf, sentence, sizeof(buf) - 1); buf[sizeof(buf) - 1] = '\0'; + // Terminate at '*' so strtok doesn't swallow the checksum as a field. + char *star = std::strchr(buf, '*'); + if (star) *star = '\0'; - // Tokenize on commas const char *fields[13]{}; int nf = 0; char *p = buf; @@ -126,31 +201,22 @@ inline bool Reader::parseRmc(const char *sentence, Fix &fix) } if (nf < 10) return false; - // field[2] = status ('A'=valid, 'V'=void) if (fields[2][0] != 'A') { fix.valid = false; return false; } - // field[1] = time std::strncpy(fix.time, fields[1], sizeof(fix.time) - 1); - // field[3] = lat DDMM.MMMM, field[4] = N/S double raw_lat = std::atof(fields[3]); int lat_deg = (int)(raw_lat / 100.0); - double lat_min = raw_lat - lat_deg * 100.0; - fix.lat = lat_deg + lat_min / 60.0; + fix.lat = lat_deg + (raw_lat - lat_deg * 100.0) / 60.0; if (fields[4][0] == 'S') fix.lat = -fix.lat; - // field[5] = lon DDDMM.MMMM, field[6] = E/W double raw_lon = std::atof(fields[5]); int lon_deg = (int)(raw_lon / 100.0); - double lon_min = raw_lon - lon_deg * 100.0; - fix.lon = lon_deg + lon_min / 60.0; + fix.lon = lon_deg + (raw_lon - lon_deg * 100.0) / 60.0; if (fields[6][0] == 'W') fix.lon = -fix.lon; fix.speed_knots = (float)std::atof(fields[7]); - - // field[9] = date DDMMYY std::strncpy(fix.date, fields[9], sizeof(fix.date) - 1); - fix.valid = true; return true; } diff --git a/chip_test_example/gps_test.cpp b/chip_test_example/gps_test.cpp index 199817a..77c816d 100644 --- a/chip_test_example/gps_test.cpp +++ b/chip_test_example/gps_test.cpp @@ -1,39 +1,82 @@ +// gps_test.cpp — u-blox GPS NMEA test +// Usage: ./gps_test [-v] [-a] [-b BAUD] +// -v verbose init debug +// -a print ALL NMEA sentences (not just RMC) +// -b BAUD set baud rate (default 38400) +// +// Enable UART: sudo raspi-config → Interfaces → Serial Port +// login shell: No, serial hardware: Yes → reboot +// Check: ls -la /dev/serial0 +// Sniff raw bytes to confirm module is transmitting: +// stty -F /dev/serial0 38400 raw && cat /dev/serial0 #include - +#include +#include #include "gps.hpp" -int main() +int main(int argc, char **argv) { - gps::Reader reader; + gps::Config cfg; + cfg.verbose = false; + bool show_all = false; - if (!reader.begin()) { - std::fprintf(stderr, "gps open failed\n" - " check /dev/serial0 (UART)\n" - " enable serial: sudo raspi-config -> Interfaces -> Serial Port\n" - " login shell = No, hardware = Yes\n"); + for (int i = 1; i < argc; ++i) { + if (std::strcmp(argv[i], "-v") == 0) { + cfg.verbose = true; + } else if (std::strcmp(argv[i], "-a") == 0) { + show_all = true; + } else if (std::strcmp(argv[i], "-b") == 0 && i + 1 < argc) { + cfg.baud = std::atoi(argv[++i]); + } + } + + std::printf("gps_test: %s @%d baud%s%s\n", + cfg.dev, cfg.baud, + cfg.verbose ? " [verbose]" : "", + show_all ? " [all sentences]" : ""); + + gps::Reader gps; + if (!gps.begin(cfg)) { + std::fprintf(stderr, "ERROR: cannot open %s\n" + " Check: UART enabled? (sudo raspi-config)\n" + " Check: ls -la /dev/serial0\n" + " Check: wiring TX=GPIO14 RX=GPIO15\n", cfg.dev); return 1; } - std::fprintf(stderr, "gps reading /dev/serial0 at 9600...\n"); + std::printf("UART open — waiting for NMEA (Ctrl+C to stop)\n\n"); - char line[128]; - gps::Fix fix; + char line[128]; + int fixes = 0; for (;;) { - if (!reader.readLine(line, sizeof(line))) { - std::fprintf(stderr, "uart read error\n"); - break; - } - if (!gps::Reader::parseRmc(line, fix)) { - // Not an RMC sentence or no fix — print raw line for debug - // std::fprintf(stderr, "raw: %s", line); + // 2s per sentence timeout — if nothing arrives, likely wiring or baud mismatch + if (!gps.readLine(line, sizeof(line), 2000)) { + std::printf(" [timeout: no NMEA for 2s — check wiring and baud rate]\n"); continue; } - if (fix.valid) { - std::fprintf(stderr, "fix lat=%+.6f lon=%+.6f spd=%.1f kn " - "time=%s date=%s\n", - fix.lat, fix.lon, fix.speed_knots, - fix.time, fix.date); - } else { - std::fprintf(stderr, "no fix\n"); + + // Checksum validation + bool cs_ok = gps::Reader::verifyChecksum(line); + if (cfg.verbose && !cs_ok) + std::printf(" [bad checksum] %s", line); + + if (show_all) { + std::printf("%s", line); + continue; + } + + gps::Fix fix; + if (gps::Reader::parseRmc(line, fix)) { + if (fix.valid) { + std::printf("[%4d] %s %s | lat=%+.6f lon=%+.6f spd=%.1fkn%s\n", + ++fixes, fix.date, fix.time, + fix.lat, fix.lon, fix.speed_knots, + cs_ok ? "" : " [bad cs]"); + } else { + std::printf(" no fix (GPRMC/GNRMC status=V)\n"); + } } } + + gps.end(); + return 0; } diff --git a/math_example/README.md b/math_example/README.md new file mode 100644 index 0000000..cd50071 --- /dev/null +++ b/math_example/README.md @@ -0,0 +1,38 @@ +# math_example + +Basic pixel → 3D vertex projection. Each pixel's brightness is used as depth in a +pinhole back-projection: + +``` +x = (u - cx) / fx * depth +y = (v - cy) / fy * depth +z = depth +``` + +Output is a triangulated OBJ mesh. Bright pixels project far from the camera, +dark pixels stay close. Gives a height-map-like 3D surface from any image. + +## Build + +```sh +make +``` + +Needs only the C++ stdlib and `stb_image` from `../shader_example/`. + +## Run + +```sh +./math # 128x128 sine-wave test pattern +./math ../shader_example/frame1.jpg # load an image +./math ../shader_example/frame1.jpg 2 # step=2 (denser mesh) +./math ../shader_example/frame1.jpg 4 90 # step=4, FOV 90° +``` + +Output: `out.obj` — open in Blender (`File → Import → Wavefront`) or MeshLab. + +## What it's for + +This is the math layer that sits between the camera and the LoRa transmitter. +Instead of sending raw pixels, you project them to vertices, run motion detection +in that 3D space, and transmit only the diff vertices over LoRa. diff --git a/shader_example/README.md b/shader_example/README.md new file mode 100644 index 0000000..4776778 --- /dev/null +++ b/shader_example/README.md @@ -0,0 +1,10 @@ +# shader_example + +Loads `frame1.jpg` and `frame2.jpg`, runs `motion.frag` (per-pixel `|A - B|`) +on the GPU, writes result to `frameO.jpg`. Desktop GL 3.3, GLFW + GLEW. + +```sh +g++ shader.cpp -o shader -lglfw -lGLEW -lGL && ./shader +``` + +`stb_image.hpp` and `stb_image_write.hpp` are vendored in this folder.