readmes and gps changes

This commit is contained in:
shinya 2026-05-21 14:11:06 +02:00
parent a5e100952c
commit b41050c2c6
7 changed files with 387 additions and 91 deletions

View File

@ -1,5 +1,22 @@
# SkyLok # 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 - https://www.youtube.com/watch?v=zFiubdrJqqI
- Client: libcamera::DirectLowResVid -> EGL::MotionShader -> MATH::Vortexes -> SPI/RadioHead::LoRa -> Server - https://github.com/ConsistentlyInconsistentYT/Pixeltovoxelprojector/
- Server: eats all incoming and cooks
## 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
```

View File

@ -1,27 +1,29 @@
# camera_example # 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 Knobs (fps, quality, AF, lens, port, resolution) are `constexpr` at the top of `camera.cpp`.
position, port, resolution).
## Build on the Pi ## Build on the Pi
```sh
apt install g++ pkg-config libcamera-dev libevent-dev libjpeg-turbo8-dev apt install g++ pkg-config libcamera-dev libevent-dev libjpeg-turbo8-dev
make 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.
## Receive ## Receive
Any TCP client that reads the framed JPEGs. Quick check: ```sh
nc <pi-ip> 5000 | ffplay -f mjpeg - nc <pi-ip> 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.

120
chip_test_example/README.md Normal file
View File

@ -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` | 0x000x07 | raw byte |

View File

@ -1,19 +1,21 @@
// gps.hpp — u-blox NMEA GPS reader over UART // gps.hpp — u-blox NMEA GPS reader over UART
// No external libs; uses POSIX termios + read(). // No external libs; uses POSIX termios + read().
// //
// Hardware pins (THE TRUTH): // Hardware (THE TRUTH):
// GPIO14/TXD0 → UBLOX_TX line (/dev/serial0) // GPIO14/TXD0 → UBLOX_TX (/dev/serial0)
// GPIO15/RXD0 → UBLOX_RX line // GPIO15/RXD0 → UBLOX_RX
// Default baud: 9600 // Default baud: 38400 (factory default on this module)
// //
// Enable UART: sudo raspi-config → Interfaces → Serial Port // Enable UART: sudo raspi-config → Interfaces → Serial Port
// "login shell over serial" = No, "serial port hardware" = Yes // "login shell over serial" = No, "serial port hardware" = Yes
// Then reboot. Check: ls -la /dev/serial0
#pragma once #pragma once
#include <cstdio> #include <chrono>
#include <cstring>
#include <cmath> #include <cmath>
#include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cstring>
#include <fcntl.h> #include <fcntl.h>
#include <termios.h> #include <termios.h>
@ -30,30 +32,57 @@ struct Fix {
char date[7]{}; // DDMMYY 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 { class Reader {
public: 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(); void end();
// Read one NMEA sentence into buf (null-terminated). Returns false on error. // Read one NMEA sentence (null-terminated, includes '\n').
bool readLine(char *buf, size_t cap); // 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 // Parse $GPRMC or $GNRMC. Returns false if wrong sentence type or fix void.
// not RMC or position is invalid.
static bool parseRmc(const char *sentence, Fix &fix); static bool parseRmc(const char *sentence, Fix &fix);
// Verify NMEA checksum: XOR of bytes between '$' and '*'.
static bool verifyChecksum(const char *sentence);
private: 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) inline bool Reader::begin(const char *dev, int baud)
{ {
fd_ = ::open(dev, O_RDWR | O_NOCTTY | O_NDELAY); fd_ = ::open(dev, O_RDWR | O_NOCTTY | O_NDELAY);
if (fd_ < 0) return false; if (fd_ < 0) {
::fcntl(fd_, F_SETFL, 0); // blocking if (verbose_)
std::fprintf(stderr, "[gps] cannot open %s: %m\n", dev);
return false;
}
::fcntl(fd_, F_SETFL, 0); // blocking reads
termios t{}; 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; speed_t spd;
switch (baud) { switch (baud) {
@ -62,14 +91,26 @@ inline bool Reader::begin(const char *dev, int baud)
case 19200: spd = B19200; break; case 19200: spd = B19200; break;
case 38400: spd = B38400; break; case 38400: spd = B38400; break;
case 57600: spd = B57600; break; case 57600: spd = B57600; break;
default: spd = B9600; 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); cfsetispeed(&t, spd);
cfsetospeed(&t, spd); cfsetospeed(&t, spd);
cfmakeraw(&t); cfmakeraw(&t);
t.c_cc[VMIN] = 1; t.c_cc[VMIN] = 1;
t.c_cc[VTIME] = 0; 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; return true;
} }
@ -78,18 +119,37 @@ inline void Reader::end()
if (fd_ >= 0) { ::close(fd_); fd_ = -1; } 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; size_t n = 0;
char c; char c;
// Wait for '$' (start of NMEA sentence)
// Wait for '$' (start of sentence).
do { do {
if (::read(fd_, &c, 1) != 1) return false; if (!timed_read(c)) return false;
} while (c != '$'); } while (c != '$');
buf[n++] = '$'; buf[n++] = '$';
while (n < cap - 1) { while (n < cap - 1) {
if (::read(fd_, &c, 1) != 1) return false; if (!timed_read(c)) return false;
buf[n++] = c; buf[n++] = c;
if (c == '\n') break; if (c == '\n') break;
} }
@ -97,23 +157,38 @@ inline bool Reader::readLine(char *buf, size_t cap)
return true; return true;
} }
// Parse $GPRMC or $GNRMC inline bool Reader::verifyChecksum(const char *sentence)
// Format: $GNRMC,HHMMSS.ss,A,DDMM.MMMM,N,DDDMM.MMMM,E,spd,crs,DDMMYY,... {
// 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) inline bool Reader::parseRmc(const char *sentence, Fix &fix)
{ {
if (!sentence) return false; if (!sentence) return false;
// Check sentence type
if (std::strncmp(sentence, "$GPRMC", 6) != 0 && if (std::strncmp(sentence, "$GPRMC", 6) != 0 &&
std::strncmp(sentence, "$GNRMC", 6) != 0) std::strncmp(sentence, "$GNRMC", 6) != 0)
return false; return false;
// Make a mutable copy // Make a mutable copy for tokenization (strip the *CS checksum tail).
char buf[128]; char buf[128];
std::strncpy(buf, sentence, sizeof(buf) - 1); std::strncpy(buf, sentence, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0'; 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]{}; const char *fields[13]{};
int nf = 0; int nf = 0;
char *p = buf; char *p = buf;
@ -126,31 +201,22 @@ inline bool Reader::parseRmc(const char *sentence, Fix &fix)
} }
if (nf < 10) return false; if (nf < 10) return false;
// field[2] = status ('A'=valid, 'V'=void)
if (fields[2][0] != 'A') { fix.valid = false; return false; } if (fields[2][0] != 'A') { fix.valid = false; return false; }
// field[1] = time
std::strncpy(fix.time, fields[1], sizeof(fix.time) - 1); 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]); double raw_lat = std::atof(fields[3]);
int lat_deg = (int)(raw_lat / 100.0); int lat_deg = (int)(raw_lat / 100.0);
double lat_min = raw_lat - lat_deg * 100.0; fix.lat = lat_deg + (raw_lat - lat_deg * 100.0) / 60.0;
fix.lat = lat_deg + lat_min / 60.0;
if (fields[4][0] == 'S') fix.lat = -fix.lat; 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]); double raw_lon = std::atof(fields[5]);
int lon_deg = (int)(raw_lon / 100.0); int lon_deg = (int)(raw_lon / 100.0);
double lon_min = raw_lon - lon_deg * 100.0; fix.lon = lon_deg + (raw_lon - lon_deg * 100.0) / 60.0;
fix.lon = lon_deg + lon_min / 60.0;
if (fields[6][0] == 'W') fix.lon = -fix.lon; if (fields[6][0] == 'W') fix.lon = -fix.lon;
fix.speed_knots = (float)std::atof(fields[7]); fix.speed_knots = (float)std::atof(fields[7]);
// field[9] = date DDMMYY
std::strncpy(fix.date, fields[9], sizeof(fix.date) - 1); std::strncpy(fix.date, fields[9], sizeof(fix.date) - 1);
fix.valid = true; fix.valid = true;
return true; return true;
} }

