mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-29 09:31:17 -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:
+56
-5
@@ -1,8 +1,9 @@
|
|||||||
# ForgeFIRM bring-up status & cold-start runbook
|
# ForgeFIRM bring-up status & cold-start runbook
|
||||||
|
|
||||||
Last updated: **2026-08-02** — milestone 2 (factory-true motion tuning)
|
Last updated: **2026-08-03** — camera service (forgectrl MJPEG on :8080)
|
||||||
bench-verified, and the controller promoted to a canonical grblHAL driver
|
implemented and bench-verified, including motion-coexistence (clamped 0
|
||||||
repo (**grblHAL-glowforge**) with bench parity re-proven on hardware.
|
while streaming). Previous milestone: factory-true motion tuning +
|
||||||
|
promotion to the canonical grblHAL driver repo (**grblHAL-glowforge**).
|
||||||
Read together with `AUDIT_ACTION_PLAN.md` in the project root (sibling of
|
Read together with `AUDIT_ACTION_PLAN.md` in the project root (sibling of
|
||||||
this repo; per-finding status of the 2026-07-03 audit) and
|
this repo; per-finding status of the 2026-07-03 audit) and
|
||||||
`kernel-module-glowforge/UAPI.md` (the pulse-stream feeder contract).
|
`kernel-module-glowforge/UAPI.md` (the pulse-stream feeder contract).
|
||||||
@@ -121,6 +122,47 @@ hardware I/O — host testing).
|
|||||||
(controlled decel) and raises an alarm; TCP disconnects never kill the
|
(controlled decel) and raises an alarm; TCP disconnects never kill the
|
||||||
process (the deadman fd stays held).
|
process (the deadman fd stays held).
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|
- `GET /` — index page with a live view; `/?action=stream|snapshot` are
|
||||||
|
the mjpg-streamer-compatible aliases (lid camera).
|
||||||
|
- `GET /cam/stream?cam=lid|head` — multipart MJPEG at 1296×972 (2×2
|
||||||
|
Bayer-superpixel demosaic, JPEG q75; `FORGECTRL_STREAM_Q` overrides).
|
||||||
|
- `GET /cam/snapshot?cam=lid|head&res=full|half&q=1..100` — single JPEG,
|
||||||
|
default full 2592×1944 (own MIT bilinear demosaic, output verified
|
||||||
|
against the gfhardware reference grab).
|
||||||
|
- `GET /cam/status` — JSON (running/cam/clients/frames/fps).
|
||||||
|
|
||||||
|
Engine model: one worker owns the V4L2 node persistently (media-ctl /
|
||||||
|
v4l2-ctl configure sequences identical to gfhardware/cam.py, factory
|
||||||
|
exposure/gain/WB, software hflip in the demosaic); starts on demand,
|
||||||
|
full teardown after 10 s idle so gfhardware one-shot grabs still work.
|
||||||
|
The cameras share the hardware video-mux, so stream clients pin the
|
||||||
|
selection — requests for the other camera return 409 until they leave.
|
||||||
|
The per-camera lamp (`pic/lid_led` / `head/white_led`) is raised to
|
||||||
|
`FORGECTRL_LAMP` (default 132) while capturing and restored on idle.
|
||||||
|
|
||||||
|
Bench (2026-08-03, on the board): stream 3.2 fps sustained at 1296×972;
|
||||||
|
full-res snapshot 2.4 s warm / 2.7 s cold (cold includes the pipeline
|
||||||
|
bring-up); camera switch works both ways; two parallel clients share the
|
||||||
|
frame rate; idle teardown observed. **Motion coexistence proven**: X
|
||||||
|
round-trip jogs at F1200 with an active stream — producer stats
|
||||||
|
`clamped 0`, max behind 4.5 ms (the daemon runs at nice +5, single
|
||||||
|
core). Run by hand: `/usr/bin/forgectrl >> /data/forgectrl.log 2>&1 &`
|
||||||
|
(kill before scp when redeploying, text-file-busy).
|
||||||
|
|
||||||
|
Not yet done: LightBurn-side camera consumption (needs an operator
|
||||||
|
session — LightBurn desktop takes USB webcams; the mjpg-streamer alias
|
||||||
|
is the standard bridge-style interface, so test whether the overlay can
|
||||||
|
use it directly or needs a relay), lens calibration / bed alignment, and
|
||||||
|
the deferred 5.6 emulator homing-image smoke (the cloud emulator can now
|
||||||
|
be pointed at live snapshots).
|
||||||
|
|
||||||
## Hardware facts bank (measured)
|
## Hardware facts bank (measured)
|
||||||
|
|
||||||
- SDMA pulse engine: ring free = 128 MiB − 32 KiB gap; script effective
|
- SDMA pulse engine: ring free = 128 MiB − 32 KiB gap; script effective
|
||||||
@@ -293,6 +335,13 @@ hardware I/O — host testing).
|
|||||||
the dependence is weak — with forced flow ΔT = P/(ṁ·c), which
|
the dependence is weak — with forced flow ΔT = P/(ṁ·c), which
|
||||||
carries no absolute-temperature term — but that is reasoning,
|
carries no absolute-temperature term — but that is reasoning,
|
||||||
not measurement.
|
not measurement.
|
||||||
|
- **OBSERVED 2026-08-03 (needs triage):** `/data/glowforge.log`
|
||||||
|
carries, from a prior controller run, a passing check (rise
|
||||||
|
11.4 °C) followed by TWO `COOLANT FLOW FAULT` lines (rise
|
||||||
|
16.5 / 15.9 °C vs the 14.4 limit, dT 11.6). Undated (raw
|
||||||
|
stderr log). Either the pump genuinely faltered or this is the
|
||||||
|
warm-baseline false-positive mode above — check the pump and
|
||||||
|
re-run a supervised verification before trusting the loop.
|
||||||
|
|
||||||
*(Superseded earlier text kept below for context.)*
|
*(Superseded earlier text kept below for context.)*
|
||||||
**Coolant flow verification (first attempt, live-verified both ways).**
|
**Coolant flow verification (first attempt, live-verified both ways).**
|
||||||
@@ -347,8 +396,10 @@ hardware I/O — host testing).
|
|||||||
hall-supervised only.
|
hall-supervised only.
|
||||||
4. **6.5 safety mapping**: door/estop evdev → feed-hold/halt in the
|
4. **6.5 safety mapping**: door/estop evdev → feed-hold/halt in the
|
||||||
backend; underrun → grblHAL alarm; interlock-trip recovery check.
|
backend; underrun → grblHAL alarm; interlock-trip recovery check.
|
||||||
5. **6.6 camera service**: persistent MJPEG (ulfius, forgectrl) — also the
|
5. **6.6 camera service: DONE 2026-08-03, bench-verified** (see "The
|
||||||
natural time for the deferred 5.6 emulator smoke (homing images).
|
camera service" section above). Remaining camera work: LightBurn
|
||||||
|
consumption test (operator), lens calibration / bed alignment, the
|
||||||
|
deferred 5.6 emulator homing-image smoke.
|
||||||
6. **Housekeeping**: ~~pick the controller's remote home~~ **DONE
|
6. **Housekeeping**: ~~pick the controller's remote home~~ **DONE
|
||||||
2026-08-02** — the controller is now the canonical driver repo
|
2026-08-02** — the controller is now the canonical driver repo
|
||||||
`github.com/ScottW514/grblHAL-glowforge` (+ `ScottW514/core` fork;
|
`github.com/ScottW514/grblHAL-glowforge` (+ `ScottW514/core` fork;
|
||||||
|
|||||||
@@ -6,14 +6,26 @@ LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda
|
|||||||
|
|
||||||
SRC_URI = "\
|
SRC_URI = "\
|
||||||
file://CMakeLists.txt \
|
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}"
|
S = "${WORKDIR}"
|
||||||
|
|
||||||
inherit cmake
|
inherit cmake update-rc.d
|
||||||
|
|
||||||
DEPENDS += "libconfig ulfius"
|
DEPENDS += "ulfius jpeg"
|
||||||
RDEPENDS:${PN} = "libconfig ulfius"
|
# 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)
|
project(forgectrl C)
|
||||||
|
|
||||||
set(CMAKE_C_STANDARD 11)
|
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)
|
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"
|
DESCRIPTION = "OpenGlow/ForgeFIRM image for Glowforge"
|
||||||
|
|
||||||
# ForgeFIRM cuts the cloud dependency: drop the Glowforge cloud client
|
# ForgeFIRM cuts the cloud dependency: drop the Glowforge cloud client
|
||||||
# (gfui-client connects to Glowforge's servers) — exactly what ForgeFIRM
|
# (gfui-client connects to Glowforge's servers). Removing it here (override
|
||||||
# replaces with a local grblHAL controller (forgectrl, currently a placeholder;
|
# only; the shared glowforge-image base is untouched) also sidesteps its
|
||||||
# see kas/README.md backlog #5). Removing it here (override only; the shared
|
# do_package failure. Its slot is filled locally by forgectrl (camera MJPEG
|
||||||
# glowforge-image base is untouched) also sidesteps its do_package failure.
|
# service; the grblHAL controller recipe is tracked in kas/README.md
|
||||||
|
# backlog #5).
|
||||||
IMAGE_INSTALL:remove = "gfui-client"
|
IMAGE_INSTALL:remove = "gfui-client"
|
||||||
|
|
||||||
# Camera bring-up tooling: v4l-utils provides v4l2-ctl (the media-ctl binary is
|
# forgectrl: the ForgeFIRM control daemon (camera MJPEG service on :8080).
|
||||||
# already pulled in) for configuring the imx-media pipeline and grabbing frames
|
# v4l-utils provides media-ctl / v4l2-ctl for the imx-media pipeline (also a
|
||||||
# while the gfhardware capture path is ported off the factory NXP V4L2 model.
|
# forgectrl runtime dependency, kept explicit here for bring-up use).
|
||||||
IMAGE_INSTALL:append = " v4l-utils"
|
IMAGE_INSTALL:append = " forgectrl v4l-utils"
|
||||||
|
|||||||
Reference in New Issue
Block a user