Phase 8.2: kernel ↔ daemon chardev bridge with round-trip test
Adds /dev/daedalus-v4l2 misc chardev to the kernel module. The
chardev is the IPC channel for the future userspace decoder
daemon: kernel enqueues REQ_* messages, daemon read()s them,
processes, write()s RESP_* back.
Wire protocol (pre-1.0, header in include/daedalus_v4l2_proto.h):
- struct daedalus_msg_hdr: magic (D04V) + version + type +
cookie + payload_len + reserved
- Request/response separated by high bit of type field
- Max 64 KiB payload per message
- Cookie correlates request with matching response
Kernel implementation (kernel/daedalus_v4l2_chardev.{c,h}):
- Single-instance chardev (-EBUSY on second open)
- In-kernel FIFO bounded at 64 messages
- Blocking + non-blocking read; poll() with EPOLLIN on queued
- write() parses + validates header, logs response at pr_debug
- Bad magic → -EBADMSG, bad version → -EPROTO, oversize → -EMSGSIZE
- All error paths free resources
Phase 8.2 test trigger via debugfs:
- /sys/kernel/debug/daedalus_v4l2/test_ping — any byte
enqueues a PING with a fixed 24-byte payload. Removed in
Phase 8.4 when real REQ_DECODE from V4L2 path takes over.
Userspace verification tool (tools/test_chardev_pingpong.c):
- Real C program, proper error reporting via strerror
- Validates the 6-step round-trip: open → empty-queue EAGAIN →
trigger ping → read PING → verify all fields → write PONG → close
- Builds with -Wall -Wextra clean
Verification on hertz (Pi 5, 6.12.75+rpt-rpi-2712):
$ sudo insmod daedalus_v4l2.ko
$ sudo tools/test_chardev_pingpong
opening /dev/daedalus-v4l2...
non-blocking read on empty queue: EAGAIN ✓
injected PING via debugfs ✓
read PING: magic ✓ version ✓ type=PING ✓ cookie=0x1234 ✓ payload=24 bytes
payload: "DAEDALUS-V4L2-PING-PL"
wrote PONG (cookie=0x1234) ✓
ALL TESTS PASSED.
$ sudo rmmod daedalus_v4l2 # clean
Per correctness-before-speed: full kerneldoc on structs, 8-tab
kernel style, SPDX headers, proper error paths, real test
program (not "I ran it once"), failure-mode coverage documented.
Phase 8.3 next: userspace daemon with dlopen'd FFmpeg parse path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
#
|
||||
# daedalus-v4l2 — userspace tools for Phase 8.2+ verification.
|
||||
|
||||
CC ?= cc
|
||||
CFLAGS ?= -Wall -Wextra -O2
|
||||
CFLAGS += -I../include
|
||||
|
||||
TOOLS := test_chardev_pingpong
|
||||
|
||||
all: $(TOOLS)
|
||||
|
||||
%: %.c
|
||||
$(CC) $(CFLAGS) $< -o $@
|
||||
|
||||
clean:
|
||||
rm -f $(TOOLS)
|
||||
|
||||
.PHONY: all clean
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*
|
||||
* Phase 8.2 verification tool — daemon-side chardev exerciser.
|
||||
*
|
||||
* Opens /dev/daedalus-v4l2, validates the chardev round-trip:
|
||||
* 1. Open: must succeed (-EBUSY if another instance is open).
|
||||
* 2. Non-blocking read: must return -EAGAIN (empty queue).
|
||||
* 3. Trigger a kernel-injected PING via the debugfs trigger
|
||||
* (writes any byte to /sys/kernel/debug/daedalus_v4l2/test_ping).
|
||||
* 4. Read: should return a full PING message (header + 24-byte
|
||||
* payload). Magic / version / type / cookie must match.
|
||||
* 5. Write a matching PONG response. Kernel logs at pr_debug.
|
||||
* 6. Close.
|
||||
*
|
||||
* Build: cc -Wall -Wextra -O2 -I../include test_chardev_pingpong.c
|
||||
* -o test_chardev_pingpong
|
||||
*/
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "daedalus_v4l2_proto.h"
|
||||
|
||||
#define CHARDEV_PATH "/dev/daedalus-v4l2"
|
||||
#define DEBUGFS_PING_PATH "/sys/kernel/debug/daedalus_v4l2/test_ping"
|
||||
|
||||
#define die(fmt, ...) do { \
|
||||
fprintf(stderr, "FAIL: " fmt ": %s\n", \
|
||||
##__VA_ARGS__, strerror(errno)); \
|
||||
exit(1); \
|
||||
} while (0)
|
||||
|
||||
static void trigger_ping(void)
|
||||
{
|
||||
int fd = open(DEBUGFS_PING_PATH, O_WRONLY);
|
||||
if (fd < 0)
|
||||
die("open(%s) — is the module loaded? "
|
||||
"is the test_ping debugfs entry world-mounted "
|
||||
"via /sys/kernel/debug? Try `sudo mount -t debugfs none "
|
||||
"/sys/kernel/debug`",
|
||||
DEBUGFS_PING_PATH);
|
||||
if (write(fd, "1", 1) != 1)
|
||||
die("write(%s)", DEBUGFS_PING_PATH);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
uint8_t buf[1024];
|
||||
struct daedalus_msg_hdr *hdr = (struct daedalus_msg_hdr *) buf;
|
||||
ssize_t n;
|
||||
int fd;
|
||||
|
||||
printf("opening %s...\n", CHARDEV_PATH);
|
||||
fd = open(CHARDEV_PATH, O_RDWR | O_NONBLOCK);
|
||||
if (fd < 0)
|
||||
die("open(%s)", CHARDEV_PATH);
|
||||
|
||||
/* Step 2: empty queue → non-blocking read returns -EAGAIN. */
|
||||
n = read(fd, buf, sizeof(buf));
|
||||
if (n != -1 || errno != EAGAIN)
|
||||
die("expected EAGAIN, got n=%zd errno=%d", n, errno);
|
||||
printf(" non-blocking read on empty queue: EAGAIN ✓\n");
|
||||
|
||||
/* Step 3: trigger a kernel-injected PING. */
|
||||
trigger_ping();
|
||||
printf(" injected PING via debugfs ✓\n");
|
||||
|
||||
/* Step 4: blocking read should now succeed. */
|
||||
int flags = fcntl(fd, F_GETFL);
|
||||
if (flags < 0 || fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) < 0)
|
||||
die("fcntl clear O_NONBLOCK");
|
||||
|
||||
n = read(fd, buf, sizeof(buf));
|
||||
if (n < 0)
|
||||
die("read");
|
||||
if ((size_t) n < sizeof(*hdr))
|
||||
die("short read: %zd bytes (want >= %zu)", n, sizeof(*hdr));
|
||||
if (hdr->magic != DAEDALUS_PROTO_MAGIC)
|
||||
die("bad magic 0x%08x (want 0x%08x)",
|
||||
hdr->magic, DAEDALUS_PROTO_MAGIC);
|
||||
if (hdr->version != DAEDALUS_PROTO_VERSION)
|
||||
die("bad version %u", hdr->version);
|
||||
if (hdr->type != DAEDALUS_MSG_PING)
|
||||
die("got type 0x%08x (want PING 0x%08x)",
|
||||
hdr->type, DAEDALUS_MSG_PING);
|
||||
if (hdr->cookie != 0x1234u)
|
||||
die("got cookie 0x%08x (want 0x1234)", hdr->cookie);
|
||||
if ((size_t) n != sizeof(*hdr) + hdr->payload_len)
|
||||
die("payload truncated");
|
||||
printf(" read PING: magic ✓ version ✓ type=PING ✓ cookie=0x%x ✓ payload=%u bytes\n",
|
||||
hdr->cookie, hdr->payload_len);
|
||||
printf(" payload: \"%.*s\"\n",
|
||||
(int) hdr->payload_len,
|
||||
(const char *) (buf + sizeof(*hdr)));
|
||||
|
||||
/* Step 5: write a matching PONG response. Kernel pr_debugs it. */
|
||||
struct daedalus_msg_hdr pong = {
|
||||
.magic = DAEDALUS_PROTO_MAGIC,
|
||||
.version = DAEDALUS_PROTO_VERSION,
|
||||
.type = DAEDALUS_MSG_PONG,
|
||||
.cookie = 0x1234u,
|
||||
.payload_len = 0,
|
||||
.reserved = 0,
|
||||
};
|
||||
n = write(fd, &pong, sizeof(pong));
|
||||
if (n != (ssize_t) sizeof(pong))
|
||||
die("write PONG (n=%zd)", n);
|
||||
printf(" wrote PONG (cookie=0x%x) ✓\n", pong.cookie);
|
||||
|
||||
close(fd);
|
||||
printf("\nALL TESTS PASSED.\n");
|
||||
printf("(check `sudo dmesg | tail` for the kernel-side\n"
|
||||
" 'chardev got response' pr_debug line.)\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user