View File

@ -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 <cstdio> #include <cstdio>
#include <cstdlib>
#include <cstring>
#include "gps.hpp" #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()) { for (int i = 1; i < argc; ++i) {
std::fprintf(stderr, "gps open failed\n" if (std::strcmp(argv[i], "-v") == 0) {
" check /dev/serial0 (UART)\n" cfg.verbose = true;
" enable serial: sudo raspi-config -> Interfaces -> Serial Port\n" } else if (std::strcmp(argv[i], "-a") == 0) {
" login shell = No, hardware = Yes\n"); 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; 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]; char line[128];
gps::Fix fix; int fixes = 0;
for (;;) { for (;;) {
if (!reader.readLine(line, sizeof(line))) { // 2s per sentence timeout — if nothing arrives, likely wiring or baud mismatch
std::fprintf(stderr, "uart read error\n"); if (!gps.readLine(line, sizeof(line), 2000)) {
break; std::printf(" [timeout: no NMEA for 2s — check wiring and baud rate]\n");
}
if (!gps::Reader::parseRmc(line, fix)) {
// Not an RMC sentence or no fix — print raw line for debug
// std::fprintf(stderr, "raw: %s", line);
continue; continue;
} }
// 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) { if (fix.valid) {
std::fprintf(stderr, "fix lat=%+.6f lon=%+.6f spd=%.1f kn " std::printf("[%4d] %s %s | lat=%+.6f lon=%+.6f spd=%.1fkn%s\n",
"time=%s date=%s\n", ++fixes, fix.date, fix.time,
fix.lat, fix.lon, fix.speed_knots, fix.lat, fix.lon, fix.speed_knots,
fix.time, fix.date); cs_ok ? "" : " [bad cs]");
} else { } else {
std::fprintf(stderr, "no fix\n"); std::printf(" no fix (GPRMC/GNRMC status=V)\n");
} }
} }
} }
gps.end();
return 0;
}

38
math_example/README.md Normal file
View File

@ -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.

10
shader_example/README.md Normal file
View File

@ -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.