Compare commits

..

No commits in common. "a5e100952c30b2016f63e073440ccc4e40a276f9" and "addb823f0c431fba9e54085be1eba395b834260d" have entirely different histories.

8 changed files with 374 additions and 516 deletions

View File

@ -1,17 +0,0 @@
# Build on the device. Needs:
# apt install g++ pkg-config libcamera-dev libevent-dev libjpeg-turbo8-dev
# (Raspberry Pi OS Bullseye+; on older releases the turbo package is libjpeg-dev.)
CXX ?= g++
CXXFLAGS ?= -O2 -std=c++17 -pipe -Wall -Wextra
PKGS = libcamera libevent libevent_pthreads libturbojpeg
camera: camera.cpp event_loop.cpp event_loop.hpp
$(CXX) $(CXXFLAGS) camera.cpp event_loop.cpp \
$(shell pkg-config --cflags --libs $(PKGS)) -lpthread \
-o camera
clean:
rm -f camera
.PHONY: clean

View File

@ -1,27 +0,0 @@
# camera_example
libcamera viewfinder (XRGB8888) -> libjpeg-turbo -> TCP. One client at a time.
Wire format: `[uint32 BE jpeg_size][jpeg_bytes]` repeating.
Knobs are constants at the top of `camera.cpp` (fps, quality, AF mode, lens
position, port, resolution).
## 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.
## Receive
Any TCP client that reads the framed JPEGs. Quick check:
nc <pi-ip> 5000 | ffplay -f mjpeg -

View File

