mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 08:41:13 -07:00
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.
This commit is contained in:
+10
-4
@@ -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).
|
||||
|
||||
+5
-2
@@ -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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* 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). 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
|
||||
@@ -1,265 +0,0 @@
|
||||
/*
|
||||
* 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 <stddef.h>
|
||||
|
||||
#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 <arm_neon.h>
|
||||
|
||||
/* 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);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
|
||||
/* 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
|
||||
@@ -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
|
||||
@@ -1,333 +0,0 @@
|
||||
/*
|
||||
* 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; 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 <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,\"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 <img> 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[] =
|
||||
"<!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;"
|
||||
"margin-top:8px}a{color:#8cf}button{margin:0 4px}"
|
||||
"#msg{color:#fc6;min-height:1.2em}</style></head><body>"
|
||||
"<h2>ForgeFIRM camera</h2>"
|
||||
"<p><button onclick=\"setCam('lid')\">Lid stream</button>"
|
||||
"<button onclick=\"setCam('head')\">Head stream</button>"
|
||||
"<button onclick=\"peek()\">Head peek</button> "
|
||||
"<a href=\"/cam/snapshot?cam=lid\">lid full</a> | "
|
||||
"<a href=\"/cam/snapshot?cam=head\">head full</a> | "
|
||||
"<a href=\"/cam/status\">status</a></p>"
|
||||
"<div id=\"msg\"></div>"
|
||||
"<img id=\"v\" alt=\"camera stream\">"
|
||||
"<img id=\"p\" alt=\"\" style=\"display:none\">"
|
||||
"<script>"
|
||||
"var cam='lid',retries=0;"
|
||||
"var v=document.getElementById('v'),p=document.getElementById('p'),"
|
||||
"msg=document.getElementById('msg');"
|
||||
"function setCam(c){cam=c;retries=0;msg.textContent='';"
|
||||
"v.src='/cam/stream?cam='+c+'&t='+Date.now();}"
|
||||
"function reload(){v.src='/cam/stream?cam='+cam+'&t='+Date.now();}"
|
||||
/* Retry only when the engine still serves our camera - if another
|
||||
* viewer preempted it, retrying would steal it right back. */
|
||||
"v.onerror=function(){fetch('/cam/status').then(function(r){"
|
||||
"return r.json();}).then(function(s){"
|
||||
"if(s.cam===cam&&retries++<5){msg.textContent='stream retrying...';"
|
||||
"setTimeout(reload,700);}else if(s.cam!==cam){msg.textContent="
|
||||
"'stream taken by another viewer ('+s.cam+') - press a button to resume';}"
|
||||
"else{msg.textContent='stream error - press a stream button to retry';}"
|
||||
"}).catch(function(){msg.textContent="
|
||||
"'service unreachable';});};"
|
||||
"function peek(){"
|
||||
"msg.textContent='head peek (stream pauses a few seconds)...';"
|
||||
"p.style.display='inline';p.onload=function(){msg.textContent='';};"
|
||||
"p.src='/cam/snapshot?cam=head&res=half&t='+Date.now();}"
|
||||
"setCam('lid');"
|
||||
"</script></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;
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
/*
|
||||
* vpu_jpeg.c - hardware JPEG encoding on the i.MX6 CODA960 VPU
|
||||
* Copyright (c) 2026 Scott Wiederhold <s.e.wiederhold@gmail.com>
|
||||
* 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 <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <linux/videodev2.h>
|
||||
#include <poll.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* vpu_jpeg.h - hardware JPEG encoding on the i.MX6 CODA960 VPU
|
||||
* Copyright (c) 2026 Scott Wiederhold <s.e.wiederhold@gmail.com>
|
||||
* 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 <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
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
|
||||
@@ -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 \
|
||||
"
|
||||
|
||||
@@ -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 <measured_C>` / `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 <path>/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. |
|
||||
|
||||
@@ -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 <path>/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/
|
||||
Reference in New Issue
Block a user