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
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
```

View File

@ -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
```sh
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.
```
## Receive
Any TCP client that reads the framed JPEGs. Quick check:
```sh
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
// 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 <cstdio>
#include <cstring>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fcntl.h>
#include <termios.h>
@ -30,30 +32,57 @@ struct Fix {
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;
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) {
@ -62,14 +91,26 @@ inline bool Reader::begin(const char *dev, int baud)
case 19200: spd = B19200; break;
case 38400: spd = B38400; 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);
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;
}

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 <cstdlib>
#include <cstring>
#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;
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;
}
// 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::fprintf(stderr, "fix lat=%+.6f lon=%+.6f spd=%.1f kn "
"time=%s date=%s\n",
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,
fix.time, fix.date);
cs_ok ? "" : " [bad cs]");
} 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.