From 8d798fa4b9d00ec7952948e5d8017e318d4e4f8b Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Mon, 3 Aug 2026 14:06:25 -0400 Subject: [PATCH] forgectrl: promote to its own repo, recipe fetches the pinned git source Sources live at github.com/ScottW514/forgectrl (history extracted, layout src/ + init/); the recipe pins SRCREV and installs the init script from the repo. Adds the build-forgectrl.sh bench cross-compile script. --- docs/BRINGUP.md | 14 +- kas/README.md | 7 +- .../recipes-forgefirm/forgectrl/forgectrl.bb | 23 +- .../forgectrl/forgectrl/CMakeLists.txt | 10 - .../forgectrl/forgectrl/cam.c | 1081 ----------------- .../forgectrl/forgectrl/cam.h | 66 - .../forgectrl/forgectrl/debayer.c | 265 ---- .../forgectrl/forgectrl/debayer.h | 42 - .../forgectrl/forgectrl/forgectrl.init | 36 - .../forgectrl/forgectrl/main.c | 333 ----- .../forgectrl/forgectrl/vpu_jpeg.c | 218 ---- .../forgectrl/forgectrl/vpu_jpeg.h | 33 - .../images/forgefirm-image-dev.bb | 2 +- scripts/bench/README.md | 1 + scripts/bench/build-forgectrl.sh | 22 + 15 files changed, 48 insertions(+), 2105 deletions(-) delete mode 100644 meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/CMakeLists.txt delete mode 100644 meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/cam.c delete mode 100644 meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/cam.h delete mode 100644 meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/debayer.c delete mode 100644 meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/debayer.h delete mode 100644 meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/forgectrl.init delete mode 100644 meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/main.c delete mode 100644 meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/vpu_jpeg.c delete mode 100644 meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/vpu_jpeg.h create mode 100644 scripts/bench/build-forgectrl.sh diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index 0a4a587..c83d2e3 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -124,10 +124,16 @@ hardware I/O — host testing). ## The camera service (forgectrl, port 8080) -Source: `meta-forgefirm/recipes-forgefirm/forgectrl/` (recipe-local C, -MIT; built by the `forgectrl` recipe, installed in both images with a -sysvinit script). One ulfius daemon exposes both OV5648 cameras as MJPEG -over the mainline imx-media pipeline: +Source: `C:\dev\openglow-forgefirm\forgectrl` — the **canonical repo** +(github.com/ScottW514/forgectrl, branch `main`, MIT). forgectrl is the +ForgeFIRM control daemon: camera service today; realtime hardware +status/settings, hardware control, and GRBL-vs-cloud mode selection are +its planned scope. The meta-forgefirm recipe pins its SRCREV (bump +deliberately after pushing) and installs the sysvinit script from the +repo's `init/`; bench builds cross-compile with +`forgefirm/scripts/bench/build-forgectrl.sh` (same toolchain-borrow +pattern as build-glowforge.sh). One ulfius daemon exposes both OV5648 +cameras as MJPEG over the mainline imx-media pipeline: - `GET /` — index page with a live view; `/?action=stream|snapshot` are the mjpg-streamer-compatible aliases (lid camera). diff --git a/kas/README.md b/kas/README.md index 7dc97d5..e37ecb1 100644 --- a/kas/README.md +++ b/kas/README.md @@ -197,8 +197,11 @@ Scarthgap, but the legacy (Dunfell/Gatesgarth) layers won't build clean until: 6. **gfui-client → forgectrl** — the Glowforge **cloud client is excluded** from `forgefirm-image` (`IMAGE_INSTALL:remove = "gfui-client"` in `meta-forgefirm/recipes-forgefirm/images/forgefirm-image.bb`) — it connected to - Glowforge's servers, the dependency ForgeFIRM exists to cut. The grblHAL - controller + web UI (`forgectrl`, currently a placeholder) will fill its place. + Glowforge's servers, the dependency ForgeFIRM exists to cut. Its slot is + filled by `forgectrl` (github.com/ScottW514/forgectrl — the ForgeFIRM + control daemon; camera MJPEG service today, hardware status/control and + GRBL-vs-cloud mode selection planned) plus the grblHAL controller + (recipe still pending, backlog item). --- diff --git a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl.bb b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl.bb index 6289774..2c7c494 100644 --- a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl.bb +++ b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl.bb @@ -2,21 +2,16 @@ DESCRIPTION = "System Control Daemon for ForgeFIRM powered Glowforge" HOMEPAGE = "https://github.com/ScottW514/forgectrl" LICENSE = "MIT" -LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302" +LIC_FILES_CHKSUM = "file://LICENSE;md5=19ed4e3e8c28a4311c16b0b2b91357ec" -SRC_URI = "\ - file://CMakeLists.txt \ - file://main.c \ - file://cam.c \ - file://cam.h \ - file://debayer.c \ - file://debayer.h \ - file://vpu_jpeg.c \ - file://vpu_jpeg.h \ - file://forgectrl.init \ -" +PE = "1" +PV = "0.1.0" -S = "${WORKDIR}" +SRC_URI = "git://github.com/ScottW514/forgectrl.git;protocol=https;branch=main" +# Pinned; bump deliberately after pushing forgectrl changes. +SRCREV = "1253328e76703e33241fb183ce89a133ddcb4466" + +S = "${WORKDIR}/git" inherit cmake update-rc.d @@ -29,5 +24,5 @@ INITSCRIPT_PARAMS = "defaults 90" do_install:append() { install -d ${D}${sysconfdir}/init.d - install -m 0755 ${WORKDIR}/forgectrl.init ${D}${sysconfdir}/init.d/forgectrl + install -m 0755 ${S}/init/forgectrl.init ${D}${sysconfdir}/init.d/forgectrl } diff --git a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/CMakeLists.txt b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/CMakeLists.txt deleted file mode 100644 index 4251931..0000000 --- a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -cmake_minimum_required(VERSION 3.10) -project(forgectrl C) - -set(CMAKE_C_STANDARD 11) - -add_executable(forgectrl main.c cam.c debayer.c vpu_jpeg.c) -target_compile_options(forgectrl PRIVATE -Wall -Wextra -O2) -target_link_libraries(forgectrl ulfius jpeg pthread m) - -install(TARGETS forgectrl RUNTIME DESTINATION bin) diff --git a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/cam.c b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/cam.c deleted file mode 100644 index 9dde7db..0000000 --- a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/cam.c +++ /dev/null @@ -1,1081 +0,0 @@ -/* - * cam.c - persistent Glowforge camera capture engine - * Copyright (c) 2026 Scott Wiederhold - * 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 "vpu_jpeg.h" - -#include -#include -#include -#include -#include -#include -#include -#include /* requires stdio.h first (FILE) */ -#include -#include -#include -#include -#include -#include -#include -#include - -#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 SWITCH_GRACE_S 3 /* wait this long for clients to drain on switch */ - -#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 -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; /* camera the pipeline is configured for - * RIGHT NOW (a borrow flips it briefly) */ - cam_id_t home_cam; /* camera the engine serves for streaming - - * what arbitration must compare against */ - int clients; - uint64_t kick_gen; /* bumped to preempt all current stream - * clients (their streams end cleanly) */ - 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). - * snap_cam may differ from the streaming camera: the worker then - * "borrows" the mux - pauses the stream, switches, grabs one frame, - * switches back (stream clients see a few-second freeze). */ - int snap_pending; /* 1 = requested, 2 = done, 3 = failed */ - cam_id_t snap_cam; - 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; - int vpu_active; /* last stream frame went through the VPU */ -} 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; -} - -/* CODA960 hardware JPEG encoder for the stream path; libjpeg remains the - * fallback (and the snapshot path). Worker-thread use only. */ -static vpu_jpeg_t *vpu; -static int vpu_disabled; - -/* ------------------------------------------------------------------ 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; - } - - pthread_mutex_lock(&eng.lock); - eng.cam = cam; - pthread_mutex_unlock(&eng.lock); - - /* 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 */ - -/* Encode the pending snapshot request from a raw frame and deliver the - * result (success or failure) to the waiter. */ -static void deliver_snap(const uint8_t *raw, uint8_t *rgb_half, - uint8_t **prgb_full) -{ - 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 (!*prgb_full) - *prgb_full = malloc((size_t)CAM_W * CAM_H * 3); - ok = *prgb_full != NULL; - if (ok) { - debayer_bggr_bilinear(raw, *prgb_full, CAM_W, CAM_H, HFLIP); - ok = jpeg_encode_rgb(*prgb_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); -} - -/* Mark a pending snapshot failed (only if not already delivered). */ -static void fail_snap(void) -{ - pthread_mutex_lock(&eng.lock); - if (eng.snap_pending == 1) { - eng.snap_pending = 3; - pthread_cond_broadcast(&eng.snap_cv); - } - pthread_mutex_unlock(&eng.lock); -} - -/* Capture one frame from the currently-started pipeline and feed it to - * deliver_snap. Used by the borrow path. */ -static int grab_one_snap(uint8_t *raw_cached, uint8_t *rgb_half, - uint8_t **prgb_full) -{ - for (int tries = 0; tries < MAX_DQ_TIMEOUTS; tries++) { - 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) { - tries--; - continue; - } - if (r <= 0) - 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; - return -1; - } - memcpy(raw_cached, eng.bufs[buf.index].start, - (size_t)CAM_W * CAM_H); - deliver_snap(raw_cached, rgb_half, prgb_full); - xioctl(eng.fd, VIDIOC_QBUF, &buf); - return 0; - } - return -1; -} - -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 */ - /* The V4L2 MMAP capture buffers are DMA-coherent = UNCACHED: byte - * reads from them cost a bus transaction each and demosaicing - * straight out of one measures ~340 ms/frame. One bulk memcpy into - * this cached bounce buffer first makes the demosaic run at cached - * speed. */ - uint8_t *raw_cached = malloc((size_t)CAM_W * CAM_H); - double stat_copy_ms = 0, stat_conv_ms = 0, stat_enc_ms = 0; - unsigned stat_n = 0; - int dq_timeouts = 0; - struct timespec fps_t0; - now_ts(&fps_t0); - uint64_t fps_frames = 0; - - if (!rgb_half || !raw_cached) { - 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 && eng.snap_cam == eng.home_cam; - int borrow = eng.snap_pending == 1 && eng.snap_cam != eng.home_cam; - cam_id_t borrow_cam = eng.snap_cam; - cam_id_t orig_cam = eng.home_cam; - int idle = clients == 0 && !snap && !borrow && - ts_diff(&now, &eng.last_activity) > IDLE_STOP_S; - pthread_mutex_unlock(&eng.lock); - - if (stop || idle) - break; - - /* Cross-camera snapshot: borrow the mux - pause the stream, - * switch, grab one frame, switch back. Stream clients just see - * the frame gap (a few seconds). */ - if (borrow) { - char berr[128]; - release_capture(); - if (start_capture(borrow_cam, berr, sizeof(berr)) == 0) { - if (grab_one_snap(raw_cached, rgb_half, &rgb_full)) - fail_snap(); - release_capture(); - } else { - fprintf(stderr, "cam: borrow start failed: %s\n", berr); - fail_snap(); - } - if (start_capture(orig_cam, berr, sizeof(berr))) { - fprintf(stderr, "cam: restore after borrow failed: %s\n", - berr); - break; /* engine dies; streams end; reconnect heals */ - } - continue; - } - - /* 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; - struct timespec c0, c1; - now_ts(&c0); - memcpy(raw_cached, eng.bufs[buf.index].start, - (size_t)CAM_W * CAM_H); - now_ts(&c1); - const uint8_t *raw = raw_cached; - - /* Snapshot request rides on the same raw frame */ - if (snap) - deliver_snap(raw, rgb_half, &rgb_full); - - /* Stream frame: demosaic + encode, VPU first, libjpeg fallback */ - if (clients > 0) { - uint8_t *jpg = NULL; - size_t len = 0; - int via_vpu = 0; - struct timespec e0, e1, e2; - now_ts(&e0); - - if (!vpu_disabled && !vpu) { - vpu = vpu_jpeg_open(HALF_W, HALF_H, eng.stream_quality); - if (!vpu) { - vpu_disabled = 1; - fprintf(stderr, "cam: no VPU JPEG encoder, " - "using software encode\n"); - } - } - if (vpu) { - uint8_t *yp, *up, *vp; - int ys, uvs; - vpu_jpeg_planes(vpu, &yp, &up, &vp, &ys, &uvs); - debayer_bggr_half_yuv420(raw, CAM_W, CAM_H, HFLIP, - yp, ys, up, vp, uvs); -#ifdef __ARM_NEON - /* One-shot NEON-vs-scalar equivalence check on a live - * frame (the paths are constructed to be bit-identical; - * this proves it on real data). Stride == width here. */ - static int neon_checked; - if (!neon_checked && getenv("FORGECTRL_NEON_CHECK")) { - neon_checked = 1; - size_t ysz = (size_t)ys * HALF_H; - size_t usz = (size_t)uvs * (HALF_H / 2); - uint8_t *ry = malloc(ysz); - uint8_t *ru = malloc(usz); - uint8_t *rv = malloc(usz); - if (ry && ru && rv) { - debayer_bggr_half_yuv420_scalar(raw, CAM_W, CAM_H, - HFLIP, ry, ys, - ru, rv, uvs); - fprintf(stderr, "cam: NEON/scalar compare: %s\n", - (!memcmp(ry, yp, ysz) && - !memcmp(ru, up, usz) && - !memcmp(rv, vp, usz)) - ? "IDENTICAL" : "MISMATCH"); - } - free(ry); - free(ru); - free(rv); - } -#endif - now_ts(&e1); - if (vpu_jpeg_encode(vpu, &jpg, &len) == 0) { - via_vpu = 1; - } else { - fprintf(stderr, "cam: VPU encode failed, " - "falling back to software\n"); - vpu_jpeg_close(vpu); - vpu = NULL; - vpu_disabled = 1; - } - } - if (!via_vpu) { - debayer_bggr_half(raw, rgb_half, CAM_W, CAM_H, HFLIP); - now_ts(&e1); - if (jpeg_encode_rgb(rgb_half, HALF_W, HALF_H, - eng.stream_quality, 1, &jpg, &len)) - jpg = NULL; - } - now_ts(&e2); - - if (jpg) { - stat_copy_ms += ts_diff(&c1, &c0) * 1e3; - stat_conv_ms += ts_diff(&e1, &e0) * 1e3; - stat_enc_ms += ts_diff(&e2, &e1) * 1e3; - if (++stat_n >= 100) { - fprintf(stderr, "cam: stream stats: copy %.0f ms, " - "convert %.0f ms, encode %.0f ms avg (%s)\n", - stat_copy_ms / stat_n, stat_conv_ms / stat_n, - stat_enc_ms / stat_n, - via_vpu ? "vpu" : "software"); - stat_copy_ms = stat_conv_ms = stat_enc_ms = 0; - stat_n = 0; - } - pthread_mutex_lock(&eng.lock); - free(eng.stream_jpg); - eng.stream_jpg = jpg; - eng.stream_len = len; - eng.vpu_active = via_vpu; - 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); - free(raw_cached); - 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; - /* Compare against the HOME camera: during a snapshot borrow the - * pipeline (eng.cam) is briefly on the other sensor, and a stream - * request racing that window must not attach to it. */ - cam_id_t cur = eng.home_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) { - /* Last request wins: preempt the current stream clients - * (single-operator machine - the newest ask is the - * operator). Kicked clients wake, end their streams - * cleanly (viewers freeze on their last frame), and - * release their pins; wait for that to drain. */ - pthread_mutex_lock(&eng.lock); - eng.kick_gen++; - pthread_cond_broadcast(&eng.frame_cv); - pthread_mutex_unlock(&eng.lock); - struct timespec t0, t; - now_ts(&t0); - do { - usleep(100 * 1000); - pthread_mutex_lock(&eng.lock); - clients = eng.clients; - pthread_mutex_unlock(&eng.lock); - now_ts(&t); - } while (clients > 0 && ts_diff(&t, &t0) < SWITCH_GRACE_S); - if (clients > 0) { - snprintf(err, errlen, - "camera switch timed out: %d client(s) still " - "attached to %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 */ - pthread_mutex_lock(&eng.lock); - eng.home_cam = cam; - pthread_mutex_unlock(&eng.lock); - 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; - } - if (getenv("FORGECTRL_NO_VPU")) - vpu_disabled = 1; -} - -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); - } - if (vpu) { - vpu_jpeg_close(vpu); - vpu = NULL; - } - 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 the engine is streaming the OTHER camera for active clients, - * don't switch it - post the request and let the worker borrow the - * mux for one frame. Otherwise make the engine run on `cam`. */ - pthread_mutex_lock(&eng.lock); - int streaming_other = eng.running && eng.home_cam != cam && - eng.clients > 0; - pthread_mutex_unlock(&eng.lock); - - if (!streaming_other && ensure_engine(cam, err, errlen)) { - pthread_mutex_unlock(&eng.ctl); - return -1; - } - - pthread_mutex_lock(&eng.lock); - eng.snap_pending = 1; - eng.snap_cam = cam; - 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; - uint64_t gen; /* kick generation at open; a bump ends the stream */ - 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++; - c->gen = eng.kick_gen; - 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 && c->gen == eng.kick_gen && 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 || c->gen != eng.kick_gen || 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.home_cam; - st->clients = eng.clients; - st->seq = eng.seq; - st->fps = eng.fps; - st->vpu = eng.vpu_active; - pthread_mutex_unlock(&eng.lock); -} diff --git a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/cam.h b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/cam.h deleted file mode 100644 index be7ad25..0000000 --- a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/cam.h +++ /dev/null @@ -1,66 +0,0 @@ -/* - * cam.h - persistent Glowforge camera capture engine - * Copyright (c) 2026 Scott Wiederhold - * 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 -#include - -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). If the other camera is streaming, the worker borrows the mux for - * one frame (the stream freezes for a few seconds) - snapshots do not fail - * busy. Returns 0, or -1 with a message in err (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 makes the engine serve `cam` (starting it, or - * preempting the current clients and switching - last request wins; the - * preempted clients' next() returns -1 so their streams end cleanly). - * 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; - int vpu; /* stream frames are VPU-encoded */ -}; -void cam_get_status(struct cam_status *st); - -const char *cam_name(cam_id_t cam); - -#endif diff --git a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/debayer.c b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/debayer.c deleted file mode 100644 index 8ebbd3f..0000000 --- a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/debayer.c +++ /dev/null @@ -1,265 +0,0 @@ -/* - * debayer.c - BGGR raw-Bayer to RGB conversion for the Glowforge cameras - * Copyright (c) 2026 Scott Wiederhold - * SPDX-License-Identifier: MIT - * - * BGGR tile layout (row 0 topmost): - * even rows: B G B G ... - * odd rows: G R G R ... - */ -#include - -#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_yuv420_scalar(const uint8_t *raw, int w, int h, - int hflip, uint8_t *yp, int y_stride, - uint8_t *up, uint8_t *vp, int uv_stride) -{ - const int ow = w / 2; /* luma dimensions */ - const int oh = h / 2; - const int uvw = ow / 2; - - /* JFIF full-range ITU-R 601, x256 fixed point: - * Y = 0.299 R + 0.587 G + 0.114 B -> 77 150 29 - * Cb = -0.169 R - 0.331 G + 0.500 B + 128 -> -43 -85 128 - * Cr = 0.500 R - 0.419 G - 0.081 B + 128 -> 128 -107 -21 */ - for (int y2 = 0; y2 < oh / 2; y2++) { - uint8_t *yrow0 = yp + (size_t)(2 * y2) * y_stride; - uint8_t *yrow1 = yrow0 + y_stride; - uint8_t *urow = up + (size_t)y2 * uv_stride; - uint8_t *vrow = vp + (size_t)y2 * uv_stride; - for (int x2 = 0; x2 < uvw; x2++) { - int rs = 0, gs = 0, bs = 0; - for (int sy = 0; sy < 2; sy++) { - const int row = 2 * y2 + sy; - const uint8_t *quad_row = raw + (size_t)(2 * row) * w; - uint8_t *yrow = sy ? yrow1 : yrow0; - for (int sx = 0; sx < 2; sx++) { - const int col = 2 * x2 + sx; - const uint8_t *q = quad_row + 2 * col; - const int b = q[0]; - const int g = (q[1] + q[w] + 1) >> 1; - const int r = q[w + 1]; - rs += r; - gs += g; - bs += b; - yrow[hflip ? ow - 1 - col : col] = - (uint8_t)((77 * r + 150 * g + 29 * b + 128) >> 8); - } - } - const int cx = hflip ? uvw - 1 - x2 : x2; - int cb = ((-43 * rs - 85 * gs + 128 * bs + 512) >> 10) + 128; - int cr = ((128 * rs - 107 * gs - 21 * bs + 512) >> 10) + 128; - urow[cx] = (uint8_t)(cb < 0 ? 0 : (cb > 255 ? 255 : cb)); - vrow[cx] = (uint8_t)(cr < 0 ? 0 : (cr > 255 ? 255 : cr)); - } - } -} - -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 */ - } - } -} - -#ifdef __ARM_NEON -#include - -/* Store 16 luma bytes at column x, mirrored when hflip (the vector is - * byte-reversed and lands at the mirrored block position). */ -static inline void store16_flip(uint8_t *row, int x, int ow, int hflip, - uint8x16_t v) -{ - if (!hflip) { - vst1q_u8(row + x, v); - } else { - uint8x16_t r = vrev64q_u8(v); - r = vextq_u8(r, r, 8); /* swap halves: full 16-byte reverse */ - vst1q_u8(row + (ow - 16 - x), r); - } -} - -static inline void store8_flip(uint8_t *row, int x, int n, int hflip, - uint8x8_t v) -{ - if (!hflip) - vst1_u8(row + x, v); - else - vst1_u8(row + (n - 8 - x), vrev64_u8(v)); -} - -/* Y = (77R + 150G + 29B + 128) >> 8 for 16 pixels. */ -static inline uint8x16_t luma16(uint8x16_t R, uint8x16_t G, uint8x16_t B) -{ - const uint8x8_t cR = vdup_n_u8(77), cG = vdup_n_u8(150), - cB = vdup_n_u8(29); - uint16x8_t lo = vmull_u8(vget_low_u8(R), cR); - lo = vmlal_u8(lo, vget_low_u8(G), cG); - lo = vmlal_u8(lo, vget_low_u8(B), cB); - uint16x8_t hi = vmull_u8(vget_high_u8(R), cR); - hi = vmlal_u8(hi, vget_high_u8(G), cG); - hi = vmlal_u8(hi, vget_high_u8(B), cB); - return vcombine_u8(vrshrn_n_u16(lo, 8), vrshrn_n_u16(hi, 8)); -} - -/* 4-superpixel block sums (vertical add then horizontal pair-add) for one - * 16-column pair of rows -> 8 lanes of u32 split across two quads. */ -static inline void block_sums(uint8x16_t a, uint8x16_t b, - int32x4_t *q0, int32x4_t *q1) -{ - uint16x8_t lo = vaddl_u8(vget_low_u8(a), vget_low_u8(b)); - uint16x8_t hi = vaddl_u8(vget_high_u8(a), vget_high_u8(b)); - *q0 = vreinterpretq_s32_u32(vpaddlq_u16(lo)); - *q1 = vreinterpretq_s32_u32(vpaddlq_u16(hi)); -} - -/* ((cr*rs + cg*gs + cb*bs + 512) >> 10) + 128, clamped to 0..255. */ -static inline uint16x4_t chroma4(int32x4_t rs, int32x4_t gs, int32x4_t bs, - int cr, int cg, int cb) -{ - int32x4_t acc = vmulq_n_s32(rs, cr); - acc = vmlaq_n_s32(acc, gs, cg); - acc = vmlaq_n_s32(acc, bs, cb); - acc = vaddq_s32(acc, vdupq_n_s32(512)); - acc = vshrq_n_s32(acc, 10); - acc = vaddq_s32(acc, vdupq_n_s32(128)); - return vqmovun_s32(acc); /* clamps < 0 */ -} - -static void debayer_bggr_half_yuv420_neon(const uint8_t *raw, int w, int h, - int hflip, - uint8_t *yp, int y_stride, - uint8_t *up, uint8_t *vp, - int uv_stride) -{ - const int ow = w / 2; - const int oh = h / 2; - const int uvw = ow / 2; - - for (int y2 = 0; y2 < oh / 2; y2++) { - const uint8_t *r0 = raw + (size_t)(4 * y2) * w; /* B G ... */ - const uint8_t *r1 = r0 + w; /* G R ... */ - const uint8_t *r2 = r1 + w; - const uint8_t *r3 = r2 + w; - uint8_t *ya = yp + (size_t)(2 * y2) * y_stride; - uint8_t *yb = ya + y_stride; - uint8_t *ur = up + (size_t)y2 * uv_stride; - uint8_t *vr = vp + (size_t)y2 * uv_stride; - - for (int x = 0; x < ow; x += 16) { - /* superpixel row A: raw rows r0/r1 */ - uint8x16x2_t ea = vld2q_u8(r0 + 2 * x); /* [0]=B [1]=G1 */ - uint8x16x2_t oa = vld2q_u8(r1 + 2 * x); /* [0]=G2 [1]=R */ - uint8x16_t Ba = ea.val[0]; - uint8x16_t Ga = vrhaddq_u8(ea.val[1], oa.val[0]); - uint8x16_t Ra = oa.val[1]; - store16_flip(ya, x, ow, hflip, luma16(Ra, Ga, Ba)); - - /* superpixel row B: raw rows r2/r3 */ - uint8x16x2_t eb = vld2q_u8(r2 + 2 * x); - uint8x16x2_t ob = vld2q_u8(r3 + 2 * x); - uint8x16_t Bb = eb.val[0]; - uint8x16_t Gb = vrhaddq_u8(eb.val[1], ob.val[0]); - uint8x16_t Rb = ob.val[1]; - store16_flip(yb, x, ow, hflip, luma16(Rb, Gb, Bb)); - - /* chroma from the 2x2 superpixel blocks of both rows */ - int32x4_t rs0, rs1, gs0, gs1, bs0, bs1; - block_sums(Ra, Rb, &rs0, &rs1); - block_sums(Ga, Gb, &gs0, &gs1); - block_sums(Ba, Bb, &bs0, &bs1); - - uint16x8_t cb = vcombine_u16(chroma4(rs0, gs0, bs0, -43, -85, 128), - chroma4(rs1, gs1, bs1, -43, -85, 128)); - uint16x8_t cr = vcombine_u16(chroma4(rs0, gs0, bs0, 128, -107, -21), - chroma4(rs1, gs1, bs1, 128, -107, -21)); - store8_flip(ur, x / 2, uvw, hflip, vqmovn_u16(cb)); - store8_flip(vr, x / 2, uvw, hflip, vqmovn_u16(cr)); - } - } -} -#endif /* __ARM_NEON */ - -void debayer_bggr_half_yuv420(const uint8_t *raw, int w, int h, int hflip, - uint8_t *yp, int y_stride, - uint8_t *up, uint8_t *vp, int uv_stride) -{ -#ifdef __ARM_NEON - static int no_neon = -1; - if (no_neon < 0) - no_neon = getenv("FORGECTRL_NO_NEON") != NULL; - if (!no_neon && w % 32 == 0 && h % 4 == 0) { - debayer_bggr_half_yuv420_neon(raw, w, h, hflip, yp, y_stride, - up, vp, uv_stride); - return; - } -#endif - debayer_bggr_half_yuv420_scalar(raw, w, h, hflip, yp, y_stride, - up, vp, uv_stride); -} diff --git a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/debayer.h b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/debayer.h deleted file mode 100644 index 1d55732..0000000 --- a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/debayer.h +++ /dev/null @@ -1,42 +0,0 @@ -/* - * debayer.h - BGGR raw-Bayer to RGB conversion for the Glowforge cameras - * Copyright (c) 2026 Scott Wiederhold - * SPDX-License-Identifier: MIT - */ -#ifndef FORGECTRL_DEBAYER_H -#define FORGECTRL_DEBAYER_H - -#include - -/* 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); - -/* Half-resolution demosaic straight to planar YUV420 (JFIF full-range, - * ITU-R 601) for the VPU JPEG encoder: luma per 2x2 BGGR quad at - * (w/2)x(h/2), chroma averaged per 2x2 luma block at (w/4)x(h/4). - * w/2 and h/2 must be even. Strides are in bytes. - * - * Dispatches to a NEON kernel when compiled for NEON and the geometry - * allows (w%32==0, h%4==0); FORGECTRL_NO_NEON=1 forces the scalar path. - * Both paths produce bit-identical output. */ -void debayer_bggr_half_yuv420(const uint8_t *raw, int w, int h, int hflip, - uint8_t *yp, int y_stride, - uint8_t *up, uint8_t *vp, int uv_stride); - -/* The scalar reference path (used by the NEON self-check). */ -void debayer_bggr_half_yuv420_scalar(const uint8_t *raw, int w, int h, - int hflip, uint8_t *yp, int y_stride, - uint8_t *up, uint8_t *vp, - int uv_stride); - -#endif diff --git a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/forgectrl.init b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/forgectrl.init deleted file mode 100644 index 870e6b8..0000000 --- a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/forgectrl.init +++ /dev/null @@ -1,36 +0,0 @@ -#!/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 diff --git a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/main.c b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/main.c deleted file mode 100644 index e155a02..0000000 --- a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/main.c +++ /dev/null @@ -1,333 +0,0 @@ -/* - * main.c - forgectrl: ForgeFIRM system control daemon - * Copyright (c) 2026 Scott Wiederhold - * 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; the newest request wins it. A - * STREAM request for the other camera preempts the current stream - * clients (their streams end cleanly - viewers freeze on the last frame) - * and switches. A SNAPSHOT of the other camera does not switch: the - * engine borrows the mux for one frame and the stream freezes briefly. - * 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 -#include -#include -#include -#include -#include - -#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,\"encoder\":\"%s\"," - "\"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, - st.vpu ? "vpu" : "software"); - ulfius_set_string_body_response(res, 200, body); - ulfius_add_header_to_response(res, "Content-Type", "application/json"); - return U_CALLBACK_CONTINUE; -} - -/* One stream at a time: the camera toggle swaps the single source - * (closing the old stream connection) and retries through the server's - * switch grace. "Head peek" uses the snapshot borrow path, so it works - * while the lid stream is up. */ -static const char index_html[] = - "ForgeFIRM camera" - "" - "

