mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -07:00
BRINGUP describes the present: the 54-test catalog and its seven-test always core, the tier counts, the shipped low-temperature gates, the density floor ($35 = 10), the two local core commits, the ffboot env write, the aa-offset route, the current bench image, and the bench measurements the audit asks for (pooled into the next session). The workstation shell notes and every em dash are gone. forgetest: the takeover waits for the cloud client too (found by its command line); the unauthenticated /boot probe names the endpoint's parameter; the UI prose is American English. Recipes: forgetest fetches its package directory and init script only and drops __pycache__ at unpack; the dev image no longer re-adds forgectrl; the release image's remove list drops the gfui-client the BSP no longer has; the platform identity strips the kernel's local-version hash from the modules directory name, so a re-patched kernel keeps its fingerprints. grblhal restart is stop then start. release.sh --dev packs the dev image. fixture.sh refuses a readable env file. Bench tools: the live-fire drills measure the lid-IR baseline before every run and point at the fire-watch thresholds the engine reads; one thermistor conversion (gfbench.degc) serves every drill; the six dated measurement records leave the tool directory; feeder.c names the two sysfs writes its caller makes. Host tests: forgetest 258 pass; the coverage lint reports no uncovered path across 54 tests. Acceptance: forgectrl.auth covers the /boot probe; update.* cover ffboot and the manifest identity; the runbook and bench-tool changes have no catalog consequence.
166 lines
5.4 KiB
C
166 lines
5.4 KiB
C
/*
|
|
* feeder.c - prove no-underrun continuous live feeding of the glowforge.ko
|
|
* SDMA pulse ring under load.
|
|
*
|
|
* Streams NOP (0x00) pulse bytes to /dev/glowforge, pacing by wall clock to
|
|
* hold a bounded queue depth (like a real grblHAL backend would),
|
|
* with the deadman flock held. The caller locks the motors and the laser
|
|
* latch first: write 1 to /sys/glowforge/cnc/motor_lock and 1 to
|
|
* /sys/glowforge/cnc/laser_latch, and read both back before starting.
|
|
*
|
|
* Reports: feed statistics, worst scheduling stall, ENOMEM count, and the
|
|
* final driver state (expect "running" throughout, "underrun" only after
|
|
* the deliberate starve at the end).
|
|
*
|
|
* Usage: feeder <step_freq_hz> <duration_s> <depth_ms>
|
|
*/
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/file.h>
|
|
#include <sys/stat.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#include <sched.h>
|
|
|
|
#define DEV "/dev/glowforge"
|
|
#define CNC "/sys/glowforge/cnc/"
|
|
|
|
static double now_s(void)
|
|
{
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return ts.tv_sec + ts.tv_nsec / 1e9;
|
|
}
|
|
|
|
static int wr_attr(const char *attr, const char *val)
|
|
{
|
|
char path[128];
|
|
int fd, ret;
|
|
snprintf(path, sizeof path, CNC "%s", attr);
|
|
fd = open(path, O_WRONLY);
|
|
if (fd < 0) return -1;
|
|
ret = (int)write(fd, val, strlen(val));
|
|
close(fd);
|
|
return ret < 0 ? -1 : 0;
|
|
}
|
|
|
|
static int rd_attr(const char *attr, char *buf, size_t len)
|
|
{
|
|
char path[128];
|
|
int fd;
|
|
ssize_t n;
|
|
snprintf(path, sizeof path, CNC "%s", attr);
|
|
fd = open(path, O_RDONLY);
|
|
if (fd < 0) return -1;
|
|
n = read(fd, buf, len - 1);
|
|
close(fd);
|
|
if (n < 0) return -1;
|
|
while (n > 0 && (buf[n-1] == '\n')) n--;
|
|
buf[n] = 0;
|
|
return 0;
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
long freq = argc > 1 ? atol(argv[1]) : 10000;
|
|
long duration = argc > 2 ? atol(argv[2]) : 60;
|
|
long depth_ms = argc > 3 ? atol(argv[3]) : 150;
|
|
static unsigned char chunk[8192]; /* NOP bytes: no step, no laser */
|
|
char state[32], u0[16], u1[16];
|
|
long long target, enqueued = 0, enomem = 0, writes = 0;
|
|
double t0, t_end, last = 0, max_stall = 0, max_wr = 0;
|
|
long depth_bytes = (long)((double)freq * depth_ms / 1000.0);
|
|
int fd;
|
|
|
|
memset(chunk, 0, sizeof chunk);
|
|
|
|
/* SCHED_FIFO like a real feeder; fall back silently if not permitted */
|
|
struct sched_param sp = { .sched_priority = 10 };
|
|
sched_setscheduler(0, SCHED_FIFO, &sp);
|
|
|
|
fd = open(DEV, O_WRONLY);
|
|
if (fd < 0) { perror("open " DEV); return 1; }
|
|
if (flock(fd, LOCK_EX) != 0) { perror("flock"); return 1; }
|
|
|
|
char fbuf[16];
|
|
snprintf(fbuf, sizeof fbuf, "%ld", freq);
|
|
if (wr_attr("step_freq", fbuf)) { perror("step_freq"); return 1; }
|
|
wr_attr("streaming", "1");
|
|
rd_attr("underruns", u0, sizeof u0);
|
|
|
|
lseek(fd, 0, SEEK_SET); /* clear data + position */
|
|
|
|
/* Prefill one queue depth, then start the run */
|
|
while (enqueued < depth_bytes) {
|
|
long n = depth_bytes - enqueued;
|
|
if (n > (long)sizeof chunk) n = sizeof chunk;
|
|
if (write(fd, chunk, n) < 0) { perror("prefill"); return 1; }
|
|
enqueued += n;
|
|
}
|
|
if (wr_attr("run", "1")) { perror("run"); return 1; }
|
|
|
|
t0 = now_s();
|
|
t_end = t0 + duration;
|
|
last = t0;
|
|
printf("feeding: %ld Hz for %ld s, queue depth %ld ms (%ld bytes)\n",
|
|
freq, duration, depth_ms, depth_bytes);
|
|
|
|
while (1) {
|
|
double t = now_s();
|
|
if (t >= t_end) break;
|
|
if (t - last > max_stall) max_stall = t - last;
|
|
last = t;
|
|
|
|
/* wall-clock pacing: keep enqueued = consumed-so-far + depth */
|
|
target = (long long)((t - t0) * freq) + depth_bytes;
|
|
while (enqueued < target) {
|
|
long n = (long)(target - enqueued);
|
|
if (n > (long)sizeof chunk) n = sizeof chunk;
|
|
double w0 = now_s(), w1;
|
|
if (write(fd, chunk, n) < 0) {
|
|
if (errno == ENOMEM) { enomem++; break; }
|
|
perror("write"); return 1;
|
|
}
|
|
w1 = now_s();
|
|
if (w1 - w0 > max_wr) max_wr = w1 - w0;
|
|
enqueued += n;
|
|
writes++;
|
|
}
|
|
|
|
struct timespec ts = { 0, 20 * 1000 * 1000 }; /* 20 ms */
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
|
|
rd_attr("state", state, sizeof state);
|
|
printf("after %ld s: state=%s enqueued=%lld writes=%lld enomem=%lld\n",
|
|
duration, state, enqueued, writes, enomem);
|
|
printf("max loop stall: %.1f ms, max write latency: %.1f ms\n",
|
|
max_stall * 1e3, max_wr * 1e3);
|
|
int fed_ok = (strcmp(state, "running") == 0) && enomem == 0;
|
|
|
|
/* Deliberate starve: stop feeding, expect a clean underrun */
|
|
double ts0 = now_s();
|
|
do {
|
|
struct timespec ts = { 0, 50 * 1000 * 1000 };
|
|
nanosleep(&ts, NULL);
|
|
rd_attr("state", state, sizeof state);
|
|
} while (strcmp(state, "running") == 0 && now_s() - ts0 < 10 + depth_ms / 1000.0);
|
|
rd_attr("underruns", u1, sizeof u1);
|
|
printf("after starve: state=%s underruns %s -> %s\n", state, u0, u1);
|
|
int starve_ok = strcmp(state, "underrun") == 0;
|
|
|
|
wr_attr("stop", "1"); /* acknowledge */
|
|
lseek(fd, 0, SEEK_SET);
|
|
wr_attr("streaming", "0");
|
|
flock(fd, LOCK_UN);
|
|
close(fd);
|
|
|
|
printf("%s: feed %s, starve->underrun %s\n",
|
|
(fed_ok && starve_ok) ? "PASS" : "FAIL",
|
|
fed_ok ? "clean" : "FAILED", starve_ok ? "detected" : "MISSED");
|
|
return (fed_ok && starve_ok) ? 0 : 1;
|
|
}
|