mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -07:00
forgectrl: implement the camera MJPEG service
Persistent imx-media capture engine (lid/head via the shared video-mux, 409 arbitration, 10 s idle teardown), 1296x972 MJPEG stream via 2x2 Bayer superpixel, full-resolution bilinear snapshots, mjpg-streamer-compatible aliases, sysvinit script; installed in both images. Bench-verified: 3.2 fps stream, clamped 0 during simultaneous motion.
This commit is contained in:
@@ -6,14 +6,26 @@ LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda
|
||||
|
||||
SRC_URI = "\
|
||||
file://CMakeLists.txt \
|
||||
file://hello.c \
|
||||
file://main.c \
|
||||
file://cam.c \
|
||||
file://cam.h \
|
||||
file://debayer.c \
|
||||
file://debayer.h \
|
||||
file://forgectrl.init \
|
||||
"
|
||||
|
||||
S = "${WORKDIR}"
|
||||
|
||||
inherit cmake
|
||||
inherit cmake update-rc.d
|
||||
|
||||
DEPENDS += "libconfig ulfius"
|
||||
RDEPENDS:${PN} = "libconfig ulfius"
|
||||
DEPENDS += "ulfius jpeg"
|
||||
# media-ctl / v4l2-ctl configure the imx-media pipeline at runtime
|
||||
RDEPENDS:${PN} = "v4l-utils"
|
||||
|
||||
EXTRA_OECMAKE = ""
|
||||
INITSCRIPT_NAME = "forgectrl"
|
||||
INITSCRIPT_PARAMS = "defaults 90"
|
||||
|
||||
do_install:append() {
|
||||
install -d ${D}${sysconfdir}/init.d
|
||||
install -m 0755 ${WORKDIR}/forgectrl.init ${D}${sysconfdir}/init.d/forgectrl
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ cmake_minimum_required(VERSION 3.10)
|
||||
project(forgectrl C)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
file(GLOB sources "${PROJECT_SOURCE_DIR}/*.c")
|
||||
|
||||
add_executable(forgectrl ${sources})
|
||||
add_executable(forgectrl main.c cam.c debayer.c)
|
||||
target_compile_options(forgectrl PRIVATE -Wall -Wextra -O2)
|
||||
target_link_libraries(forgectrl ulfius jpeg pthread m)
|
||||
|
||||
install(TARGETS forgectrl RUNTIME DESTINATION bin)
|
||||
|
||||
@@ -0,0 +1,847 @@
|
||||
/*
|
||||
* cam.c - persistent Glowforge camera capture engine
|
||||
* Copyright (c) 2026 Scott Wiederhold <s.e.wiederhold@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Pipeline model (mainline imx-media): both OV5648 sensors feed one
|
||||
* video-mux -> MIPI CSI-2 -> IPU CSI path to the 'ipu1_csi0 capture' video
|
||||
* node. Camera selection is which mux sink link is enabled; sensor controls
|
||||
* live on the sensor subdev; illumination is the per-camera LED driven via
|
||||
* sysfs. Links and pad formats are configured with media-ctl / v4l2-ctl
|
||||
* (the same sequences python3-gfhardware uses for one-shot grabs), then the
|
||||
* capture node is held open and streamed continuously.
|
||||
*
|
||||
* Threading: a control mutex serializes engine start/stop/switch; the
|
||||
* engine lock covers frame data and counters. The worker thread only ever
|
||||
* takes the engine lock, so control paths may join it while holding the
|
||||
* control mutex.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include "cam.h"
|
||||
#include "debayer.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <linux/videodev2.h>
|
||||
#include <pthread.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <jpeglib.h> /* requires stdio.h first (FILE) */
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define CAM_W 2592
|
||||
#define CAM_H 1944
|
||||
#define HALF_W (CAM_W / 2)
|
||||
#define HALF_H (CAM_H / 2)
|
||||
#define HFLIP 1 /* factory image orientation (see debayer.h) */
|
||||
|
||||
#define N_BUFS 4
|
||||
#define DQ_TIMEOUT_S 2 /* select() timeout per frame; also the idle tick */
|
||||
#define MAX_DQ_TIMEOUTS 3 /* consecutive timeouts -> engine gives up */
|
||||
#define IDLE_STOP_S 10 /* no clients/snapshots for this long -> teardown */
|
||||
#define SNAP_TIMEOUT_S 15
|
||||
#define CLIENT_WAIT_S 5
|
||||
|
||||
#define CAPTURE_ENTITY "ipu1_csi0 capture"
|
||||
#define MBUS_FMT "SBGGR8_1X8/2592x1944 field:none"
|
||||
|
||||
struct camdef {
|
||||
const char *name;
|
||||
int bus; /* I2C bus: sensor entity resolved by <bus>-0036 */
|
||||
int muxpad; /* video-mux sink pad */
|
||||
const char *lamp; /* sysfs illumination attribute */
|
||||
int exposure; /* 1/16-line units, frame-length ceiling ~31600 */
|
||||
int gain;
|
||||
};
|
||||
|
||||
static const struct camdef camdefs[2] = {
|
||||
[CAM_LID] = { "lid", 0, 0, "/sys/glowforge/pic/lid_led", 24000, 50 },
|
||||
[CAM_HEAD] = { "head", 3, 1, "/sys/glowforge/head/white_led", 24000, 200 },
|
||||
};
|
||||
|
||||
struct buffer {
|
||||
void *start;
|
||||
size_t length;
|
||||
};
|
||||
|
||||
static struct {
|
||||
/* control path (start/stop/switch) - taken before lock, never by the
|
||||
* worker thread */
|
||||
pthread_mutex_t ctl;
|
||||
|
||||
/* engine state */
|
||||
pthread_mutex_t lock;
|
||||
pthread_cond_t frame_cv; /* new stream frame published */
|
||||
pthread_cond_t snap_cv; /* snapshot request completed */
|
||||
pthread_t tid;
|
||||
int tid_valid;
|
||||
int running; /* worker alive and capturing */
|
||||
int stop_flag;
|
||||
cam_id_t cam;
|
||||
int clients;
|
||||
struct timespec last_activity;
|
||||
|
||||
/* published stream frame (half-res JPEG) */
|
||||
uint8_t *stream_jpg;
|
||||
size_t stream_len;
|
||||
size_t stream_cap;
|
||||
uint64_t seq;
|
||||
double fps;
|
||||
|
||||
/* one pending snapshot request at a time (control mutex serializes) */
|
||||
int snap_pending; /* 1 = requested, 2 = done, 3 = failed */
|
||||
int snap_full;
|
||||
int snap_quality;
|
||||
uint8_t *snap_jpg; /* malloc'd result, taken by requester */
|
||||
size_t snap_len;
|
||||
|
||||
/* capture resources (worker/start/teardown only) */
|
||||
int fd;
|
||||
struct buffer bufs[N_BUFS];
|
||||
int n_bufs;
|
||||
int streaming;
|
||||
int lamp_prev; /* -1 = unknown, restore to 0 */
|
||||
|
||||
/* config */
|
||||
int stream_quality;
|
||||
int lamp_level;
|
||||
} eng = {
|
||||
.ctl = PTHREAD_MUTEX_INITIALIZER,
|
||||
.lock = PTHREAD_MUTEX_INITIALIZER,
|
||||
.frame_cv = PTHREAD_COND_INITIALIZER,
|
||||
.snap_cv = PTHREAD_COND_INITIALIZER,
|
||||
.fd = -1,
|
||||
.lamp_prev = -1,
|
||||
.stream_quality = 75,
|
||||
.lamp_level = 132,
|
||||
};
|
||||
|
||||
const char *cam_name(cam_id_t cam)
|
||||
{
|
||||
return camdefs[cam].name;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ util */
|
||||
|
||||
static void now_ts(struct timespec *ts)
|
||||
{
|
||||
clock_gettime(CLOCK_MONOTONIC, ts);
|
||||
}
|
||||
|
||||
static double ts_diff(const struct timespec *a, const struct timespec *b)
|
||||
{
|
||||
return (double)(a->tv_sec - b->tv_sec) +
|
||||
(double)(a->tv_nsec - b->tv_nsec) / 1e9;
|
||||
}
|
||||
|
||||
static int xioctl(int fd, unsigned long req, void *arg)
|
||||
{
|
||||
int r;
|
||||
do {
|
||||
r = ioctl(fd, req, arg);
|
||||
} while (r == -1 && errno == EINTR);
|
||||
return r;
|
||||
}
|
||||
|
||||
/* Run a shell command, logging and returning nonzero on failure. */
|
||||
static int run(const char *fmt, ...)
|
||||
{
|
||||
char cmd[512];
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(cmd, sizeof(cmd), fmt, ap);
|
||||
va_end(ap);
|
||||
int rc = system(cmd);
|
||||
if (rc == -1 || !WIFEXITED(rc) || WEXITSTATUS(rc) != 0) {
|
||||
fprintf(stderr, "cam: command failed (%d): %s\n", rc, cmd);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Run a command and capture its first line of output. */
|
||||
static int run_read(char *out, size_t outlen, const char *fmt, ...)
|
||||
{
|
||||
char cmd[512];
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(cmd, sizeof(cmd), fmt, ap);
|
||||
va_end(ap);
|
||||
FILE *p = popen(cmd, "r");
|
||||
if (!p)
|
||||
return -1;
|
||||
out[0] = '\0';
|
||||
if (!fgets(out, (int)outlen, p)) {
|
||||
pclose(p);
|
||||
return -1;
|
||||
}
|
||||
int rc = pclose(p);
|
||||
out[strcspn(out, "\r\n")] = '\0';
|
||||
if (rc == -1 || !WIFEXITED(rc) || WEXITSTATUS(rc) != 0 || !out[0]) {
|
||||
fprintf(stderr, "cam: command failed: %s\n", cmd);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int sysfs_read_int(const char *path, int *val)
|
||||
{
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f)
|
||||
return -1;
|
||||
int ok = fscanf(f, "%d", val) == 1;
|
||||
fclose(f);
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
static int sysfs_write_int(const char *path, int val)
|
||||
{
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f)
|
||||
return -1;
|
||||
int ok = fprintf(f, "%d", val) > 0;
|
||||
fclose(f);
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------- pipeline configure */
|
||||
|
||||
/* Resolve the sensor media entity on an I2C bus (e.g. "ov5648 0-0036") by
|
||||
* its address suffix, so OV5648 and OV8856 (HD model) both match. */
|
||||
static int sensor_entity(int bus, char *out, size_t outlen)
|
||||
{
|
||||
char needle[16];
|
||||
snprintf(needle, sizeof(needle), " %d-0036", bus);
|
||||
|
||||
FILE *p = popen("media-ctl -p", "r");
|
||||
if (!p)
|
||||
return -1;
|
||||
char line[512];
|
||||
int found = 0;
|
||||
while (fgets(line, sizeof(line), p)) {
|
||||
char *e = strstr(line, "entity ");
|
||||
if (!e)
|
||||
continue;
|
||||
char *colon = strchr(e, ':');
|
||||
char *hit = strstr(line, needle);
|
||||
if (!colon || !hit || hit < colon)
|
||||
continue;
|
||||
char *start = colon + 2;
|
||||
char *end = hit + strlen(needle);
|
||||
if (end <= start || (size_t)(end - start) >= outlen)
|
||||
continue;
|
||||
memcpy(out, start, (size_t)(end - start));
|
||||
out[end - start] = '\0';
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
pclose(p);
|
||||
return found ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Route the selected sensor through the video-mux to the capture node and
|
||||
* set the raw-Bayer format on every pad of the active path. Exactly one
|
||||
* mux sink link may be enabled, so the other camera's link (if that sensor
|
||||
* exists) is disabled first. */
|
||||
static int configure_pipeline(cam_id_t cam, const char *sensor,
|
||||
const char *other_sensor)
|
||||
{
|
||||
const struct camdef *c = &camdefs[cam];
|
||||
const struct camdef *o = &camdefs[cam == CAM_LID ? CAM_HEAD : CAM_LID];
|
||||
|
||||
if (other_sensor[0] &&
|
||||
run("media-ctl -l '\"%s\":0 -> \"video-mux\":%d [0]'",
|
||||
other_sensor, o->muxpad))
|
||||
return -1;
|
||||
|
||||
if (run("media-ctl -l '\"%s\":0 -> \"video-mux\":%d [1]'",
|
||||
sensor, c->muxpad) ||
|
||||
run("media-ctl -l '\"video-mux\":2 -> \"imx6-mipi-csi2\":0 [1]'") ||
|
||||
run("media-ctl -l '\"imx6-mipi-csi2\":1 -> \"ipu1_csi0_mux\":0 [1]'") ||
|
||||
run("media-ctl -l '\"ipu1_csi0_mux\":5 -> \"ipu1_csi0\":0 [1]'") ||
|
||||
run("media-ctl -l '\"ipu1_csi0\":2 -> \"" CAPTURE_ENTITY "\":0 [1]'"))
|
||||
return -1;
|
||||
|
||||
if (run("media-ctl -V '\"%s\":0 [fmt:" MBUS_FMT "]'", sensor) ||
|
||||
run("media-ctl -V '\"video-mux\":%d [fmt:" MBUS_FMT "]'", c->muxpad) ||
|
||||
run("media-ctl -V '\"video-mux\":2 [fmt:" MBUS_FMT "]'") ||
|
||||
run("media-ctl -V '\"imx6-mipi-csi2\":0 [fmt:" MBUS_FMT "]'") ||
|
||||
run("media-ctl -V '\"imx6-mipi-csi2\":1 [fmt:" MBUS_FMT "]'") ||
|
||||
run("media-ctl -V '\"ipu1_csi0_mux\":0 [fmt:" MBUS_FMT "]'") ||
|
||||
run("media-ctl -V '\"ipu1_csi0_mux\":5 [fmt:" MBUS_FMT "]'") ||
|
||||
run("media-ctl -V '\"ipu1_csi0\":0 [fmt:" MBUS_FMT "]'") ||
|
||||
run("media-ctl -V '\"ipu1_csi0\":2 [fmt:" MBUS_FMT "]'"))
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Manual exposure/gain/white-balance on the sensor subdev (factory values).
|
||||
* The auto-clusters must go manual before the manual values take effect.
|
||||
* The sensor flips stay off: HFLIP breaks imx-media CSI capture, so the
|
||||
* factory mirror is applied in software (debayer). */
|
||||
static int configure_sensor(const char *sensor, const struct camdef *c)
|
||||
{
|
||||
char subdev[64];
|
||||
if (run_read(subdev, sizeof(subdev), "media-ctl -e '%s'", sensor))
|
||||
return -1;
|
||||
if (run("v4l2-ctl -d %s -c auto_exposure=1 -c gain_automatic=0"
|
||||
" -c white_balance_automatic=0", subdev))
|
||||
return -1;
|
||||
if (run("v4l2-ctl -d %s -c exposure=%d -c gain=%d -c red_balance=1100"
|
||||
" -c blue_balance=1400 -c horizontal_flip=0 -c vertical_flip=0",
|
||||
subdev, c->exposure, c->gain))
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------- jpeg encoding */
|
||||
|
||||
static int jpeg_encode_rgb(const uint8_t *rgb, int w, int h, int quality,
|
||||
int fast, uint8_t **out, size_t *outlen)
|
||||
{
|
||||
struct jpeg_compress_struct ci;
|
||||
struct jpeg_error_mgr jerr;
|
||||
unsigned char *buf = NULL;
|
||||
unsigned long buflen = 0;
|
||||
|
||||
ci.err = jpeg_std_error(&jerr);
|
||||
jpeg_create_compress(&ci);
|
||||
jpeg_mem_dest(&ci, &buf, &buflen);
|
||||
ci.image_width = (JDIMENSION)w;
|
||||
ci.image_height = (JDIMENSION)h;
|
||||
ci.input_components = 3;
|
||||
ci.in_color_space = JCS_RGB;
|
||||
jpeg_set_defaults(&ci);
|
||||
jpeg_set_quality(&ci, quality, TRUE);
|
||||
if (fast)
|
||||
ci.dct_method = JDCT_FASTEST;
|
||||
jpeg_start_compress(&ci, TRUE);
|
||||
while (ci.next_scanline < ci.image_height) {
|
||||
JSAMPROW row = (JSAMPROW)(rgb + (long)ci.next_scanline * w * 3);
|
||||
jpeg_write_scanlines(&ci, &row, 1);
|
||||
}
|
||||
jpeg_finish_compress(&ci);
|
||||
jpeg_destroy_compress(&ci);
|
||||
*out = buf;
|
||||
*outlen = (size_t)buflen;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------- capture start/stop */
|
||||
|
||||
/* Release every capture resource and restore the lamp. Safe to call from
|
||||
* any state; called by the worker on exit and by a failed start. */
|
||||
static void release_capture(void)
|
||||
{
|
||||
if (eng.streaming) {
|
||||
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
xioctl(eng.fd, VIDIOC_STREAMOFF, &type);
|
||||
eng.streaming = 0;
|
||||
}
|
||||
for (int i = 0; i < eng.n_bufs; i++) {
|
||||
if (eng.bufs[i].start) {
|
||||
munmap(eng.bufs[i].start, eng.bufs[i].length);
|
||||
eng.bufs[i].start = NULL;
|
||||
}
|
||||
}
|
||||
eng.n_bufs = 0;
|
||||
if (eng.fd >= 0) {
|
||||
close(eng.fd);
|
||||
eng.fd = -1;
|
||||
}
|
||||
if (eng.lamp_prev >= 0) {
|
||||
sysfs_write_int(camdefs[eng.cam].lamp, eng.lamp_prev);
|
||||
eng.lamp_prev = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Configure the media graph and sensor, light the lamp, and bring up the
|
||||
* V4L2 capture node streaming. Called with ctl held, engine not running. */
|
||||
static int start_capture(cam_id_t cam, char *err, size_t errlen)
|
||||
{
|
||||
const struct camdef *c = &camdefs[cam];
|
||||
char sensor[64], other[64] = "";
|
||||
|
||||
if (sensor_entity(c->bus, sensor, sizeof(sensor))) {
|
||||
snprintf(err, errlen, "no camera sensor on i2c-%d", c->bus);
|
||||
return -1;
|
||||
}
|
||||
(void)sensor_entity(camdefs[cam == CAM_LID ? CAM_HEAD : CAM_LID].bus,
|
||||
other, sizeof(other));
|
||||
|
||||
if (configure_pipeline(cam, sensor, other)) {
|
||||
snprintf(err, errlen, "media pipeline configuration failed");
|
||||
return -1;
|
||||
}
|
||||
if (configure_sensor(sensor, c)) {
|
||||
snprintf(err, errlen, "sensor configuration failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
char dev[64];
|
||||
if (run_read(dev, sizeof(dev), "media-ctl -e '" CAPTURE_ENTITY "'")) {
|
||||
snprintf(err, errlen, "cannot resolve capture video node");
|
||||
return -1;
|
||||
}
|
||||
|
||||
eng.cam = cam;
|
||||
|
||||
/* Scene lighting for the duration; the previous level is restored at
|
||||
* teardown (raw register write - instant, no fade). */
|
||||
if (sysfs_read_int(c->lamp, &eng.lamp_prev))
|
||||
eng.lamp_prev = 0;
|
||||
sysfs_write_int(c->lamp, eng.lamp_level);
|
||||
|
||||
eng.fd = open(dev, O_RDWR | O_NONBLOCK, 0);
|
||||
if (eng.fd < 0) {
|
||||
snprintf(err, errlen, "open %s: %s", dev, strerror(errno));
|
||||
release_capture();
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct v4l2_format fmt = {0};
|
||||
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
fmt.fmt.pix.width = CAM_W;
|
||||
fmt.fmt.pix.height = CAM_H;
|
||||
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_SBGGR8;
|
||||
fmt.fmt.pix.field = V4L2_FIELD_NONE;
|
||||
if (xioctl(eng.fd, VIDIOC_S_FMT, &fmt) < 0) {
|
||||
snprintf(err, errlen, "S_FMT: %s", strerror(errno));
|
||||
release_capture();
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct v4l2_requestbuffers req = {0};
|
||||
req.count = N_BUFS;
|
||||
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
req.memory = V4L2_MEMORY_MMAP;
|
||||
if (xioctl(eng.fd, VIDIOC_REQBUFS, &req) < 0 || req.count < 2) {
|
||||
snprintf(err, errlen, "REQBUFS: %s (device busy?)", strerror(errno));
|
||||
release_capture();
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (eng.n_bufs = 0; eng.n_bufs < (int)req.count; eng.n_bufs++) {
|
||||
struct v4l2_buffer buf = {0};
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
buf.index = (unsigned)eng.n_bufs;
|
||||
if (xioctl(eng.fd, VIDIOC_QUERYBUF, &buf) < 0) {
|
||||
snprintf(err, errlen, "QUERYBUF: %s", strerror(errno));
|
||||
release_capture();
|
||||
return -1;
|
||||
}
|
||||
eng.bufs[eng.n_bufs].length = buf.length;
|
||||
eng.bufs[eng.n_bufs].start = mmap(NULL, buf.length,
|
||||
PROT_READ | PROT_WRITE, MAP_SHARED,
|
||||
eng.fd, buf.m.offset);
|
||||
if (eng.bufs[eng.n_bufs].start == MAP_FAILED) {
|
||||
eng.bufs[eng.n_bufs].start = NULL;
|
||||
snprintf(err, errlen, "mmap: %s", strerror(errno));
|
||||
release_capture();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < eng.n_bufs; i++) {
|
||||
struct v4l2_buffer buf = {0};
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
buf.index = (unsigned)i;
|
||||
if (xioctl(eng.fd, VIDIOC_QBUF, &buf) < 0) {
|
||||
snprintf(err, errlen, "QBUF: %s", strerror(errno));
|
||||
release_capture();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
if (xioctl(eng.fd, VIDIOC_STREAMON, &type) < 0) {
|
||||
snprintf(err, errlen, "STREAMON: %s", strerror(errno));
|
||||
release_capture();
|
||||
return -1;
|
||||
}
|
||||
eng.streaming = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------- worker loop */
|
||||
|
||||
static void *worker(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
uint8_t *rgb_half = malloc((size_t)HALF_W * HALF_H * 3);
|
||||
uint8_t *rgb_full = NULL; /* allocated on first full-res snapshot */
|
||||
int dq_timeouts = 0;
|
||||
struct timespec fps_t0;
|
||||
now_ts(&fps_t0);
|
||||
uint64_t fps_frames = 0;
|
||||
|
||||
if (!rgb_half) {
|
||||
fprintf(stderr, "cam: worker OOM\n");
|
||||
goto out;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
struct timespec now;
|
||||
now_ts(&now);
|
||||
pthread_mutex_lock(&eng.lock);
|
||||
int stop = eng.stop_flag;
|
||||
int clients = eng.clients;
|
||||
int snap = eng.snap_pending == 1;
|
||||
int idle = clients == 0 && !snap &&
|
||||
ts_diff(&now, &eng.last_activity) > IDLE_STOP_S;
|
||||
pthread_mutex_unlock(&eng.lock);
|
||||
|
||||
if (stop || idle)
|
||||
break;
|
||||
|
||||
/* Wait for a frame */
|
||||
fd_set fds;
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(eng.fd, &fds);
|
||||
struct timeval tv = { .tv_sec = DQ_TIMEOUT_S };
|
||||
int r = select(eng.fd + 1, &fds, NULL, NULL, &tv);
|
||||
if (r == -1 && errno == EINTR)
|
||||
continue;
|
||||
if (r <= 0) {
|
||||
if (r == 0 && ++dq_timeouts >= MAX_DQ_TIMEOUTS) {
|
||||
fprintf(stderr, "cam: %d consecutive frame timeouts, "
|
||||
"stopping engine\n", dq_timeouts);
|
||||
break;
|
||||
}
|
||||
if (r == -1) {
|
||||
fprintf(stderr, "cam: select: %s\n", strerror(errno));
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
struct v4l2_buffer buf = {0};
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
if (xioctl(eng.fd, VIDIOC_DQBUF, &buf) < 0) {
|
||||
if (errno == EAGAIN || errno == EIO)
|
||||
continue;
|
||||
fprintf(stderr, "cam: DQBUF: %s\n", strerror(errno));
|
||||
break;
|
||||
}
|
||||
dq_timeouts = 0;
|
||||
const uint8_t *raw = eng.bufs[buf.index].start;
|
||||
|
||||
/* Snapshot request rides on the same raw frame */
|
||||
if (snap) {
|
||||
uint8_t *jpg = NULL;
|
||||
size_t len = 0;
|
||||
int ok;
|
||||
pthread_mutex_lock(&eng.lock);
|
||||
int full = eng.snap_full;
|
||||
int q = eng.snap_quality;
|
||||
pthread_mutex_unlock(&eng.lock);
|
||||
if (full) {
|
||||
if (!rgb_full)
|
||||
rgb_full = malloc((size_t)CAM_W * CAM_H * 3);
|
||||
ok = rgb_full != NULL;
|
||||
if (ok) {
|
||||
debayer_bggr_bilinear(raw, rgb_full, CAM_W, CAM_H, HFLIP);
|
||||
ok = jpeg_encode_rgb(rgb_full, CAM_W, CAM_H, q, 0,
|
||||
&jpg, &len) == 0;
|
||||
}
|
||||
} else {
|
||||
debayer_bggr_half(raw, rgb_half, CAM_W, CAM_H, HFLIP);
|
||||
ok = jpeg_encode_rgb(rgb_half, HALF_W, HALF_H, q, 0,
|
||||
&jpg, &len) == 0;
|
||||
}
|
||||
pthread_mutex_lock(&eng.lock);
|
||||
free(eng.snap_jpg);
|
||||
eng.snap_jpg = ok ? jpg : NULL;
|
||||
eng.snap_len = ok ? len : 0;
|
||||
eng.snap_pending = ok ? 2 : 3;
|
||||
now_ts(&eng.last_activity);
|
||||
pthread_cond_broadcast(&eng.snap_cv);
|
||||
pthread_mutex_unlock(&eng.lock);
|
||||
}
|
||||
|
||||
/* Stream frame */
|
||||
if (clients > 0) {
|
||||
uint8_t *jpg = NULL;
|
||||
size_t len = 0;
|
||||
debayer_bggr_half(raw, rgb_half, CAM_W, CAM_H, HFLIP);
|
||||
if (jpeg_encode_rgb(rgb_half, HALF_W, HALF_H, eng.stream_quality,
|
||||
1, &jpg, &len) == 0) {
|
||||
pthread_mutex_lock(&eng.lock);
|
||||
free(eng.stream_jpg);
|
||||
eng.stream_jpg = jpg;
|
||||
eng.stream_len = len;
|
||||
eng.seq++;
|
||||
fps_frames++;
|
||||
struct timespec t;
|
||||
now_ts(&t);
|
||||
double dt = ts_diff(&t, &fps_t0);
|
||||
if (dt >= 2.0) {
|
||||
eng.fps = (double)fps_frames / dt;
|
||||
fps_frames = 0;
|
||||
fps_t0 = t;
|
||||
}
|
||||
pthread_cond_broadcast(&eng.frame_cv);
|
||||
pthread_mutex_unlock(&eng.lock);
|
||||
}
|
||||
}
|
||||
|
||||
if (xioctl(eng.fd, VIDIOC_QBUF, &buf) < 0) {
|
||||
fprintf(stderr, "cam: QBUF: %s\n", strerror(errno));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
out:
|
||||
release_capture();
|
||||
free(rgb_half);
|
||||
free(rgb_full);
|
||||
pthread_mutex_lock(&eng.lock);
|
||||
eng.running = 0;
|
||||
/* fail any waiter: stream clients see running==0, a pending snapshot
|
||||
* is marked failed */
|
||||
if (eng.snap_pending == 1)
|
||||
eng.snap_pending = 3;
|
||||
pthread_cond_broadcast(&eng.frame_cv);
|
||||
pthread_cond_broadcast(&eng.snap_cv);
|
||||
pthread_mutex_unlock(&eng.lock);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------- engine control */
|
||||
|
||||
/* With ctl held: make the engine run on `cam`. Fails if clients hold the
|
||||
* other camera. */
|
||||
static int ensure_engine(cam_id_t cam, char *err, size_t errlen)
|
||||
{
|
||||
for (;;) {
|
||||
pthread_mutex_lock(&eng.lock);
|
||||
int running = eng.running;
|
||||
cam_id_t cur = eng.cam;
|
||||
int clients = eng.clients;
|
||||
int tid_valid = eng.tid_valid;
|
||||
pthread_mutex_unlock(&eng.lock);
|
||||
|
||||
if (running && cur == cam)
|
||||
return 0;
|
||||
|
||||
if (running && cur != cam) {
|
||||
if (clients > 0) {
|
||||
snprintf(err, errlen,
|
||||
"camera busy: %d client(s) streaming %s",
|
||||
clients, camdefs[cur].name);
|
||||
return -1;
|
||||
}
|
||||
pthread_mutex_lock(&eng.lock);
|
||||
eng.stop_flag = 1;
|
||||
pthread_mutex_unlock(&eng.lock);
|
||||
/* worker notices at the next tick (<= DQ_TIMEOUT_S) */
|
||||
}
|
||||
|
||||
if (tid_valid) {
|
||||
pthread_join(eng.tid, NULL);
|
||||
pthread_mutex_lock(&eng.lock);
|
||||
eng.tid_valid = 0;
|
||||
eng.stop_flag = 0;
|
||||
pthread_mutex_unlock(&eng.lock);
|
||||
continue; /* re-evaluate from a clean state */
|
||||
}
|
||||
|
||||
/* cold start */
|
||||
if (start_capture(cam, err, errlen))
|
||||
return -1;
|
||||
pthread_mutex_lock(&eng.lock);
|
||||
eng.running = 1;
|
||||
eng.stop_flag = 0;
|
||||
eng.seq = 0;
|
||||
eng.fps = 0;
|
||||
now_ts(&eng.last_activity);
|
||||
if (pthread_create(&eng.tid, NULL, worker, NULL)) {
|
||||
eng.running = 0;
|
||||
pthread_mutex_unlock(&eng.lock);
|
||||
release_capture();
|
||||
snprintf(err, errlen, "worker thread creation failed");
|
||||
return -1;
|
||||
}
|
||||
eng.tid_valid = 1;
|
||||
pthread_mutex_unlock(&eng.lock);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void cam_engine_init(void)
|
||||
{
|
||||
const char *v;
|
||||
if ((v = getenv("FORGECTRL_STREAM_Q")) != NULL) {
|
||||
int q = atoi(v);
|
||||
if (q >= 1 && q <= 100)
|
||||
eng.stream_quality = q;
|
||||
}
|
||||
if ((v = getenv("FORGECTRL_LAMP")) != NULL) {
|
||||
int l = atoi(v);
|
||||
if (l >= 0 && l <= 1023)
|
||||
eng.lamp_level = l;
|
||||
}
|
||||
}
|
||||
|
||||
void cam_engine_shutdown(void)
|
||||
{
|
||||
pthread_mutex_lock(&eng.ctl);
|
||||
pthread_mutex_lock(&eng.lock);
|
||||
int tid_valid = eng.tid_valid;
|
||||
eng.stop_flag = 1;
|
||||
pthread_mutex_unlock(&eng.lock);
|
||||
if (tid_valid) {
|
||||
pthread_join(eng.tid, NULL);
|
||||
pthread_mutex_lock(&eng.lock);
|
||||
eng.tid_valid = 0;
|
||||
pthread_mutex_unlock(&eng.lock);
|
||||
}
|
||||
pthread_mutex_unlock(&eng.ctl);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ snapshots */
|
||||
|
||||
int cam_snapshot(cam_id_t cam, int full, int quality,
|
||||
uint8_t **jpeg, size_t *len, char *err, size_t errlen)
|
||||
{
|
||||
pthread_mutex_lock(&eng.ctl);
|
||||
if (ensure_engine(cam, err, errlen)) {
|
||||
pthread_mutex_unlock(&eng.ctl);
|
||||
return -1;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&eng.lock);
|
||||
eng.snap_pending = 1;
|
||||
eng.snap_full = full;
|
||||
eng.snap_quality = quality;
|
||||
now_ts(&eng.last_activity);
|
||||
|
||||
struct timespec deadline;
|
||||
clock_gettime(CLOCK_REALTIME, &deadline);
|
||||
deadline.tv_sec += SNAP_TIMEOUT_S;
|
||||
int rc = 0;
|
||||
while (eng.snap_pending == 1) {
|
||||
if (pthread_cond_timedwait(&eng.snap_cv, &eng.lock, &deadline)
|
||||
== ETIMEDOUT) {
|
||||
rc = ETIMEDOUT;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (rc == 0 && eng.snap_pending == 2) {
|
||||
*jpeg = eng.snap_jpg;
|
||||
*len = eng.snap_len;
|
||||
eng.snap_jpg = NULL;
|
||||
eng.snap_len = 0;
|
||||
eng.snap_pending = 0;
|
||||
} else {
|
||||
if (eng.snap_pending == 1 || eng.snap_pending == 3)
|
||||
snprintf(err, errlen, rc == ETIMEDOUT ?
|
||||
"snapshot timed out" : "snapshot capture failed");
|
||||
eng.snap_pending = 0;
|
||||
rc = -1;
|
||||
}
|
||||
now_ts(&eng.last_activity);
|
||||
pthread_mutex_unlock(&eng.lock);
|
||||
pthread_mutex_unlock(&eng.ctl);
|
||||
return rc == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------- stream client */
|
||||
|
||||
struct cam_client {
|
||||
uint64_t last_seq;
|
||||
uint8_t *buf;
|
||||
size_t cap;
|
||||
};
|
||||
|
||||
cam_client_t *cam_client_open(cam_id_t cam, char *err, size_t errlen)
|
||||
{
|
||||
pthread_mutex_lock(&eng.ctl);
|
||||
if (ensure_engine(cam, err, errlen)) {
|
||||
pthread_mutex_unlock(&eng.ctl);
|
||||
return NULL;
|
||||
}
|
||||
cam_client_t *c = calloc(1, sizeof(*c));
|
||||
if (!c) {
|
||||
pthread_mutex_unlock(&eng.ctl);
|
||||
snprintf(err, errlen, "out of memory");
|
||||
return NULL;
|
||||
}
|
||||
pthread_mutex_lock(&eng.lock);
|
||||
eng.clients++;
|
||||
now_ts(&eng.last_activity);
|
||||
pthread_mutex_unlock(&eng.lock);
|
||||
pthread_mutex_unlock(&eng.ctl);
|
||||
return c;
|
||||
}
|
||||
|
||||
long cam_client_next(cam_client_t *c, const uint8_t **jpeg)
|
||||
{
|
||||
pthread_mutex_lock(&eng.lock);
|
||||
int timeouts = 0;
|
||||
while (eng.running && eng.seq <= c->last_seq) {
|
||||
struct timespec deadline;
|
||||
clock_gettime(CLOCK_REALTIME, &deadline);
|
||||
deadline.tv_sec += CLIENT_WAIT_S;
|
||||
if (pthread_cond_timedwait(&eng.frame_cv, &eng.lock, &deadline)
|
||||
== ETIMEDOUT && ++timeouts >= 2)
|
||||
break;
|
||||
}
|
||||
if (!eng.running || eng.seq <= c->last_seq) {
|
||||
pthread_mutex_unlock(&eng.lock);
|
||||
return -1;
|
||||
}
|
||||
if (c->cap < eng.stream_len) {
|
||||
uint8_t *nb = realloc(c->buf, eng.stream_len);
|
||||
if (!nb) {
|
||||
pthread_mutex_unlock(&eng.lock);
|
||||
return -1;
|
||||
}
|
||||
c->buf = nb;
|
||||
c->cap = eng.stream_len;
|
||||
}
|
||||
memcpy(c->buf, eng.stream_jpg, eng.stream_len);
|
||||
long len = (long)eng.stream_len;
|
||||
c->last_seq = eng.seq;
|
||||
now_ts(&eng.last_activity);
|
||||
pthread_mutex_unlock(&eng.lock);
|
||||
*jpeg = c->buf;
|
||||
return len;
|
||||
}
|
||||
|
||||
void cam_client_close(cam_client_t *c)
|
||||
{
|
||||
if (!c)
|
||||
return;
|
||||
pthread_mutex_lock(&eng.lock);
|
||||
if (eng.clients > 0)
|
||||
eng.clients--;
|
||||
now_ts(&eng.last_activity);
|
||||
pthread_mutex_unlock(&eng.lock);
|
||||
free(c->buf);
|
||||
free(c);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- status */
|
||||
|
||||
void cam_get_status(struct cam_status *st)
|
||||
{
|
||||
pthread_mutex_lock(&eng.lock);
|
||||
st->running = eng.running;
|
||||
st->cam = eng.cam;
|
||||
st->clients = eng.clients;
|
||||
st->seq = eng.seq;
|
||||
st->fps = eng.fps;
|
||||
pthread_mutex_unlock(&eng.lock);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* cam.h - persistent Glowforge camera capture engine
|
||||
* Copyright (c) 2026 Scott Wiederhold <s.e.wiederhold@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* One worker thread owns the imx-media pipeline and V4L2 capture node for
|
||||
* the selected camera (lid or head - they share the video-mux, so exactly
|
||||
* one can stream at a time). The engine starts on demand, publishes the
|
||||
* latest half-resolution JPEG for stream clients, serves full-resolution
|
||||
* snapshot requests from the same raw frames, and tears the pipeline down
|
||||
* after an idle period so one-shot users (gfhardware) can still grab.
|
||||
*/
|
||||
#ifndef FORGECTRL_CAM_H
|
||||
#define FORGECTRL_CAM_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef enum {
|
||||
CAM_LID = 0,
|
||||
CAM_HEAD = 1,
|
||||
} cam_id_t;
|
||||
|
||||
/* Read once at startup (env overrides): stream JPEG quality and lamp level. */
|
||||
void cam_engine_init(void);
|
||||
|
||||
/* Stop the engine (if running) and release everything. */
|
||||
void cam_engine_shutdown(void);
|
||||
|
||||
/* Blocking snapshot from the live engine. full=1 -> 2592x1944 bilinear,
|
||||
* full=0 -> 1296x972. quality 1..100. On success *jpeg is malloc'd (caller
|
||||
* frees). Returns 0, or -1 with a message in err (engine busy on the other
|
||||
* camera, pipeline failure, timeout). */
|
||||
int cam_snapshot(cam_id_t cam, int full, int quality,
|
||||
uint8_t **jpeg, size_t *len, char *err, size_t errlen);
|
||||
|
||||
/* Stream client: open pins the engine to a camera (starting or switching it
|
||||
* if needed), next blocks for a frame newer than the last one returned and
|
||||
* copies it into a client-owned buffer, close releases the pin. */
|
||||
typedef struct cam_client cam_client_t;
|
||||
|
||||
cam_client_t *cam_client_open(cam_id_t cam, char *err, size_t errlen);
|
||||
/* Returns frame length (>0), or -1 when the engine stopped / timed out and
|
||||
* the stream should end. The returned pointer stays valid until the next
|
||||
* cam_client_next() or cam_client_close(). */
|
||||
long cam_client_next(cam_client_t *c, const uint8_t **jpeg);
|
||||
void cam_client_close(cam_client_t *c);
|
||||
|
||||
/* Status snapshot for /cam/status. */
|
||||
struct cam_status {
|
||||
int running;
|
||||
cam_id_t cam;
|
||||
int clients;
|
||||
uint64_t seq;
|
||||
double fps;
|
||||
};
|
||||
void cam_get_status(struct cam_status *st);
|
||||
|
||||
const char *cam_name(cam_id_t cam);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* debayer.c - BGGR raw-Bayer to RGB conversion for the Glowforge cameras
|
||||
* Copyright (c) 2026 Scott Wiederhold <s.e.wiederhold@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* BGGR tile layout (row 0 topmost):
|
||||
* even rows: B G B G ...
|
||||
* odd rows: G R G R ...
|
||||
*/
|
||||
#include "debayer.h"
|
||||
|
||||
static inline int clampi(int v, int lo, int hi)
|
||||
{
|
||||
return v < lo ? lo : (v > hi ? hi : v);
|
||||
}
|
||||
|
||||
void debayer_bggr_bilinear(const uint8_t *raw, uint8_t *rgb,
|
||||
int w, int h, int hflip)
|
||||
{
|
||||
/* Border pixels use clamped neighbor coordinates; the interior uses the
|
||||
* same expressions with the clamps folding to identity. Per-pixel clamp
|
||||
* cost is acceptable: full resolution is only used for snapshots. */
|
||||
for (int y = 0; y < h; y++) {
|
||||
const int yn = clampi(y - 1, 0, h - 1) * w;
|
||||
const int yc = y * w;
|
||||
const int ys = clampi(y + 1, 0, h - 1) * w;
|
||||
uint8_t *out_row = rgb + (long)y * w * 3;
|
||||
for (int x = 0; x < w; x++) {
|
||||
const int xw = clampi(x - 1, 0, w - 1);
|
||||
const int xe = clampi(x + 1, 0, w - 1);
|
||||
unsigned r, g, b;
|
||||
if ((y & 1) == 0) {
|
||||
if ((x & 1) == 0) { /* B site */
|
||||
b = raw[yc + x];
|
||||
g = (raw[yc + xw] + raw[yc + xe] +
|
||||
raw[yn + x] + raw[ys + x] + 2) >> 2;
|
||||
r = (raw[yn + xw] + raw[yn + xe] +
|
||||
raw[ys + xw] + raw[ys + xe] + 2) >> 2;
|
||||
} else { /* G site on a B row */
|
||||
g = raw[yc + x];
|
||||
b = (raw[yc + xw] + raw[yc + xe] + 1) >> 1;
|
||||
r = (raw[yn + x] + raw[ys + x] + 1) >> 1;
|
||||
}
|
||||
} else {
|
||||
if ((x & 1) == 0) { /* G site on an R row */
|
||||
g = raw[yc + x];
|
||||
r = (raw[yc + xw] + raw[yc + xe] + 1) >> 1;
|
||||
b = (raw[yn + x] + raw[ys + x] + 1) >> 1;
|
||||
} else { /* R site */
|
||||
r = raw[yc + x];
|
||||
g = (raw[yc + xw] + raw[yc + xe] +
|
||||
raw[yn + x] + raw[ys + x] + 2) >> 2;
|
||||
b = (raw[yn + xw] + raw[yn + xe] +
|
||||
raw[ys + xw] + raw[ys + xe] + 2) >> 2;
|
||||
}
|
||||
}
|
||||
uint8_t *px = out_row + (long)(hflip ? w - 1 - x : x) * 3;
|
||||
px[0] = (uint8_t)r;
|
||||
px[1] = (uint8_t)g;
|
||||
px[2] = (uint8_t)b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void debayer_bggr_half(const uint8_t *raw, uint8_t *rgb,
|
||||
int w, int h, int hflip)
|
||||
{
|
||||
const int ow = w / 2;
|
||||
const int oh = h / 2;
|
||||
for (int y = 0; y < oh; y++) {
|
||||
const uint8_t *row_b = raw + (long)(2 * y) * w; /* B G ... */
|
||||
const uint8_t *row_r = row_b + w; /* G R ... */
|
||||
uint8_t *out_row = rgb + (long)y * ow * 3;
|
||||
for (int x = 0; x < ow; x++) {
|
||||
const int xi = 2 * x;
|
||||
uint8_t *px = out_row + (long)(hflip ? ow - 1 - x : x) * 3;
|
||||
px[0] = row_r[xi + 1]; /* R */
|
||||
px[1] = (uint8_t)((row_b[xi + 1] + row_r[xi] + 1) >> 1); /* G */
|
||||
px[2] = row_b[xi]; /* B */
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* debayer.h - BGGR raw-Bayer to RGB conversion for the Glowforge cameras
|
||||
* Copyright (c) 2026 Scott Wiederhold <s.e.wiederhold@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#ifndef FORGECTRL_DEBAYER_H
|
||||
#define FORGECTRL_DEBAYER_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Full-resolution bilinear demosaic of a BGGR frame. rgb must hold w*h*3
|
||||
* bytes. hflip mirrors the output horizontally (the factory image
|
||||
* orientation: the sensor HFLIP register breaks imx-media CSI capture, so
|
||||
* the mirror is applied in software). */
|
||||
void debayer_bggr_bilinear(const uint8_t *raw, uint8_t *rgb,
|
||||
int w, int h, int hflip);
|
||||
|
||||
/* Half-resolution demosaic: each 2x2 BGGR quad becomes one RGB pixel
|
||||
* (greens averaged) - no interpolation. Output is (w/2)x(h/2); rgb must
|
||||
* hold (w/2)*(h/2)*3 bytes. */
|
||||
void debayer_bggr_half(const uint8_t *raw, uint8_t *rgb,
|
||||
int w, int h, int hflip);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/bin/sh
|
||||
### BEGIN INIT INFO
|
||||
# Provides: forgectrl
|
||||
# Required-Start: $network
|
||||
# Required-Stop:
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: ForgeFIRM control daemon (camera MJPEG service)
|
||||
### END INIT INFO
|
||||
|
||||
DAEMON=/usr/bin/forgectrl
|
||||
PIDFILE=/var/run/forgectrl.pid
|
||||
LOG=/data/forgectrl.log
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
echo "Starting forgectrl"
|
||||
start-stop-daemon -S -q -p $PIDFILE -m -b -x /bin/sh -- \
|
||||
-c "exec $DAEMON >> $LOG 2>&1"
|
||||
;;
|
||||
stop)
|
||||
echo "Stopping forgectrl"
|
||||
start-stop-daemon -K -q -p $PIDFILE
|
||||
rm -f $PIDFILE
|
||||
;;
|
||||
restart)
|
||||
$0 stop
|
||||
sleep 1
|
||||
$0 start
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
@@ -1,6 +0,0 @@
|
||||
#include <stdio.h>
|
||||
int main() {
|
||||
// printf() displays the string inside quotation
|
||||
printf("Hello, World!");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
* main.c - forgectrl: ForgeFIRM system control daemon
|
||||
* Copyright (c) 2026 Scott Wiederhold <s.e.wiederhold@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* HTTP service (ulfius) exposing the Glowforge cameras as MJPEG:
|
||||
*
|
||||
* GET / index page with a live view
|
||||
* GET /?action=stream mjpg-streamer-compatible stream (lid)
|
||||
* GET /?action=snapshot mjpg-streamer-compatible snapshot (lid)
|
||||
* GET /cam/stream?cam=lid|head multipart MJPEG, 1296x972
|
||||
* GET /cam/snapshot?cam=&res=full|half&q= single JPEG (default full res)
|
||||
* GET /cam/status JSON engine status
|
||||
*
|
||||
* The two cameras share the hardware mux, so streaming clients pin the
|
||||
* selection; requests for the other camera return 409 until they leave.
|
||||
* Environment: FORGECTRL_PORT (8080), FORGECTRL_STREAM_Q (75),
|
||||
* FORGECTRL_LAMP (132).
|
||||
*
|
||||
* ulfius runs libmicrohttpd in thread-per-connection mode, so each stream
|
||||
* callback may block waiting for the next frame.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include "cam.h"
|
||||
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ulfius.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define DEFAULT_PORT 8080
|
||||
#define BOUNDARY "forgectrl-frame"
|
||||
#define SNAP_Q_DEF 75
|
||||
|
||||
static volatile sig_atomic_t quit = 0;
|
||||
|
||||
static void on_signal(int sig)
|
||||
{
|
||||
(void)sig;
|
||||
quit = 1;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------- helpers */
|
||||
|
||||
static cam_id_t parse_cam(const struct _u_request *req, int *ok)
|
||||
{
|
||||
const char *v = u_map_get(req->map_url, "cam");
|
||||
*ok = 1;
|
||||
if (!v || !strcmp(v, "lid"))
|
||||
return CAM_LID;
|
||||
if (!strcmp(v, "head"))
|
||||
return CAM_HEAD;
|
||||
*ok = 0;
|
||||
return CAM_LID;
|
||||
}
|
||||
|
||||
static int reply_error(struct _u_response *res, unsigned status,
|
||||
const char *msg)
|
||||
{
|
||||
ulfius_set_string_body_response(res, status, msg);
|
||||
ulfius_add_header_to_response(res, "Content-Type", "text/plain");
|
||||
return U_CALLBACK_CONTINUE;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ streaming */
|
||||
|
||||
struct stream_ctx {
|
||||
cam_client_t *cl;
|
||||
uint8_t *chunk; /* current multipart chunk being drained */
|
||||
size_t chunk_cap;
|
||||
size_t chunk_len;
|
||||
size_t off;
|
||||
};
|
||||
|
||||
static ssize_t stream_cb(void *cls, uint64_t pos, char *buf, size_t max)
|
||||
{
|
||||
(void)pos;
|
||||
struct stream_ctx *sc = cls;
|
||||
|
||||
if (sc->off >= sc->chunk_len) {
|
||||
const uint8_t *jpg;
|
||||
long len = cam_client_next(sc->cl, &jpg);
|
||||
if (len < 0)
|
||||
return U_STREAM_END;
|
||||
|
||||
char head[128];
|
||||
int headlen = snprintf(head, sizeof(head),
|
||||
"--" BOUNDARY "\r\n"
|
||||
"Content-Type: image/jpeg\r\n"
|
||||
"Content-Length: %ld\r\n\r\n", len);
|
||||
size_t need = (size_t)headlen + (size_t)len + 2;
|
||||
if (sc->chunk_cap < need) {
|
||||
uint8_t *nb = realloc(sc->chunk, need);
|
||||
if (!nb)
|
||||
return U_STREAM_ERROR;
|
||||
sc->chunk = nb;
|
||||
sc->chunk_cap = need;
|
||||
}
|
||||
memcpy(sc->chunk, head, (size_t)headlen);
|
||||
memcpy(sc->chunk + headlen, jpg, (size_t)len);
|
||||
memcpy(sc->chunk + headlen + len, "\r\n", 2);
|
||||
sc->chunk_len = need;
|
||||
sc->off = 0;
|
||||
}
|
||||
|
||||
size_t n = sc->chunk_len - sc->off;
|
||||
if (n > max)
|
||||
n = max;
|
||||
memcpy(buf, sc->chunk + sc->off, n);
|
||||
sc->off += n;
|
||||
return (ssize_t)n;
|
||||
}
|
||||
|
||||
static void stream_free_cb(void *cls)
|
||||
{
|
||||
struct stream_ctx *sc = cls;
|
||||
cam_client_close(sc->cl);
|
||||
free(sc->chunk);
|
||||
free(sc);
|
||||
}
|
||||
|
||||
static int do_stream(cam_id_t cam, struct _u_response *res)
|
||||
{
|
||||
char err[256];
|
||||
cam_client_t *cl = cam_client_open(cam, err, sizeof(err));
|
||||
if (!cl)
|
||||
return reply_error(res, strstr(err, "busy") ? 409 : 503, err);
|
||||
|
||||
struct stream_ctx *sc = calloc(1, sizeof(*sc));
|
||||
if (!sc) {
|
||||
cam_client_close(cl);
|
||||
return reply_error(res, 500, "out of memory");
|
||||
}
|
||||
sc->cl = cl;
|
||||
|
||||
ulfius_add_header_to_response(res, "Content-Type",
|
||||
"multipart/x-mixed-replace; boundary=" BOUNDARY);
|
||||
ulfius_add_header_to_response(res, "Cache-Control", "no-store");
|
||||
ulfius_set_stream_response(res, 200, stream_cb, stream_free_cb,
|
||||
U_STREAM_SIZE_UNKNOWN, 64 * 1024, sc);
|
||||
return U_CALLBACK_CONTINUE;
|
||||
}
|
||||
|
||||
static int do_snapshot(cam_id_t cam, int full, int quality,
|
||||
struct _u_response *res)
|
||||
{
|
||||
uint8_t *jpg = NULL;
|
||||
size_t len = 0;
|
||||
char err[256];
|
||||
if (cam_snapshot(cam, full, quality, &jpg, &len, err, sizeof(err)))
|
||||
return reply_error(res, strstr(err, "busy") ? 409 : 503, err);
|
||||
ulfius_set_binary_body_response(res, 200, (const char *)jpg, len);
|
||||
ulfius_add_header_to_response(res, "Content-Type", "image/jpeg");
|
||||
ulfius_add_header_to_response(res, "Cache-Control", "no-store");
|
||||
free(jpg);
|
||||
return U_CALLBACK_CONTINUE;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ callbacks */
|
||||
|
||||
static int cb_stream(const struct _u_request *req, struct _u_response *res,
|
||||
void *user_data)
|
||||
{
|
||||
(void)user_data;
|
||||
int ok;
|
||||
cam_id_t cam = parse_cam(req, &ok);
|
||||
if (!ok)
|
||||
return reply_error(res, 400, "cam must be 'lid' or 'head'");
|
||||
return do_stream(cam, res);
|
||||
}
|
||||
|
||||
static int cb_snapshot(const struct _u_request *req, struct _u_response *res,
|
||||
void *user_data)
|
||||
{
|
||||
(void)user_data;
|
||||
int ok;
|
||||
cam_id_t cam = parse_cam(req, &ok);
|
||||
if (!ok)
|
||||
return reply_error(res, 400, "cam must be 'lid' or 'head'");
|
||||
|
||||
int full = 1;
|
||||
const char *v = u_map_get(req->map_url, "res");
|
||||
if (v) {
|
||||
if (!strcmp(v, "half"))
|
||||
full = 0;
|
||||
else if (strcmp(v, "full"))
|
||||
return reply_error(res, 400, "res must be 'full' or 'half'");
|
||||
}
|
||||
int quality = SNAP_Q_DEF;
|
||||
if ((v = u_map_get(req->map_url, "q")) != NULL) {
|
||||
quality = atoi(v);
|
||||
if (quality < 1 || quality > 100)
|
||||
return reply_error(res, 400, "q must be 1..100");
|
||||
}
|
||||
return do_snapshot(cam, full, quality, res);
|
||||
}
|
||||
|
||||
static int cb_status(const struct _u_request *req, struct _u_response *res,
|
||||
void *user_data)
|
||||
{
|
||||
(void)req;
|
||||
(void)user_data;
|
||||
struct cam_status st;
|
||||
cam_get_status(&st);
|
||||
char body[256];
|
||||
snprintf(body, sizeof(body),
|
||||
"{\"running\":%s,\"cam\":\"%s\",\"clients\":%d,"
|
||||
"\"frames\":%llu,\"fps\":%.1f,"
|
||||
"\"stream\":{\"width\":1296,\"height\":972},"
|
||||
"\"snapshot\":{\"width\":2592,\"height\":1944}}",
|
||||
st.running ? "true" : "false", cam_name(st.cam), st.clients,
|
||||
(unsigned long long)st.seq, st.fps);
|
||||
ulfius_set_string_body_response(res, 200, body);
|
||||
ulfius_add_header_to_response(res, "Content-Type", "application/json");
|
||||
return U_CALLBACK_CONTINUE;
|
||||
}
|
||||
|
||||
static const char index_html[] =
|
||||
"<!DOCTYPE html><html><head><title>ForgeFIRM camera</title>"
|
||||
"<style>body{font-family:sans-serif;background:#111;color:#ddd;"
|
||||
"text-align:center}img{max-width:95%;border:1px solid #444}"
|
||||
"a{color:#8cf}</style></head><body>"
|
||||
"<h2>ForgeFIRM camera</h2>"
|
||||
"<p><a href=\"/cam/stream?cam=lid\">lid stream</a> | "
|
||||
"<a href=\"/cam/snapshot?cam=lid\">lid snapshot</a> | "
|
||||
"<a href=\"/cam/stream?cam=head\">head stream</a> | "
|
||||
"<a href=\"/cam/snapshot?cam=head\">head snapshot</a> | "
|
||||
"<a href=\"/cam/status\">status</a></p>"
|
||||
"<img src=\"/cam/stream?cam=lid\" alt=\"lid camera stream\">"
|
||||
"</body></html>";
|
||||
|
||||
/* "/" serves the index, plus the mjpg-streamer-compatible
|
||||
* ?action=stream / ?action=snapshot aliases many clients expect. */
|
||||
static int cb_root(const struct _u_request *req, struct _u_response *res,
|
||||
void *user_data)
|
||||
{
|
||||
(void)user_data;
|
||||
const char *action = u_map_get(req->map_url, "action");
|
||||
if (action) {
|
||||
if (!strcmp(action, "stream"))
|
||||
return do_stream(CAM_LID, res);
|
||||
if (!strcmp(action, "snapshot"))
|
||||
return do_snapshot(CAM_LID, 1, SNAP_Q_DEF, res);
|
||||
return reply_error(res, 400, "unknown action");
|
||||
}
|
||||
ulfius_set_string_body_response(res, 200, index_html);
|
||||
ulfius_add_header_to_response(res, "Content-Type", "text/html");
|
||||
return U_CALLBACK_CONTINUE;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ main */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
unsigned port = DEFAULT_PORT;
|
||||
const char *v = getenv("FORGECTRL_PORT");
|
||||
if (v && atoi(v) > 0 && atoi(v) < 65536)
|
||||
port = (unsigned)atoi(v);
|
||||
|
||||
/* Stay well below the motion feeder (SCHED_FIFO) and the controller;
|
||||
* best effort. */
|
||||
(void)nice(5);
|
||||
|
||||
cam_engine_init();
|
||||
|
||||
struct _u_instance inst;
|
||||
if (ulfius_init_instance(&inst, port, NULL, NULL) != U_OK) {
|
||||
fprintf(stderr, "forgectrl: ulfius init failed\n");
|
||||
return 1;
|
||||
}
|
||||
ulfius_add_endpoint_by_val(&inst, "GET", "/", NULL, 0, &cb_root, NULL);
|
||||
ulfius_add_endpoint_by_val(&inst, "GET", "/cam/stream", NULL, 0,
|
||||
&cb_stream, NULL);
|
||||
ulfius_add_endpoint_by_val(&inst, "GET", "/cam/snapshot", NULL, 0,
|
||||
&cb_snapshot, NULL);
|
||||
ulfius_add_endpoint_by_val(&inst, "GET", "/cam/status", NULL, 0,
|
||||
&cb_status, NULL);
|
||||
|
||||
if (ulfius_start_framework(&inst) != U_OK) {
|
||||
fprintf(stderr, "forgectrl: cannot start HTTP on port %u\n", port);
|
||||
ulfius_clean_instance(&inst);
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, "forgectrl: listening on port %u\n", port);
|
||||
|
||||
signal(SIGINT, on_signal);
|
||||
signal(SIGTERM, on_signal);
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
while (!quit)
|
||||
pause();
|
||||
|
||||
fprintf(stderr, "forgectrl: shutting down\n");
|
||||
ulfius_stop_framework(&inst);
|
||||
ulfius_clean_instance(&inst);
|
||||
cam_engine_shutdown();
|
||||
return 0;
|
||||
}
|
||||
@@ -3,13 +3,14 @@ require recipes-glowforge/images/glowforge-image.bb
|
||||
DESCRIPTION = "OpenGlow/ForgeFIRM image for Glowforge"
|
||||
|
||||
# ForgeFIRM cuts the cloud dependency: drop the Glowforge cloud client
|
||||
# (gfui-client connects to Glowforge's servers) — exactly what ForgeFIRM
|
||||
# replaces with a local grblHAL controller (forgectrl, currently a placeholder;
|
||||
# see kas/README.md backlog #5). Removing it here (override only; the shared
|
||||
# glowforge-image base is untouched) also sidesteps its do_package failure.
|
||||
# (gfui-client connects to Glowforge's servers). Removing it here (override
|
||||
# only; the shared glowforge-image base is untouched) also sidesteps its
|
||||
# do_package failure. Its slot is filled locally by forgectrl (camera MJPEG
|
||||
# service; the grblHAL controller recipe is tracked in kas/README.md
|
||||
# backlog #5).
|
||||
IMAGE_INSTALL:remove = "gfui-client"
|
||||
|
||||
# Camera bring-up tooling: v4l-utils provides v4l2-ctl (the media-ctl binary is
|
||||
# already pulled in) for configuring the imx-media pipeline and grabbing frames
|
||||
# while the gfhardware capture path is ported off the factory NXP V4L2 model.
|
||||
IMAGE_INSTALL:append = " v4l-utils"
|
||||
# forgectrl: the ForgeFIRM control daemon (camera MJPEG service on :8080).
|
||||
# v4l-utils provides media-ctl / v4l2-ctl for the imx-media pipeline (also a
|
||||
# forgectrl runtime dependency, kept explicit here for bring-up use).
|
||||
IMAGE_INSTALL:append = " forgectrl v4l-utils"
|
||||
|
||||
Reference in New Issue
Block a user