Monorepo for Aesthetic.Computer
aesthetic.computer
1#include "udp-client.h"
2
3#include <stdint.h>
4#include <stdio.h>
5#include <stdlib.h>
6#include <string.h>
7#include <unistd.h>
8#include <errno.h>
9#include <netdb.h>
10#include <sys/socket.h>
11#include <arpa/inet.h>
12#include <fcntl.h>
13#include <poll.h>
14#include <time.h>
15
16extern void ac_log(const char *fmt, ...);
17
18// Packet format (binary, little-endian):
19// [1 byte type] [payload...]
20// Type 0x01 = fairy point:
21// [1] [4 float x] [4 float y] [1 handle_len] [N handle_bytes]
22// Type 0x02 = fairy broadcast (from server):
23// [1] [4 float x] [4 float y] [1 handle_len] [N handle_bytes]
24
25#define PKT_FAIRY_SEND 0x01
26#define PKT_FAIRY_RECV 0x02
27
28static uint64_t udp_now_ms(void) {
29 struct timespec ts;
30 clock_gettime(CLOCK_REALTIME, &ts);
31 return (uint64_t)ts.tv_sec * 1000ULL + (uint64_t)ts.tv_nsec / 1000000ULL;
32}
33
34static void json_escape_copy(char *dst, size_t dst_size, const char *src) {
35 size_t out = 0;
36
37 if (!dst || dst_size == 0) return;
38 if (!src) src = "";
39
40 while (*src && out + 1 < dst_size) {
41 unsigned char c = (unsigned char)*src++;
42 if (c == '"' || c == '\\') {
43 if (out + 2 >= dst_size) break;
44 dst[out++] = '\\';
45 dst[out++] = (char)c;
46 } else if (c >= 32 && c < 127) {
47 dst[out++] = (char)c;
48 }
49 }
50
51 dst[out] = '\0';
52}
53
54static int udp_snapshot_identity(
55 ACUdp *udp,
56 int *sock_out,
57 struct sockaddr_in *addr_out,
58 char *handle_out,
59 size_t handle_out_size,
60 char *machine_out,
61 size_t machine_out_size
62) {
63 int sock = -1;
64 int connected = 0;
65
66 if (!udp) return 0;
67
68 pthread_mutex_lock(&udp->mu);
69 sock = udp->sock;
70 connected = udp->connected;
71 if (addr_out) *addr_out = udp->server_addr;
72 if (handle_out && handle_out_size > 0) {
73 strncpy(handle_out, udp->handle, handle_out_size - 1);
74 handle_out[handle_out_size - 1] = 0;
75 }
76 if (machine_out && machine_out_size > 0) {
77 strncpy(machine_out, udp->machine_id, machine_out_size - 1);
78 machine_out[machine_out_size - 1] = 0;
79 }
80 pthread_mutex_unlock(&udp->mu);
81
82 if (sock_out) *sock_out = sock;
83 return connected && sock >= 0;
84}
85
86static void udp_send_json(ACUdp *udp, const char *json) {
87 int sock = -1;
88 struct sockaddr_in server_addr;
89
90 if (!udp || !json || !json[0]) return;
91 if (!udp_snapshot_identity(udp, &sock, &server_addr, NULL, 0, NULL, 0)) return;
92
93 ssize_t sent = sendto(
94 sock,
95 json,
96 strlen(json),
97 0,
98 (struct sockaddr *)&server_addr,
99 sizeof(server_addr)
100 );
101
102 if (sent < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
103 ac_log("[udp] json send failed: %s\n", strerror(errno));
104 }
105}
106
107static void *udp_thread(void *arg) {
108 ACUdp *udp = (ACUdp *)arg;
109 unsigned char buf[256];
110
111 while (udp->thread_running) {
112 if (udp->sock < 0) {
113 usleep(100000); // 100ms
114 continue;
115 }
116
117 // Send pending fairy point
118 pthread_mutex_lock(&udp->mu);
119 int do_send = udp->send_pending;
120 float sx = udp->send_x, sy = udp->send_y;
121 char handle[64];
122 strncpy(handle, udp->handle, sizeof(handle) - 1);
123 handle[sizeof(handle) - 1] = 0;
124 udp->send_pending = 0;
125 pthread_mutex_unlock(&udp->mu);
126
127 if (do_send && udp->connected) {
128 int hlen = (int)strlen(handle);
129 unsigned char pkt[128];
130 pkt[0] = PKT_FAIRY_SEND;
131 memcpy(pkt + 1, &sx, 4);
132 memcpy(pkt + 5, &sy, 4);
133 pkt[9] = (unsigned char)hlen;
134 memcpy(pkt + 10, handle, hlen);
135 int pkt_len = 10 + hlen;
136 sendto(udp->sock, pkt, pkt_len, 0,
137 (struct sockaddr *)&udp->server_addr,
138 sizeof(udp->server_addr));
139 }
140
141 // Recv with short timeout
142 struct pollfd pfd = { .fd = udp->sock, .events = POLLIN };
143 int ret = poll(&pfd, 1, 16); // 16ms = ~60Hz
144 if (ret > 0 && (pfd.revents & POLLIN)) {
145 ssize_t n = recvfrom(udp->sock, buf, sizeof(buf), 0, NULL, NULL);
146 if (n > 0 && buf[0] == PKT_FAIRY_RECV && n >= 10) {
147 float fx, fy;
148 memcpy(&fx, buf + 1, 4);
149 memcpy(&fy, buf + 5, 4);
150
151 pthread_mutex_lock(&udp->mu);
152 if (udp->fairy_count < UDP_MAX_FAIRIES) {
153 udp->fairies[udp->fairy_count].x = fx;
154 udp->fairies[udp->fairy_count].y = fy;
155 udp->fairy_count++;
156 }
157 pthread_mutex_unlock(&udp->mu);
158 }
159 }
160 }
161 return NULL;
162}
163
164ACUdp *udp_create(void) {
165 ACUdp *udp = calloc(1, sizeof(ACUdp));
166 udp->sock = -1;
167 pthread_mutex_init(&udp->mu, NULL);
168 udp->thread_running = 1;
169 pthread_create(&udp->thread, NULL, udp_thread, udp);
170 return udp;
171}
172
173void udp_destroy(ACUdp *udp) {
174 if (!udp) return;
175 udp->thread_running = 0;
176 pthread_join(udp->thread, NULL);
177 if (udp->sock >= 0) close(udp->sock);
178 pthread_mutex_destroy(&udp->mu);
179 free(udp);
180}
181
182void udp_connect(ACUdp *udp, const char *host, int port) {
183 if (!udp) return;
184
185 // Resolve hostname
186 struct hostent *he = gethostbyname(host);
187 if (!he) {
188 ac_log("[udp] DNS resolve failed for %s\n", host);
189 return;
190 }
191
192 memset(&udp->server_addr, 0, sizeof(udp->server_addr));
193 udp->server_addr.sin_family = AF_INET;
194 udp->server_addr.sin_port = htons(port);
195 memcpy(&udp->server_addr.sin_addr, he->h_addr_list[0], he->h_length);
196
197 // Create socket
198 int fd = socket(AF_INET, SOCK_DGRAM, 0);
199 if (fd < 0) {
200 ac_log("[udp] socket() failed: %s\n", strerror(errno));
201 return;
202 }
203
204 // Non-blocking
205 int flags = fcntl(fd, F_GETFL, 0);
206 fcntl(fd, F_SETFL, flags | O_NONBLOCK);
207
208 pthread_mutex_lock(&udp->mu);
209 udp->sock = fd;
210 udp->connected = 1;
211 pthread_mutex_unlock(&udp->mu);
212
213 ac_log("[udp] connected to %s:%d\n", host, port);
214}
215
216void udp_send_fairy(ACUdp *udp, float x, float y) {
217 if (!udp) return;
218 pthread_mutex_lock(&udp->mu);
219 udp->send_x = x;
220 udp->send_y = y;
221 udp->send_pending = 1;
222 pthread_mutex_unlock(&udp->mu);
223}
224
225void udp_set_identity(ACUdp *udp, const char *handle, const char *machine_id) {
226 if (!udp) return;
227 pthread_mutex_lock(&udp->mu);
228 if (handle) {
229 strncpy(udp->handle, handle, sizeof(udp->handle) - 1);
230 udp->handle[sizeof(udp->handle) - 1] = 0;
231 }
232 if (machine_id) {
233 strncpy(udp->machine_id, machine_id, sizeof(udp->machine_id) - 1);
234 udp->machine_id[sizeof(udp->machine_id) - 1] = 0;
235 }
236 pthread_mutex_unlock(&udp->mu);
237}
238
239void udp_send_midi(ACUdp *udp, const char *event, int note, int velocity, int channel, const char *piece) {
240 char handle[64] = "";
241 char machine_id[64] = "";
242 char handle_json[128];
243 char machine_json[128];
244 char piece_json[64];
245 char event_json[32];
246 char json[512];
247
248 if (!udp || !event || !piece) return;
249 if (!udp_snapshot_identity(udp, NULL, NULL, handle, sizeof(handle), machine_id, sizeof(machine_id))) return;
250
251 json_escape_copy(handle_json, sizeof(handle_json), handle);
252 json_escape_copy(machine_json, sizeof(machine_json), machine_id[0] ? machine_id : "unknown");
253 json_escape_copy(piece_json, sizeof(piece_json), piece);
254 json_escape_copy(event_json, sizeof(event_json), event);
255
256 snprintf(
257 json,
258 sizeof(json),
259 "{\"type\":\"notepat:midi\",\"event\":\"%s\",\"note\":%d,\"velocity\":%d,"
260 "\"channel\":%d,\"handle\":\"%s\",\"machineId\":\"%s\",\"piece\":\"%s\",\"ts\":%llu}",
261 event_json,
262 note,
263 velocity,
264 channel,
265 handle_json,
266 machine_json,
267 piece_json,
268 (unsigned long long)udp_now_ms()
269 );
270
271 udp_send_json(udp, json);
272}
273
274void udp_send_midi_heartbeat(ACUdp *udp, const char *piece) {
275 char handle[64] = "";
276 char machine_id[64] = "";
277 char handle_json[128];
278 char machine_json[128];
279 char piece_json[64];
280 char json[512];
281
282 if (!udp || !piece) return;
283 if (!udp_snapshot_identity(udp, NULL, NULL, handle, sizeof(handle), machine_id, sizeof(machine_id))) return;
284
285 json_escape_copy(handle_json, sizeof(handle_json), handle);
286 json_escape_copy(machine_json, sizeof(machine_json), machine_id[0] ? machine_id : "unknown");
287 json_escape_copy(piece_json, sizeof(piece_json), piece);
288
289 snprintf(
290 json,
291 sizeof(json),
292 "{\"type\":\"notepat:midi:heartbeat\",\"handle\":\"%s\",\"machineId\":\"%s\","
293 "\"piece\":\"%s\",\"broadcast\":true,\"ts\":%llu}",
294 handle_json,
295 machine_json,
296 piece_json,
297 (unsigned long long)udp_now_ms()
298 );
299
300 udp_send_json(udp, json);
301}
302
303int udp_poll_fairies(ACUdp *udp, UDPFairy *out, int max) {
304 if (!udp) return 0;
305 pthread_mutex_lock(&udp->mu);
306 int count = udp->fairy_count;
307 if (count > max) count = max;
308 if (count > 0) {
309 memcpy(out, udp->fairies, count * sizeof(UDPFairy));
310 udp->fairy_count = 0;
311 }
312 pthread_mutex_unlock(&udp->mu);
313 return count;
314}