Compare commits
4 Commits
addb823f0c
...
a5e100952c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5e100952c | ||
|
|
d7ad154d20 | ||
|
|
ea7f7ec704 | ||
|
|
b26a6ea7dd |
17
camera_example/Makefile
Normal file
17
camera_example/Makefile
Normal file
@ -0,0 +1,17 @@
|
||||
# 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
|
||||
27
camera_example/README.md
Normal file
27
camera_example/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# 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 -
|
||||
@ -1,402 +1,277 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
/*
|
||||
* Copyright (C) 2020, Ideas on Board Oy.
|
||||
*
|
||||
* A simple libcamera capture example
|
||||
*/
|
||||
|
||||
#include <iomanip>
|
||||
#include <atomic>
|
||||
#include <csignal>
|
||||
#include <cstdint>
|
||||
#include <errno.h>
|
||||
#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;
|
||||
|
||||
/*
|
||||
* --------------------------------------------------------------------
|
||||
* 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.
|
||||
*/
|
||||
// --- knobs -------------------------------------------------------------------
|
||||
// Goal: throughput >= latency >= quality. Tune here and rebuild.
|
||||
|
||||
static void processRequest(Request *request);
|
||||
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 requestComplete(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)
|
||||
{
|
||||
while (n) {
|
||||
ssize_t r = ::send(fd, p, n, MSG_NOSIGNAL);
|
||||
if (r <= 0) return false;
|
||||
p += r;
|
||||
n -= r;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void closeClient()
|
||||
{
|
||||
int fd = client_fd.exchange(-1);
|
||||
if (fd >= 0) ::close(fd);
|
||||
}
|
||||
|
||||
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(std::bind(&processRequest, request));
|
||||
loop.callLater([request] { processRequest(request); });
|
||||
}
|
||||
|
||||
static void processRequest(Request *request)
|
||||
void processRequest(Request *request)
|
||||
{
|
||||
std::cout << std::endl
|
||||
<< "Request completed: " << request->toString() << std::endl;
|
||||
auto it = request->buffers().begin();
|
||||
const Stream *stream = it->first;
|
||||
FrameBuffer *buffer = it->second;
|
||||
|
||||
/*
|
||||
* 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;
|
||||
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];
|
||||
|
||||
std::cout << "\t" << id->name() << " = " << value.toString()
|
||||
<< std::endl;
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// --- main --------------------------------------------------------------------
|
||||
|
||||
int main()
|
||||
{
|
||||
/*
|
||||
* --------------------------------------------------------------------
|
||||
* 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>();
|
||||
std::signal(SIGPIPE, SIG_IGN);
|
||||
std::thread(serverThread).detach();
|
||||
|
||||
auto cm = std::make_unique<CameraManager>();
|
||||
cm->start();
|
||||
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* 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;
|
||||
std::cerr << "no cameras\n";
|
||||
cm->stop();
|
||||
return EXIT_FAILURE;
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string cameraId = cm->cameras()[0]->id();
|
||||
camera = cm->get(cameraId);
|
||||
camera = cm->get(cm->cameras()[0]->id());
|
||||
camera->acquire();
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
auto config = camera->generateConfiguration({ StreamRole::Viewfinder });
|
||||
StreamConfiguration &sc = config->at(0);
|
||||
sc.pixelFormat = formats::XRGB8888;
|
||||
sc.size = { kWidth, kHeight };
|
||||
sc.bufferCount = kBufferCount;
|
||||
|
||||
/*
|
||||
* --------------------------------------------------------------------
|
||||
* 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 } );
|
||||
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/*
|
||||
* Once we have a validated configuration, we can apply it to the
|
||||
* Camera.
|
||||
*/
|
||||
camera->configure(config.get());
|
||||
|
||||
/*
|
||||
* --------------------------------------------------------------------
|
||||
* 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);
|
||||
|
||||
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 (config->validate() == CameraConfiguration::Invalid
|
||||
|| sc.pixelFormat != formats::XRGB8888) {
|
||||
std::cerr << "got " << sc.pixelFormat.toString()
|
||||
<< ", need XRGB8888\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t allocated = allocator->buffers(cfg.stream()).size();
|
||||
std::cout << "Allocated " << allocated << " buffers for stream" << std::endl;
|
||||
std::cerr << "stream " << sc.size.toString()
|
||||
<< " " << sc.pixelFormat.toString()
|
||||
<< " stride " << sc.stride
|
||||
<< " buffers " << sc.bufferCount << "\n";
|
||||
|
||||
if (camera->configure(config.get()) < 0) {
|
||||
std::cerr << "configure failed\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* --------------------------------------------------------------------
|
||||
* 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);
|
||||
auto *allocator = new FrameBufferAllocator(camera);
|
||||
Stream *stream = sc.stream();
|
||||
if (allocator->allocate(stream) < 0) {
|
||||
std::cerr << "alloc failed\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
tj_handle = tjInitCompress();
|
||||
jpeg_cap = tjBufSize(sc.size.width, sc.size.height, kJpegSubsamp);
|
||||
jpeg_buf = tjAlloc(jpeg_cap);
|
||||
|
||||
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;
|
||||
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));
|
||||
}
|
||||
|
||||
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;
|
||||
// Initial controls: AF + frame-rate cap.
|
||||
ControlList ctrls(camera->controls());
|
||||
const ControlInfoMap &caps = camera->controls();
|
||||
|
||||
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 }));
|
||||
}
|
||||
|
||||
/*
|
||||
* 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);
|
||||
if (camera->start(&ctrls) < 0) {
|
||||
std::cerr << "camera start failed\n";
|
||||
return 1;
|
||||
}
|
||||
for (std::unique_ptr<Request> &r : requests)
|
||||
camera->queueRequest(r.get());
|
||||
|
||||
/*
|
||||
* --------------------------------------------------------------------
|
||||
* 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());
|
||||
std::cerr << "streaming, Ctrl+C to stop\n";
|
||||
loop.exec();
|
||||
|
||||
/*
|
||||
* --------------------------------------------------------------------
|
||||
* 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();
|
||||
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();
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
if (jpeg_buf) tjFree(jpeg_buf);
|
||||
if (tj_handle) tjDestroy(tj_handle);
|
||||
closeClient();
|
||||
return 0;
|
||||
}
|
||||
|
||||
9
camera_example/deploy.sh
Executable file
9
camera_example/deploy.sh
Executable file
@ -0,0 +1,9 @@
|
||||
#!/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"
|
||||
158
chip_test_example/gps.hpp
Normal file
158
chip_test_example/gps.hpp
Normal file
@ -0,0 +1,158 @@
|
||||
// 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
|
||||
39
chip_test_example/gps_test.cpp
Normal file
39
chip_test_example/gps_test.cpp
Normal file
@ -0,0 +1,39 @@
|
||||
#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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
#version 330 core
|
||||
precision mediump float;
|
||||
|
||||
in vec2 uv;
|
||||
|
||||
|
||||
@ -24,6 +24,14 @@ 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;
|
||||
}
|
||||
|
||||
@ -37,9 +45,9 @@ int main()
|
||||
glewInit();
|
||||
|
||||
// ---------------- LOAD IMAGES ----------------
|
||||
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);
|
||||
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);
|
||||
|
||||
if(!img1 || !img2)
|
||||
{
|
||||
@ -48,9 +56,9 @@ int main()
|
||||
}
|
||||
|
||||
// ---------------- TEXTURES ----------------
|
||||
GLuint texA, texB;
|
||||
glGenTextures(1,&texA);
|
||||
glGenTextures(1,&texB);
|
||||
GLuint tex[2];
|
||||
glGenTextures(2, tex);
|
||||
GLuint texA = tex[0], texB = tex[1];
|
||||
|
||||
auto upload = [&](GLuint t, unsigned char* d)
|
||||
{
|
||||
@ -86,6 +94,14 @@ 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;
|
||||
|
||||
@ -142,6 +158,8 @@ 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";
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user