Monorepo for Aesthetic.Computer
aesthetic.computer
1// camera.c — V4L2 camera capture + quirc QR code scanning for AC native
2// Designed for single-frame grab-and-scan (not continuous streaming).
3
4#include "camera.h"
5#include <stdio.h>
6#include <stdlib.h>
7#include <string.h>
8#include <fcntl.h>
9#include <unistd.h>
10#include <errno.h>
11#include <sys/ioctl.h>
12#include <sys/mman.h>
13#include <linux/videodev2.h>
14#include "../lib/quirc/lib/quirc.h"
15
16extern void ac_log(const char *fmt, ...);
17
18// Preferred capture resolution (small = fast QR scan)
19#define CAM_W 640
20#define CAM_H 480
21#define CAM_BUF_COUNT 2
22
23static int xioctl(int fd, int request, void *arg) {
24 int r;
25 do { r = ioctl(fd, request, arg); }
26 while (r == -1 && errno == EINTR);
27 return r;
28}
29
30int camera_open(ACCamera *cam) {
31 memset(cam, 0, sizeof(*cam));
32 cam->fd = -1;
33
34 // Try /dev/video0 through /dev/video3
35 char devpath[32];
36 for (int i = 0; i < 4; i++) {
37 snprintf(devpath, sizeof(devpath), "/dev/video%d", i);
38 int fd = open(devpath, O_RDWR | O_NONBLOCK);
39 if (fd < 0) continue;
40
41 // Check it's a capture device
42 struct v4l2_capability cap;
43 if (xioctl(fd, VIDIOC_QUERYCAP, &cap) < 0 ||
44 !(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) ||
45 !(cap.capabilities & V4L2_CAP_STREAMING)) {
46 close(fd);
47 continue;
48 }
49
50 cam->fd = fd;
51 ac_log("[camera] opened %s (%s)\n", devpath, cap.card);
52 break;
53 }
54
55 if (cam->fd < 0) {
56 snprintf(cam->scan_error, sizeof(cam->scan_error), "no camera found");
57 return -1;
58 }
59
60 // Set format: YUYV (most common for USB webcams), fallback to MJPEG
61 struct v4l2_format fmt = {0};
62 fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
63 fmt.fmt.pix.width = CAM_W;
64 fmt.fmt.pix.height = CAM_H;
65 fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
66 fmt.fmt.pix.field = V4L2_FIELD_NONE;
67
68 if (xioctl(cam->fd, VIDIOC_S_FMT, &fmt) < 0) {
69 // Try MJPEG as fallback
70 fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
71 if (xioctl(cam->fd, VIDIOC_S_FMT, &fmt) < 0) {
72 snprintf(cam->scan_error, sizeof(cam->scan_error),
73 "can't set camera format");
74 close(cam->fd);
75 cam->fd = -1;
76 return -1;
77 }
78 }
79
80 cam->width = fmt.fmt.pix.width;
81 cam->height = fmt.fmt.pix.height;
82 ac_log("[camera] format: %dx%d pixfmt=0x%08x\n",
83 cam->width, cam->height, fmt.fmt.pix.pixelformat);
84
85 // Request buffers
86 struct v4l2_requestbuffers req = {0};
87 req.count = CAM_BUF_COUNT;
88 req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
89 req.memory = V4L2_MEMORY_MMAP;
90 if (xioctl(cam->fd, VIDIOC_REQBUFS, &req) < 0 || req.count < 1) {
91 snprintf(cam->scan_error, sizeof(cam->scan_error), "buffer request failed");
92 close(cam->fd);
93 cam->fd = -1;
94 return -1;
95 }
96 cam->buffer_count = (int)req.count;
97 if (cam->buffer_count > 4) cam->buffer_count = 4;
98
99 // Map buffers and queue them
100 for (int i = 0; i < cam->buffer_count; i++) {
101 struct v4l2_buffer buf = {0};
102 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
103 buf.memory = V4L2_MEMORY_MMAP;
104 buf.index = i;
105 if (xioctl(cam->fd, VIDIOC_QUERYBUF, &buf) < 0) {
106 snprintf(cam->scan_error, sizeof(cam->scan_error), "querybuf failed");
107 close(cam->fd);
108 cam->fd = -1;
109 return -1;
110 }
111 cam->buffers[i] = mmap(NULL, buf.length,
112 PROT_READ | PROT_WRITE, MAP_SHARED,
113 cam->fd, buf.m.offset);
114 if (cam->buffers[i] == MAP_FAILED) {
115 cam->buffers[i] = NULL;
116 snprintf(cam->scan_error, sizeof(cam->scan_error), "mmap failed");
117 close(cam->fd);
118 cam->fd = -1;
119 return -1;
120 }
121 xioctl(cam->fd, VIDIOC_QBUF, &buf);
122 }
123
124 // Start streaming
125 enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
126 if (xioctl(cam->fd, VIDIOC_STREAMON, &type) < 0) {
127 snprintf(cam->scan_error, sizeof(cam->scan_error), "streamon failed");
128 close(cam->fd);
129 cam->fd = -1;
130 return -1;
131 }
132 cam->streaming = 1;
133
134 // Allocate grayscale buffer
135 cam->gray = malloc(cam->width * cam->height);
136 if (!cam->gray) {
137 snprintf(cam->scan_error, sizeof(cam->scan_error), "gray alloc failed");
138 camera_close(cam);
139 return -1;
140 }
141
142 // Allocate display buffer (mutex-protected copy for main thread rendering)
143 cam->display = malloc(cam->width * cam->height);
144 if (!cam->display) {
145 snprintf(cam->scan_error, sizeof(cam->scan_error), "display alloc failed");
146 camera_close(cam);
147 return -1;
148 }
149 pthread_mutex_init(&cam->display_mu, NULL);
150 cam->display_ready = 0;
151
152 ac_log("[camera] ready: %dx%d, %d buffers\n",
153 cam->width, cam->height, cam->buffer_count);
154 return 0;
155}
156
157void camera_close(ACCamera *cam) {
158 if (cam->streaming && cam->fd >= 0) {
159 enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
160 xioctl(cam->fd, VIDIOC_STREAMOFF, &type);
161 cam->streaming = 0;
162 }
163 // Unmap buffers (we don't track individual sizes, but munmap is safe
164 // since we queried them — in practice the kernel handles cleanup on close)
165 if (cam->fd >= 0) {
166 close(cam->fd);
167 cam->fd = -1;
168 }
169 if (cam->gray) {
170 free(cam->gray);
171 cam->gray = NULL;
172 }
173 if (cam->display) {
174 pthread_mutex_destroy(&cam->display_mu);
175 free(cam->display);
176 cam->display = NULL;
177 }
178 cam->gray_ready = 0;
179 cam->display_ready = 0;
180}
181
182int camera_grab(ACCamera *cam) {
183 if (cam->fd < 0 || !cam->streaming || !cam->gray) return -1;
184
185 struct v4l2_buffer buf = {0};
186 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
187 buf.memory = V4L2_MEMORY_MMAP;
188
189 if (xioctl(cam->fd, VIDIOC_DQBUF, &buf) < 0) {
190 if (errno == EAGAIN) return -1; // no frame ready yet
191 snprintf(cam->scan_error, sizeof(cam->scan_error),
192 "dqbuf failed: %s", strerror(errno));
193 return -1;
194 }
195
196 // Convert YUYV to grayscale (Y channel = every other byte)
197 uint8_t *src = cam->buffers[buf.index];
198 int pixels = cam->width * cam->height;
199 for (int i = 0; i < pixels; i++) {
200 cam->gray[i] = src[i * 2]; // Y from YUYV
201 }
202 cam->gray_ready = 1;
203
204 // Copy to display buffer for main thread rendering
205 if (cam->display) {
206 pthread_mutex_lock(&cam->display_mu);
207 memcpy(cam->display, cam->gray, cam->width * cam->height);
208 cam->display_ready = 1;
209 pthread_mutex_unlock(&cam->display_mu);
210 }
211
212 // Re-queue the buffer
213 xioctl(cam->fd, VIDIOC_QBUF, &buf);
214 return 0;
215}
216
217int camera_scan_qr(ACCamera *cam) {
218 if (!cam->gray || !cam->gray_ready) return 0;
219
220 struct quirc *qr = quirc_new();
221 if (!qr) return 0;
222
223 if (quirc_resize(qr, cam->width, cam->height) < 0) {
224 quirc_destroy(qr);
225 return 0;
226 }
227
228 // Copy grayscale into quirc's image buffer
229 int w, h;
230 uint8_t *image = quirc_begin(qr, &w, &h);
231 int copy_w = cam->width < w ? cam->width : w;
232 int copy_h = cam->height < h ? cam->height : h;
233 for (int y = 0; y < copy_h; y++) {
234 memcpy(image + y * w, cam->gray + y * cam->width, copy_w);
235 }
236 quirc_end(qr);
237
238 int count = quirc_count(qr);
239 cam->scan_result[0] = 0;
240
241 for (int i = 0; i < count; i++) {
242 struct quirc_code code;
243 struct quirc_data data;
244 quirc_extract(qr, i, &code);
245 if (quirc_decode(&code, &data) == QUIRC_SUCCESS) {
246 int len = data.payload_len;
247 if (len > (int)sizeof(cam->scan_result) - 1)
248 len = sizeof(cam->scan_result) - 1;
249 memcpy(cam->scan_result, data.payload, len);
250 cam->scan_result[len] = 0;
251 ac_log("[camera] QR decoded: %d bytes\n", len);
252 break; // take first successful decode
253 }
254 }
255
256 int found = (cam->scan_result[0] != 0) ? 1 : 0;
257 quirc_destroy(qr);
258 return found;
259}