@ -1,277 +1,402 @@
#include <atomic>
#include <csignal>
#include <cstdint>
#include <errno.h>
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2020, Ideas on Board Oy.
*
* A simple libcamera capture example
*/
#include <iomanip>
#include <iostream>
#include <memory>
#include <thread>
#include <unordered_map>
#include <vector>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <unistd.h>
#include <turbojpeg.h>
#include <libcamera/libcamera.h>
#include <libcamera/control_ids.h>
#include <libcamera/formats.h>
#include "event_loop.hpp"
#define TIMEOUT_SEC 3
using namespace libcamera;
static std::shared_ptr<Camera> camera;
static EventLoop loop;
// --- knobs -------------------------------------------------------------------
// Goal: throughput >= latency >= quality. Tune here and rebuild.
/*
* --------------------------------------------------------------------
* Handle RequestComplete
*
* For each Camera::requestCompleted Signal emitted from the Camera the
* connected Slot is invoked.
*
* The Slot is invoked in the CameraManager's thread, hence one should avoid
* any heavy processing here. The processing of the request shall be re-directed
* to the application's thread instead, so as not to block the CameraManager's
* thread for large amount of time.
*
* The Slot receives the Request as a parameter.
*/
constexpr int kPort = 5000;
constexpr int kWidth = 640;
constexpr int kHeight = 480;
constexpr int kTargetFps = 30; // 0 = leave to camera defaults
constexpr int kBufferCount = 4; // pipeline depth
constexpr int kJpegQuality = 80;
constexpr int kJpegSubsamp = TJSAMP_420;
constexpr int kSendBufBytes = 1 << 20; // 1 MiB SO_SNDBUF (throughput)
static void processRequest(Request *request);
// Autofocus: true → continuous AF; false → fixed lens at kLensDiopters
// diopters: 0 = infinity, ~10 = closest. Silently ignored on cameras
// without AF (Pi cam v1/v2). Pi cam v3 supports it.
constexpr bool kAutoFocus = true;
constexpr float kLensDiopters = 0.0f;
// Pi viewfinder native format: XRGB8888 — DRM byte order, so memory layout
// is B,G,R,X. Feed turbojpeg TJPF_BGRX so it walks 4 B/pixel and ignores X.
// No software color conversion, no row copy.
constexpr int kTjPixelFmt = TJPF_BGRX;
// --- state -------------------------------------------------------------------
namespace {
std::shared_ptr<Camera> camera;
EventLoop loop;
std::atomic<int> client_fd{-1};
struct Mapped { uint8_t *data; size_t size; };
std::unordered_map<FrameBuffer *, Mapped> mapped;
tjhandle tj_handle = nullptr;
uint8_t *jpeg_buf = nullptr;
unsigned long jpeg_cap = 0;
// --- net ---------------------------------------------------------------------
bool sendAll(int fd, const uint8_t *p, size_t n)
static void requestComplete(Request *request)
{
while (n) {
ssize_t r = ::send(fd, p, n, MSG_NOSIGNAL);
if (r <= 0) return false;
p += r;
n -= r;
}
return true;
if (request->status() == Request::RequestCancelled)
return;
loop.callLater(std::bind(&processRequest, request));
}
void closeClient()
static void processRequest(Request *request)
{
int fd = client_fd.exchange(-1);
if (fd >= 0) ::close(fd);
std::cout << std::endl
<< "Request completed: " << request->toString() << std::endl;
/*
* When a request has completed, it is populated with a metadata control
* list that allows an application to determine various properties of
* the completed request. This can include the timestamp of the Sensor
* capture, or its gain and exposure values, or properties from the IPA
* such as the state of the 3A algorithms.
*
* ControlValue types have a toString, so to examine each request, print
* all the metadata for inspection. A custom application can parse each
* of these items and process them according to its needs.
*/
const ControlList &requestMetadata = request->metadata();
for (const auto &ctrl : requestMetadata) {
const ControlId *id = controls::controls.at(ctrl.first);
const ControlValue &value = ctrl.second;
std::cout << "\t" << id->name() << " = " << value.toString()
<< std::endl;
}
/*
* Each buffer has its own FrameMetadata to describe its state, or the
* usage of each buffer. While in our simple capture we only provide one
* buffer per request, a request can have a buffer for each stream that
* is established when configuring the camera.
*
* This allows a viewfinder and a still image to be processed at the
* same time, or to allow obtaining the RAW capture buffer from the
* sensor along with the image as processed by the ISP.
*/
const Request::BufferMap &buffers = request->buffers();
for (auto bufferPair : buffers) {
// (Unused) Stream *stream = bufferPair.first;
FrameBuffer *buffer = bufferPair.second;
const FrameMetadata &metadata = buffer->metadata();
/* Print some information about the buffer which has completed. */
std::cout << " seq: " << std::setw(6) << std::setfill('0') << metadata.sequence
<< " timestamp: " << metadata.timestamp
<< " bytesused: ";
unsigned int nplane = 0;
for (const FrameMetadata::Plane &plane : metadata.planes())
{
std::cout << plane.bytesused;
if (++nplane < metadata.planes().size())
std::cout << "/";
}
std::cout << std::endl;
/*
* Image data can be accessed here, but the FrameBuffer
* must be mapped by the application
*/
}
/* Re-queue the Request to the camera. */
request->reuse(Request::ReuseBuffers);
camera->queueRequest(request);
}
void serverThread()
{
int srv = ::socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
int yes = 1;
::setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(kPort);
addr.sin_addr.s_addr = INADDR_ANY;
if (::bind(srv, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) < 0
|| ::listen(srv, 4) < 0) {
perror("bind/listen");
::close(srv);
return;
}
std::cerr << "tcp://0.0.0.0:" << kPort << " waiting...\n";
for (;;) {
int c = ::accept(srv, nullptr, nullptr);
if (c < 0) {
if (errno == EINTR) continue;
break;
}
int one = 1, sndbuf = kSendBufBytes;
::setsockopt(c, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
::setsockopt(c, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
int old = client_fd.exchange(c);
if (old >= 0) ::close(old);
std::cerr << "client connected\n";
}
}
// --- camera ------------------------------------------------------------------
void processRequest(Request *request);
void requestComplete(Request *request)
{
if (request->status() == Request::RequestCancelled)
return;
loop.callLater([request] { processRequest(request); });
}
void processRequest(Request *request)
{
auto it = request->buffers().begin();
const Stream *stream = it->first;
FrameBuffer *buffer = it->second;
int fd = client_fd.load(std::memory_order_acquire);
if (fd >= 0 && buffer->metadata().status == FrameMetadata::FrameSuccess) {
const StreamConfiguration &cfg = stream->configuration();
const Mapped &m = mapped[buffer];
unsigned long size = jpeg_cap;
if (tjCompress2(tj_handle,
m.data, cfg.size.width, cfg.stride, cfg.size.height,
kTjPixelFmt,
&jpeg_buf, &size,
kJpegSubsamp, kJpegQuality,
TJFLAG_FASTDCT | TJFLAG_NOREALLOC) == 0) {
uint32_t len = htonl(static_cast<uint32_t>(size));
if (!sendAll(fd, reinterpret_cast<uint8_t *>(&len), sizeof(len))
|| !sendAll(fd, jpeg_buf, size)) {
std::cerr << "client gone\n";
closeClient();
}
} else {
std::cerr << "tjCompress2: " << tjGetErrorStr2(tj_handle) << "\n";
}
}
request->reuse(Request::ReuseBuffers);
camera->queueRequest(request);
}
} // namespace
// --- main --------------------------------------------------------------------
int main()
{
std::signal(SIGPIPE, SIG_IGN);
std::thread(serverThread).detach();
/*
* --------------------------------------------------------------------
* Create a Camera Manager.
*
* The Camera Manager is responsible for enumerating all the Camera
* in the system, by associating Pipeline Handlers with media entities
* registered in the system.
*
* The CameraManager provides a list of available Cameras that
* applications can operate on.
*
* When the CameraManager is no longer to be used, it should be deleted.
* We use a unique_ptr here to manage the lifetime automatically during
* the scope of this function.
*
* There can only be a single CameraManager constructed within any
* process space.
*/
std::unique_ptr<CameraManager> cm = std::make_unique<CameraManager>();
cm->start();
auto cm = std::make_unique<CameraManager>();
cm->start();
if (cm->cameras().empty()) {
std::cerr << "no cameras\n";
cm->stop();
return 1;
}
/*
* Just as a test, generate names of the Cameras registered in the
* system, and list them.
*/
for (auto const &camera : cm->cameras())
std::cout << " - " << camera.get()->id() << std::endl;
camera = cm->get(cm->cameras()[0]->id());
camera->acquire();
/*
* --------------------------------------------------------------------
* Camera
*
* Camera are entities created by pipeline handlers, inspecting the
* entities registered in the system and reported to applications
* by the CameraManager.
*
* In general terms, a Camera corresponds to a single image source
* available in the system, such as an image sensor.
*
* Application lock usage of Camera by 'acquiring' them.
* Once done with it, application shall similarly 'release' the Camera.
*
* As an example, use the first available camera in the system after
* making sure that at least one camera is available.
*
* Cameras can be obtained by their ID or their index, to demonstrate
* this, the following code gets the ID of the first camera; then gets
* the camera associated with that ID (which is of course the same as
* cm->cameras()[0]).
*/
if (cm->cameras().empty()) {
std::cout << "No cameras were identified on the system."
<< std::endl;
cm->stop();
return EXIT_FAILURE;
}
auto config = camera->generateConfiguration({ StreamRole::Viewfinder });
StreamConfiguration &sc = config->at(0);
sc.pixelFormat = formats::XRGB8888;
sc.size = { kWidth, kHeight };
sc.bufferCount = kBufferCount;
std::string cameraId = cm->cameras()[0]->id();
camera = cm->get(cameraId);
camera->acquire();
if (config->validate() == CameraConfiguration::Invalid
|| sc.pixelFormat != formats::XRGB8888) {
std::cerr << "got " << sc.pixelFormat.toString()
<< ", need XRGB8888\n";
return 1;
}
/*
* Stream
*
* Each Camera supports a variable number of Stream. A Stream is
* produced by processing data produced by an image source, usually
* by an ISP.
*
* +-------------------------------------------------------+
* | Camera |
* | +-----------+ |
* | +--------+ | |------> [ Main output ] |
* | | Image | | | |
* | | |---->| ISP |------> [ Viewfinder ] |
* | | Source | | | |
* | +--------+ | |------> [ Still Capture ] |
* | +-----------+ |
* +-------------------------------------------------------+
*
* The number and capabilities of the Stream in a Camera are
* a platform dependent property, and it's the pipeline handler
* implementation that has the responsibility of correctly
* report them.
*/
std::cerr << "stream " << sc.size.toString()
<< " " << sc.pixelFormat.toString()
<< " stride " << sc.stride
<< " buffers " << sc.bufferCount << "\n";
/*
* --------------------------------------------------------------------
* Camera Configuration.
*
* Camera configuration is tricky! It boils down to assign resources
* of the system (such as DMA engines, scalers, format converters) to
* the different image streams an application has requested.
*
* Depending on the system characteristics, some combinations of
* sizes, formats and stream usages might or might not be possible.
*
* A Camera produces a CameraConfigration based on a set of intended
* roles for each Stream the application requires.
*/
std::unique_ptr<CameraConfiguration> config =
camera->generateConfiguration( { StreamRole::Viewfinder } );
if (camera->configure(config.get()) < 0) {
std::cerr << "configure failed\n";
return 1;
}
/*
* The CameraConfiguration contains a StreamConfiguration instance
* for each StreamRole requested by the application, provided
* the Camera can support all of them.
*
* Each StreamConfiguration has default size and format, assigned
* by the Camera depending on the Role the application has requested.
*/
StreamConfiguration &streamConfig = config->at(0);
std::cout << "Default viewfinder configuration is: "
<< streamConfig.toString() << std::endl;
auto *allocator = new FrameBufferAllocator(camera);
Stream *stream = sc.stream();
if (allocator->allocate(stream) < 0) {
std::cerr << "alloc failed\n";
return 1;
}
/*
* Each StreamConfiguration parameter which is part of a
* CameraConfiguration can be independently modified by the
* application.
*
* In order to validate the modified parameter, the CameraConfiguration
* should be validated -before- the CameraConfiguration gets applied
* to the Camera.
*
* The CameraConfiguration validation process adjusts each
* StreamConfiguration to a valid value.
*/
for (const std::unique_ptr<FrameBuffer> &b : allocator->buffers(stream)) {
const FrameBuffer::Plane &p = b->planes()[0];
void *m = mmap(nullptr, p.length, PROT_READ, MAP_SHARED,
p.fd.get(), p.offset);
if (m == MAP_FAILED) { perror("mmap"); return 1; }
mapped[b.get()] = { static_cast<uint8_t *>(m), p.length };
}
/*
* Validating a CameraConfiguration -before- applying it will adjust it
* to a valid configuration which is as close as possible to the one
* requested.
*/
config->validate();
std::cout << "Validated viewfinder configuration is: "
<< streamConfig.toString() << std::endl;
tj_handle = tjInitCompress();
jpeg_cap = tjBufSize(sc.size.width, sc.size.height, kJpegSubsamp);
jpeg_buf = tjAlloc(jpeg_cap);
/*
* Once we have a validated configuration, we can apply it to the
* Camera.
*/
camera->configure(config.get());
std::vector<std::unique_ptr<Request>> requests;
for (const std::unique_ptr<FrameBuffer> &b : allocator->buffers(stream)) {
std::unique_ptr<Request> r = camera->createRequest();
if (!r || r->addBuffer(stream, b.get()) < 0) {
std::cerr << "request setup failed\n";
return 1;
}
requests.push_back(std::move(r));
}
/*
* --------------------------------------------------------------------
* Buffer Allocation
*
* Now that a camera has been configured, it knows all about its
* Streams sizes and formats. The captured images need to be stored in
* framebuffers which can either be provided by the application to the
* library, or allocated in the Camera and exposed to the application
* by libcamera.
*
* An application may decide to allocate framebuffers from elsewhere,
* for example in memory allocated by the display driver that will
* render the captured frames. The application will provide them to
* libcamera by constructing FrameBuffer instances to capture images
* directly into.
*
* Alternatively libcamera can help the application by exporting
* buffers allocated in the Camera using a FrameBufferAllocator
* instance and referencing a configured Camera to determine the
* appropriate buffer size and types to create.
*/
FrameBufferAllocator *allocator = new FrameBufferAllocator(camera);
// Initial controls: AF + frame-rate cap.
ControlList ctrls(camera->controls());
const ControlInfoMap &caps = camera->controls();
for (StreamConfiguration &cfg : *config) {
int ret = allocator->allocate(cfg.stream());
if (ret < 0) {
std::cerr << "Can't allocate buffers" << std::endl;
return EXIT_FAILURE;
}
if (caps.count(&controls::AfMode)) {
if (kAutoFocus) {
ctrls.set(controls::AfMode, controls::AfModeContinuous);
} else {
ctrls.set(controls::AfMode, controls::AfModeManual);
ctrls.set(controls::LensPosition, kLensDiopters);
}
}
if (kTargetFps > 0) {
int64_t dur = 1000000 / kTargetFps;
ctrls.set(controls::FrameDurationLimits,
Span<const int64_t, 2>({ dur, dur }));
}
size_t allocated = allocator->buffers(cfg.stream()).size();
std::cout << "Allocated " << allocated << " buffers for stream" << std::endl;
}
camera->requestCompleted.connect(requestComplete);
if (camera->start(&ctrls) < 0) {
std::cerr << "camera start failed\n";
return 1;
}
for (std::unique_ptr<Request> &r : requests)
camera->queueRequest(r.get());
/*
* --------------------------------------------------------------------
* Frame Capture
*
* libcamera frames capture model is based on the 'Request' concept.
* For each frame a Request has to be queued to the Camera.
*
* A Request refers to (at least one) Stream for which a Buffer that
* will be filled with image data shall be added to the Request.
*
* A Request is associated with a list of Controls, which are tunable
* parameters (similar to v4l2_controls) that have to be applied to
* the image.
*
* Once a request completes, all its buffers will contain image data
* that applications can access and for each of them a list of metadata
* properties that reports the capture parameters applied to the image.
*/
Stream *stream = streamConfig.stream();
const std::vector<std::unique_ptr<FrameBuffer>> &buffers = allocator->buffers(stream);
std::vector<std::unique_ptr<Request>> requests;
for (unsigned int i = 0; i < buffers.size(); ++i) {
std::unique_ptr<Request> request = camera->createRequest();
if (!request)
{
std::cerr << "Can't create request" << std::endl;
return EXIT_FAILURE;
}
std::cerr << "streaming, Ctrl+C to stop\n";
loop.exec();
const std::unique_ptr<FrameBuffer> &buffer = buffers[i];
int ret = request->addBuffer(stream, buffer.get());
if (ret < 0)
{
std::cerr << "Can't set buffer for request"
<< std::endl;
return EXIT_FAILURE;
}
camera->stop();
camera->requestCompleted.disconnect();
for (auto &kv : mapped) munmap(kv.second.data, kv.second.size);
allocator->free(stream);
delete allocator;
camera->release();
camera.reset();
cm->stop();
if (jpeg_buf) tjFree(jpeg_buf);
if (tj_handle) tjDestroy(tj_handle);
closeClient();
return 0;
/*
* Controls can be added to a request on a per frame basis.
*/
ControlList &controls = request->controls();
controls.set(controls::Brightness, 0.5);
requests.push_back(std::move(request));
}
/*
* --------------------------------------------------------------------
* Signal&Slots
*
* libcamera uses a Signal&Slot based system to connect events to
* callback operations meant to handle them, inspired by the QT graphic
* toolkit.
*
* Signals are events 'emitted' by a class instance.
* Slots are callbacks that can be 'connected' to a Signal.
*
* A Camera exposes Signals, to report the completion of a Request and
* the completion of a Buffer part of a Request to support partial
* Request completions.
*
* In order to receive the notification for request completions,
* applications shall connecte a Slot to the Camera 'requestCompleted'
* Signal before the camera is started.
*/
camera->requestCompleted.connect(requestComplete);
/*
* --------------------------------------------------------------------
* Start Capture
*
* In order to capture frames the Camera has to be started and
* Request queued to it. Enough Request to fill the Camera pipeline
* depth have to be queued before the Camera start delivering frames.
*
* For each delivered frame, the Slot connected to the
* Camera::requestCompleted Signal is called.
*/
camera->start();
for (std::unique_ptr<Request> &request : requests)
camera->queueRequest(request.get());
/*
* --------------------------------------------------------------------
* Run an EventLoop
*
* In order to dispatch events received from the video devices, such
* as buffer completions, an event loop has to be run.
*/
loop.timeout(TIMEOUT_SEC);
int ret = loop.exec();
std::cout << "Capture ran for " << TIMEOUT_SEC << " seconds and "
<< "stopped with exit status: " << ret << std::endl;
/*
* --------------------------------------------------------------------
* Clean Up
*
* Stop the Camera, release resources and stop the CameraManager.
* libcamera has now released all resources it owned.
*/
camera->stop();
allocator->free(stream);
delete allocator;
camera->release();
camera.reset();
cm->stop();
return EXIT_SUCCESS;
}

View File

@ -1,9 +0,0 @@
#!/usr/bin/env sh
# Push sources to the Pi and build there.
# usage: ./deploy.sh me@10.91.51.165
set -eu
host=${1:-me@10.91.51.165}
rsync -az --delete --exclude=camera --exclude=result \
./ "$host:~/camera_example/"
ssh "$host" 'cd ~/camera_example && make -j"$(nproc)"'
echo "built at $host:~/camera_example/camera"

View File

@ -1,158 +0,0 @@
// 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
//
// Enable UART: sudo raspi-config → Interfaces → Serial Port
// "login shell over serial" = No, "serial port hardware" = Yes
#pragma once
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
namespace gps {
struct Fix {
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
};
class Reader {
public:
bool begin(const char *dev = "/dev/serial0", int baud = 9600);
void end();
// Read one NMEA sentence into buf (null-terminated). Returns false on error.
bool readLine(char *buf, size_t cap);
// Parse $GPRMC or $GNRMC from a sentence. Returns false if sentence is
// not RMC or position is invalid.
static bool parseRmc(const char *sentence, Fix &fix);
private:
int fd_ = -1;
};
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
termios t{};
if (tcgetattr(fd_, &t) < 0) 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;
}
cfsetispeed(&t, spd);
cfsetospeed(&t, spd);
cfmakeraw(&t);
t.c_cc[VMIN] = 1;
t.c_cc[VTIME] = 0;
tcsetattr(fd_, TCSANOW, &t);
return true;
}
inline void Reader::end()
{
if (fd_ >= 0) { ::close(fd_); fd_ = -1; }
}
inline bool Reader::readLine(char *buf, size_t cap)
{
size_t n = 0;
char c;
// Wait for '$' (start of NMEA sentence)
do {
if (::read(fd_, &c, 1) != 1) return false;
} while (c != '$');
buf[n++] = '$';
while (n < cap - 1) {
if (::read(fd_, &c, 1) != 1) return false;
buf[n++] = c;
if (c == '\n') break;
}
buf[n] = '\0';
return true;
}
// Parse $GPRMC or $GNRMC
// Format: $GNRMC,HHMMSS.ss,A,DDMM.MMMM,N,DDDMM.MMMM,E,spd,crs,DDMMYY,...
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
char buf[128];
std::strncpy(buf, sentence, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
// Tokenize on commas
const char *fields[13]{};
int nf = 0;
char *p = buf;
while (*p && nf < 13) {
fields[nf++] = p;
char *next = std::strchr(p, ',');
if (!next) break;
*next = '\0';
p = next + 1;
}
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;
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;
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;
}
} // namespace gps