ForgeFIRM camera

" - "

" - "" - "   " - "lid full | " - "head full | " - "status

" - "
" - "\"camera" - "\"\"" - ""; - -/* "/" 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; -} diff --git a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/vpu_jpeg.c b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/vpu_jpeg.c deleted file mode 100644 index f3bf318..0000000 --- a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/vpu_jpeg.c +++ /dev/null @@ -1,218 +0,0 @@ -/* - * vpu_jpeg.c - hardware JPEG encoding on the i.MX6 CODA960 VPU - * Copyright (c) 2026 Scott Wiederhold - * SPDX-License-Identifier: MIT - * - * V4L2 mem2mem, single-planar API against the mainline coda driver: one - * MMAP buffer on each queue, synchronous QBUF/DQBUF per frame. The node - * is found by personality (driver "coda", JPEG on the capture side, - * YUV420 accepted on the output side), never by number - coda registers - * four nodes and the numbering depends on probe order. - */ -#include "vpu_jpeg.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define ENCODE_TIMEOUT_MS 1000 - -struct vpu_jpeg { - int fd; - int w, h; - int bpl; /* OUTPUT luma stride from S_FMT */ - uint8_t *out; /* mapped OUTPUT (YUV420) buffer */ - size_t out_size; - uint8_t *cap; /* mapped CAPTURE (JPEG) buffer */ - size_t cap_size; -}; - -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; -} - -/* Is this node the coda JPEG encoder? (JPEG capture, YUV420 output) */ -static int is_jpeg_encoder(int fd) -{ - struct v4l2_capability cap = {0}; - if (xioctl(fd, VIDIOC_QUERYCAP, &cap) < 0 || - strcmp((const char *)cap.driver, "coda") != 0 || - !(cap.device_caps & V4L2_CAP_VIDEO_M2M)) - return 0; - - struct v4l2_fmtdesc fd0 = { .type = V4L2_BUF_TYPE_VIDEO_CAPTURE }; - if (xioctl(fd, VIDIOC_ENUM_FMT, &fd0) < 0 || - fd0.pixelformat != V4L2_PIX_FMT_JPEG) - return 0; - - for (unsigned i = 0; ; i++) { - struct v4l2_fmtdesc fo = { .type = V4L2_BUF_TYPE_VIDEO_OUTPUT, - .index = i }; - if (xioctl(fd, VIDIOC_ENUM_FMT, &fo) < 0) - return 0; - if (fo.pixelformat == V4L2_PIX_FMT_YUV420) - return 1; - } -} - -static int find_encoder(void) -{ - for (int i = 0; i < 32; i++) { - char path[32]; - snprintf(path, sizeof(path), "/dev/video%d", i); - int fd = open(path, O_RDWR | O_NONBLOCK, 0); - if (fd < 0) - continue; - if (is_jpeg_encoder(fd)) - return fd; - close(fd); - } - return -1; -} - -static int map_one(int fd, enum v4l2_buf_type type, uint8_t **mem, - size_t *size) -{ - struct v4l2_requestbuffers req = { .count = 1, .type = type, - .memory = V4L2_MEMORY_MMAP }; - if (xioctl(fd, VIDIOC_REQBUFS, &req) < 0 || req.count < 1) - return -1; - struct v4l2_buffer buf = { .type = type, .memory = V4L2_MEMORY_MMAP, - .index = 0 }; - if (xioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) - return -1; - *mem = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, - fd, buf.m.offset); - if (*mem == MAP_FAILED) { - *mem = NULL; - return -1; - } - *size = buf.length; - return 0; -} - -vpu_jpeg_t *vpu_jpeg_open(int w, int h, int quality) -{ - vpu_jpeg_t *v = calloc(1, sizeof(*v)); - if (!v) - return NULL; - v->fd = find_encoder(); - if (v->fd < 0) - goto fail; - - struct v4l2_format fo = { .type = V4L2_BUF_TYPE_VIDEO_OUTPUT }; - fo.fmt.pix.width = (unsigned)w; - fo.fmt.pix.height = (unsigned)h; - fo.fmt.pix.pixelformat = V4L2_PIX_FMT_YUV420; - fo.fmt.pix.field = V4L2_FIELD_NONE; - if (xioctl(v->fd, VIDIOC_S_FMT, &fo) < 0 || - fo.fmt.pix.width != (unsigned)w || - fo.fmt.pix.height != (unsigned)h) { - fprintf(stderr, "vpu: S_FMT output rejected %dx%d\n", w, h); - goto fail; - } - v->w = w; - v->h = h; - v->bpl = (int)fo.fmt.pix.bytesperline; - - struct v4l2_format fc = { .type = V4L2_BUF_TYPE_VIDEO_CAPTURE }; - fc.fmt.pix.width = (unsigned)w; - fc.fmt.pix.height = (unsigned)h; - fc.fmt.pix.pixelformat = V4L2_PIX_FMT_JPEG; - if (xioctl(v->fd, VIDIOC_S_FMT, &fc) < 0) - goto fail; - - struct v4l2_control q = { .id = V4L2_CID_JPEG_COMPRESSION_QUALITY, - .value = quality }; - xioctl(v->fd, VIDIOC_S_CTRL, &q); /* best effort */ - - if (map_one(v->fd, V4L2_BUF_TYPE_VIDEO_OUTPUT, &v->out, &v->out_size) || - map_one(v->fd, V4L2_BUF_TYPE_VIDEO_CAPTURE, &v->cap, &v->cap_size)) - goto fail; - - enum v4l2_buf_type t = V4L2_BUF_TYPE_VIDEO_OUTPUT; - if (xioctl(v->fd, VIDIOC_STREAMON, &t) < 0) - goto fail; - t = V4L2_BUF_TYPE_VIDEO_CAPTURE; - if (xioctl(v->fd, VIDIOC_STREAMON, &t) < 0) - goto fail; - return v; - -fail: - vpu_jpeg_close(v); - return NULL; -} - -void vpu_jpeg_planes(vpu_jpeg_t *v, uint8_t **y, uint8_t **u, uint8_t **vv, - int *y_stride, int *uv_stride) -{ - *y = v->out; - *u = v->out + (size_t)v->bpl * v->h; - *vv = *u + (size_t)(v->bpl / 2) * (v->h / 2); - *y_stride = v->bpl; - *uv_stride = v->bpl / 2; -} - -int vpu_jpeg_encode(vpu_jpeg_t *v, uint8_t **jpeg, size_t *len) -{ - struct v4l2_buffer cb = { .type = V4L2_BUF_TYPE_VIDEO_CAPTURE, - .memory = V4L2_MEMORY_MMAP, .index = 0 }; - struct v4l2_buffer ob = { .type = V4L2_BUF_TYPE_VIDEO_OUTPUT, - .memory = V4L2_MEMORY_MMAP, .index = 0 }; - ob.bytesused = (unsigned)v->out_size; - - if (xioctl(v->fd, VIDIOC_QBUF, &cb) < 0 || - xioctl(v->fd, VIDIOC_QBUF, &ob) < 0) - return -1; - - struct pollfd pfd = { .fd = v->fd, .events = POLLIN }; - int pr; - do { - pr = poll(&pfd, 1, ENCODE_TIMEOUT_MS); - } while (pr == -1 && errno == EINTR); - if (pr <= 0) - return -1; - - if (xioctl(v->fd, VIDIOC_DQBUF, &cb) < 0) - return -1; - xioctl(v->fd, VIDIOC_DQBUF, &ob); - - uint8_t *out = malloc(cb.bytesused); - if (!out) - return -1; - memcpy(out, v->cap, cb.bytesused); - *jpeg = out; - *len = cb.bytesused; - return 0; -} - -void vpu_jpeg_close(vpu_jpeg_t *v) -{ - if (!v) - return; - if (v->fd >= 0) { - enum v4l2_buf_type t = V4L2_BUF_TYPE_VIDEO_OUTPUT; - xioctl(v->fd, VIDIOC_STREAMOFF, &t); - t = V4L2_BUF_TYPE_VIDEO_CAPTURE; - xioctl(v->fd, VIDIOC_STREAMOFF, &t); - } - if (v->out) - munmap(v->out, v->out_size); - if (v->cap) - munmap(v->cap, v->cap_size); - if (v->fd >= 0) - close(v->fd); - free(v); -} diff --git a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/vpu_jpeg.h b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/vpu_jpeg.h deleted file mode 100644 index 673f19a..0000000 --- a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl/vpu_jpeg.h +++ /dev/null @@ -1,33 +0,0 @@ -/* - * vpu_jpeg.h - hardware JPEG encoding on the i.MX6 CODA960 VPU - * Copyright (c) 2026 Scott Wiederhold - * SPDX-License-Identifier: MIT - * - * Thin wrapper around the mainline coda V4L2 mem2mem JPEG encoder: the - * caller writes planar YUV420 directly into the encoder's OUTPUT buffer - * (vpu_jpeg_planes) and gets back a malloc'd JFIF JPEG. - */ -#ifndef FORGECTRL_VPU_JPEG_H -#define FORGECTRL_VPU_JPEG_H - -#include -#include - -typedef struct vpu_jpeg vpu_jpeg_t; - -/* Locate the CODA JPEG encoder video node, configure it for w x h YUV420 - * -> JPEG at the given quality (5..100), and map one buffer per queue. - * Returns NULL if no encoder exists or setup fails. */ -vpu_jpeg_t *vpu_jpeg_open(int w, int h, int quality); - -/* Planes of the mapped OUTPUT buffer for direct fill. */ -void vpu_jpeg_planes(vpu_jpeg_t *v, uint8_t **y, uint8_t **u, uint8_t **vv, - int *y_stride, int *uv_stride); - -/* Encode the currently-filled OUTPUT buffer. On success *jpeg is malloc'd - * (caller frees) and 0 is returned. */ -int vpu_jpeg_encode(vpu_jpeg_t *v, uint8_t **jpeg, size_t *len); - -void vpu_jpeg_close(vpu_jpeg_t *v); - -#endif diff --git a/meta-forgefirm/recipes-forgefirm/images/forgefirm-image-dev.bb b/meta-forgefirm/recipes-forgefirm/images/forgefirm-image-dev.bb index 8a1c520..4064a74 100644 --- a/meta-forgefirm/recipes-forgefirm/images/forgefirm-image-dev.bb +++ b/meta-forgefirm/recipes-forgefirm/images/forgefirm-image-dev.bb @@ -3,7 +3,7 @@ require forgefirm-image.bb DESCRIPTION = "OpenGlow/ForgeFIRM development image for Glowforge" # Strict superset of forgefirm-image: everything the main image ships, plus -# the forgectrl placeholder and debug tooling. +# debug tooling. IMAGE_INSTALL += " \ forgectrl \ " diff --git a/scripts/bench/README.md b/scripts/bench/README.md index 8480eb1..70496ce 100644 --- a/scripts/bench/README.md +++ b/scripts/bench/README.md @@ -19,6 +19,7 @@ target board (dev image, python3 present) unless noted. | `flow_recheck_char.py` | Characterizes short in-run re-checks and the differential metric; shows why over-temp cannot see a stopped pump and why passive warming trends are ambiguous. | | `temp_calibrate.py` | Coolant temperature spot-check helper (`watch` / `point ` / `fit`) — pairs a measured temperature with averaged raw ADC readings and fits a per-machine line to sanity-check the factory curve against a thermometer. | | `build-glowforge.sh` | Cross-compiles **grblHAL-glowforge** (the canonical driver repo, `../../../grblHAL-glowforge`) in the forge-yocto WSL distro. Run: `wsl -d forge-yocto -- bash /build-glowforge.sh` (from PowerShell; Git Bash mangles /mnt/c paths). This is the production controller build. | +| `build-forgectrl.sh` | Cross-compiles **forgectrl** (the canonical control-daemon repo, `../../../forgectrl`) the same way, borrowing the toolchain from the forgectrl recipe workdir (regenerate with `bitbake forgectrl` after a clean). | | `build-feeder.sh` | Cross-compiles `feeder.c` the same way. | | `puls_profile.py` | Decodes factory `.puls` streams (raw or GF1-headered) into velocity/accel profiles: peak speeds, ramp-slope fits, per-move segments, Z cadence. Runs anywhere (stdlib only). Source of the factory-true grblHAL defaults: 700/590 mm/s² accel, 200 mm/s max rate, 28160 Hz travel tick. | | `bench_m2.py` | Motion-quality bench, runs against the board over TCP:23: bounded round-trip jogs (sanity, max-rate, diagonal) + feed-hold/resume mid-move, reporting peak feed, state transitions, and position drift. | diff --git a/scripts/bench/build-forgectrl.sh b/scripts/bench/build-forgectrl.sh new file mode 100644 index 0000000..a600de2 --- /dev/null +++ b/scripts/bench/build-forgectrl.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Cross-compiles forgectrl (the canonical repo, ../../../forgectrl -> synced +# to ~/dev/openglow-forgefirm/forgectrl) for the factory board, borrowing the +# Yocto cross toolchain + sysroot from the forgectrl recipe work directory +# (which carries ulfius and libjpeg). If that path ages out after a clean, +# regenerate it with: bitbake forgectrl. Run from PowerShell: +# wsl -d forge-yocto -- bash /build-forgectrl.sh +set -e +TC=/home/builder/dev/openglow-forgefirm/forgefirm/build/tmp/work/cortexa9t2hf-neon-fslc-linux-gnueabi/forgectrl/0.1.0 +export PATH="$TC/recipe-sysroot-native/usr/bin:$TC/recipe-sysroot-native/usr/bin/arm-fslc-linux-gnueabi:$PATH" +cd /home/builder/dev/openglow-forgefirm/forgectrl +rm -rf build-arm +cmake -B build-arm \ + -DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=arm \ + -DCMAKE_C_COMPILER=arm-fslc-linux-gnueabi-gcc \ + -DCMAKE_BUILD_TYPE=None \ + "-DCMAKE_C_FLAGS=--sysroot=$TC/recipe-sysroot -mthumb -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a9 -O2 -g" \ + "-DCMAKE_EXE_LINKER_FLAGS=--sysroot=$TC/recipe-sysroot" \ + > /tmp/cmake-fc.log 2>&1 +cmake --build build-arm -j8 > /tmp/fcbuild.log 2>&1 || { tail -30 /tmp/fcbuild.log; exit 1; } +echo BUILD-OK +file build-arm/forgectrl 2>/dev/null || ls build-arm/