View File

@ -1,39 +0,0 @@
#include <cstdio>
#include "gps.hpp"
int main()
{
gps::Reader reader;
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");
return 1;
}
std::fprintf(stderr, "gps reading /dev/serial0 at 9600...\n");
char line[128];
gps::Fix fix;
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);
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");
}
}
}

View File

@ -1,4 +1,5 @@
#version 330 core
precision mediump float;
in vec2 uv;

View File

@ -24,14 +24,6 @@ GLuint compile(GLenum t, const std::string& src)
const char* c = src.c_str();
glShaderSource(s, 1, &c, NULL);
glCompileShader(s);
GLint ok = 0;
glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
if (!ok) {
char log[1024];
glGetShaderInfoLog(s, sizeof(log), NULL, log);
std::cout << "shader compile failed:\n" << log;
}
return s;
}
@ -45,9 +37,9 @@ int main()
glewInit();
// ---------------- LOAD IMAGES ----------------
int w,h;
unsigned char* img1 = stbi_load("frame1.jpg",&w,&h,NULL,4);
unsigned char* img2 = stbi_load("frame2.jpg",&w,&h,NULL,4);
int w,h,c;
unsigned char* img1 = stbi_load("frame1.jpg",&w,&h,&c,4);
unsigned char* img2 = stbi_load("frame2.jpg",&w,&h,&c,4);
if(!img1 || !img2)
{
@ -56,9 +48,9 @@ int main()
}
// ---------------- TEXTURES ----------------
GLuint tex[2];
glGenTextures(2, tex);
GLuint texA = tex[0], texB = tex[1];
GLuint texA, texB;
glGenTextures(1,&texA);
glGenTextures(1,&texB);
auto upload = [&](GLuint t, unsigned char* d)
{
@ -94,14 +86,6 @@ int main()
glAttachShader(prog,f);
glLinkProgram(prog);
GLint linked = 0;
glGetProgramiv(prog, GL_LINK_STATUS, &linked);
if (!linked) {
char log[1024];
glGetProgramInfoLog(prog, sizeof(log), NULL, log);
std::cout << "program link failed:\n" << log;
}
// ---------------- QUAD (FIXED) ----------------
GLuint vao,vbo;
@ -158,8 +142,6 @@ int main()
glReadPixels(0,0,w,h,GL_RGBA,GL_UNSIGNED_BYTE,out.data());
stbi_write_jpg("frameO.jpg",w,h,4,out.data(),90);
stbi_image_free(img1);
stbi_image_free(img2);
std::cout << "done\n";
}