Release 0.12.0

This commit is contained in:
matze19999 committed 2026-10-06 22:01:38 +02:00
1 parent f74a658097
commit 7f6eeab39e
48 files changed
+3617 -1266

No files matched your search

+215 -59
View File
@@ -5,67 +5,223 @@
#endif
/* Independent raw LED subscription: bounded, nonblocking, before receiver
* color correction. Old result and newer data envelopes are both accepted. */
typedef struct {int fd,stage;uint64_t deadline,retry_at,updated;unsigned sequence,count,revision,layout_revision;LedBlend *mapping;char *data,*request;size_t used,sent,length;unsigned char colors[16][3];bool valid;} ReceiverStream;
static ReceiverStream stream={.fd=-1};
static void stream_close(uint64_t now){if(stream.fd>=0)close(stream.fd);free(stream.mapping);free(stream.data);free(stream.request);stream=(ReceiverStream){.fd=-1,.retry_at=now+5000};}
static bool stream_queue(const char *line){
if(stream.fd<0 || stream.request)return false;
stream.request=strdup(line);if(!stream.request)return false;stream.length=strlen(line);stream.sent=0;return true;
#include "runtime_states.h"
typedef struct {
int fd;
StreamStage stage;
uint64_t deadline, retry_at, updated;
unsigned sequence, count, revision, layout_revision;
LedBlend *mapping;
char *data, *request;
size_t used, sent, length;
unsigned char colors[16][3];
bool valid;
} ReceiverStream;
static ReceiverStream stream = {.fd = -1};
static void stream_close(uint64_t now) {
if (stream.fd >= 0)
close(stream.fd);
free(stream.mapping);
free(stream.data);
free(stream.request);
stream = (ReceiverStream){.fd = -1, .retry_at = now + 5000};
}
static void stream_start(uint64_t now){
stream.fd=target_open(&target);if(stream.fd<0){stream.retry_at=now+5000;return;}
stream.data=malloc(262145);if(!stream.data){stream_close(now);return;}stream.stage=1;stream.deadline=now+3000;stream.revision=calibration_revision();stream.layout_revision=RECEIVER_LAYOUT_REVISION;
static bool stream_queue(const char *line) {
if (stream.fd < 0 || stream.request)
return false;
stream.request = strdup(line);
if (!stream.request)
return false;
stream.length = strlen(line);
stream.sent = 0;
return true;
}
static bool stream_colors(PlaybackJson *j,void *root,uint64_t now){
void *body=playback_field(j,root,"result");if(!body)body=playback_field(j,root,"data");
void *leds=playback_field(j,body,"leds");
if(!leds || j->type(leds)!=5)return false;
if(j->length(leds)!=(size_t)stream.count*3){stream.revision=0;return false;}
uint64_t sums[16][3]={{0}},weights[16]={0};
for(unsigned i=0;i<stream.count;i++){
unsigned values[3];for(unsigned c=0;c<3;c++){void *v=j->at(leds,i*3+c);if(!v || j->type(v)!=3)return false;char *end;long n=strtol(j->string(v),&end,10);if(*end || n<0 || n>255)return false;values[c]=(unsigned)n;}
LedBlend b=stream.mapping[i];unsigned w[2]={65535-b.weight,b.weight},r[2]={b.a,b.b};
for(unsigned k=0;k<2;k++){weights[r[k]]+=w[k];for(unsigned c=0;c<3;c++)sums[r[k]][c]+=(uint64_t)values[c]*w[k];}
}
unsigned char mean[3]={0};uint64_t total=0,all[3]={0};
for(unsigned r=0;r<16;r++){total+=weights[r];for(unsigned c=0;c<3;c++)all[c]+=sums[r][c];}
if(!total)return false;
for(unsigned c=0;c<3;c++)mean[c]=(unsigned char)(all[c]/total);
for(unsigned r=0;r<16;r++)for(unsigned c=0;c<3;c++)stream.colors[r][c]=weights[r]?(unsigned char)(sums[r][c]/weights[r]):mean[c];
stream.valid=true;stream.updated=now;stream.sequence++;return true;
static void stream_start(uint64_t now) {
stream.fd = target_open(&target);
if (stream.fd < 0) {
stream.retry_at = now + 5000;
return;
}
stream.data = malloc(262145);
if (!stream.data) {
stream_close(now);
return;
}
stream.stage = STREAM_CONNECTING;
stream.deadline = now + 3000;
stream.revision = calibration_revision();
stream.layout_revision = RECEIVER_LAYOUT_REVISION;
}
static void stream_tick(PlaybackJson *j,uint64_t now,bool enabled){
if(!enabled){if(stream.fd>=0)stream_close(now);return;}
if(stream.fd>=0 && (stream.revision!=calibration_revision() || stream.layout_revision!=RECEIVER_LAYOUT_REVISION)){stream_close(now);stream.retry_at=now;}
if(stream.fd<0){if(now>=stream.retry_at)stream_start(now);return;}
if(stream.stage<4 && now>=stream.deadline){stream_close(now);return;}
if(stream.stage==1){
struct pollfd p={stream.fd,POLLOUT,0};int error=0;socklen_t size=sizeof(error);if(poll(&p,1,0)<=0)return;
if(getsockopt(stream.fd,SOL_SOCKET,SO_ERROR,&error,&size)!=0 || error){stream_close(now);return;}
char line[384];if(target.token[0]){snprintf(line,sizeof(line),"{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n",target.token);stream.stage=2;}
else {snprintf(line,sizeof(line),"{\"command\":\"serverinfo\"}\n");stream.stage=3;}
if(!stream_queue(line)){stream_close(now);return;}
}
if(stream.request){
ssize_t n=send(stream.fd,stream.request+stream.sent,stream.length-stream.sent,MSG_NOSIGNAL);
if(n<0 && (errno==EAGAIN || errno==EWOULDBLOCK || errno==EINTR))return;
if(n<=0){stream_close(now);return;}stream.sent+=(size_t)n;
if(stream.sent==stream.length){free(stream.request);stream.request=NULL;}
}
if(stream.used==262144){stream_close(now);return;}
size_t available=262144-stream.used;if(available>65536)available=65536;
ssize_t n=recv(stream.fd,stream.data+stream.used,available,0);
if(n<0 && (errno==EAGAIN || errno==EWOULDBLOCK || errno==EINTR))return;
if(n<=0){stream_close(now);return;}stream.used+=(size_t)n;stream.data[stream.used]=0;
for(unsigned messages=0;messages<8;messages++){
char *end=strchr(stream.data,'\n');if(!end)break;*end=0;void *root=j->parse(stream.data);
if(stream.stage==2){if(!playback_true(j,root,"success")){if(root)j->put(root);stream_close(now);return;}stream.stage=3;stream_queue("{\"command\":\"serverinfo\"}\n");stream.deadline=now+3000;}
else if(stream.stage==3){
LedBlend *mapping=NULL;unsigned count=0;if(!layout_from_json(stream.data,&mapping,&count)){if(root)j->put(root);stream_close(now);return;}
stream.mapping=mapping;stream.count=count;stream.stage=4;stream_queue("{\"command\":\"ledcolors\",\"subcommand\":\"ledstream-start\",\"interval\":100}\n");
}else if(playback_is(j,root,"command","ledcolors-ledstream-update"))stream_colors(j,root,now);
if(root)j->put(root);
size_t consumed=(size_t)(end-stream.data)+1;memmove(stream.data,stream.data+consumed,stream.used-consumed);stream.used-=consumed;stream.data[stream.used]=0;
}
static bool stream_colors(PlaybackJson *j, void *root, uint64_t now) {
void *body = playback_field(j, root, "result");
if (!body)
body = playback_field(j, root, "data");
void *leds = playback_field(j, body, "leds");
if (!leds || j->type(leds) != 5)
return false;
if (j->length(leds) != (size_t)stream.count * 3) {
stream.revision = 0;
return false;
}
uint64_t sums[16][3] = {{0}}, weights[16] = {0};
for (unsigned i = 0; i < stream.count; i++) {
unsigned values[3];
for (unsigned c = 0; c < 3; c++) {
void *v = j->at(leds, i * 3 + c);
if (!v || j->type(v) != 3)
return false;
char *end;
long n = strtol(j->string(v), &end, 10);
if (*end || n < 0 || n > 255)
return false;
values[c] = (unsigned)n;
}
LedBlend b = stream.mapping[i];
unsigned w[2] = {65535 - b.weight, b.weight}, r[2] = {b.a, b.b};
for (unsigned k = 0; k < 2; k++) {
weights[r[k]] += w[k];
for (unsigned c = 0; c < 3; c++)
sums[r[k]][c] += (uint64_t)values[c] * w[k];
}
}
unsigned char mean[3] = {0};
uint64_t total = 0, all[3] = {0};
for (unsigned r = 0; r < 16; r++) {
total += weights[r];
for (unsigned c = 0; c < 3; c++)
all[c] += sums[r][c];
}
if (!total)
return false;
for (unsigned c = 0; c < 3; c++)
mean[c] = (unsigned char)(all[c] / total);
for (unsigned r = 0; r < 16; r++)
for (unsigned c = 0; c < 3; c++)
stream.colors[r][c] = weights[r] ? (unsigned char)(sums[r][c] / weights[r]) : mean[c];
stream.valid = true;
stream.updated = now;
stream.sequence++;
return true;
}
static void stream_tick(PlaybackJson *j, uint64_t now, bool enabled) {
if (!enabled) {
if (stream.fd >= 0)
stream_close(now);
return;
}
if (stream.fd >= 0 && (stream.revision != calibration_revision() ||
stream.layout_revision != RECEIVER_LAYOUT_REVISION)) {
stream_close(now);
stream.retry_at = now;
}
if (stream.fd < 0) {
if (now >= stream.retry_at)
stream_start(now);
return;
}
if (stream.stage < STREAM_SUBSCRIBED && now >= stream.deadline) {
stream_close(now);
return;
}
if (stream.stage == STREAM_CONNECTING) {
struct pollfd p = {stream.fd, POLLOUT, 0};
int error = 0;
socklen_t size = sizeof(error);
if (poll(&p, 1, 0) <= 0)
return;
if (getsockopt(stream.fd, SOL_SOCKET, SO_ERROR, &error, &size) != 0 || error) {
stream_close(now);
return;
}
char line[384];
if (target.token[0]) {
snprintf(line, sizeof(line),
"{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n",
target.token);
stream.stage = STREAM_AUTHENTICATING;
} else {
snprintf(line, sizeof(line), "{\"command\":\"serverinfo\"}\n");
stream.stage = STREAM_LAYOUT;
}
if (!stream_queue(line)) {
stream_close(now);
return;
}
}
if (stream.request) {
ssize_t n = send(stream.fd, stream.request + stream.sent, stream.length - stream.sent,
MSG_NOSIGNAL);
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR))
return;
if (n <= 0) {
stream_close(now);
return;
}
stream.sent += (size_t)n;
if (stream.sent == stream.length) {
free(stream.request);
stream.request = NULL;
}
}
/* Drain complete buffered replies even when recv would return EAGAIN.
* The per-tick budget applies independently of incoming network traffic. */
if (stream.used == 262144 && !strchr(stream.data, '\n')) {
stream_close(now);
return;
}
size_t available = 262144 - stream.used;
if (available > 65536)
available = 65536;
ssize_t n = recv(stream.fd, stream.data + stream.used, available, 0);
if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
stream_close(now);
return;
}
if (n == 0 && available) {
stream_close(now);
return;
}
if (n > 0)
stream.used += (size_t)n;
stream.data[stream.used] = 0;
for (unsigned messages = 0; messages < 8; messages++) {
char *end = strchr(stream.data, '\n');
if (!end)
break;
*end = 0;
void *root = j->parse(stream.data);
if (stream.stage == STREAM_AUTHENTICATING) {
if (!playback_true(j, root, "success")) {
if (root)
j->put(root);
stream_close(now);
return;
}
stream.stage = STREAM_LAYOUT;
stream_queue("{\"command\":\"serverinfo\"}\n");
stream.deadline = now + 3000;
} else if (stream.stage == STREAM_LAYOUT) {
LedBlend *mapping = NULL;
unsigned count = 0;
if (!layout_from_json(stream.data, &mapping, &count)) {
if (root)
j->put(root);
stream_close(now);
return;
}
stream.mapping = mapping;
stream.count = count;
stream.stage = STREAM_SUBSCRIBED;
stream_queue("{\"command\":\"ledcolors\",\"subcommand\":\"ledstream-start\","
"\"interval\":100}\n");
} else if (playback_is(j, root, "command", "ledcolors-ledstream-update"))
stream_colors(j, root, now);
if (root)
j->put(root);
size_t consumed = (size_t)(end - stream.data) + 1;
memmove(stream.data, stream.data + consumed, stream.used - consumed);
stream.used -= consumed;
stream.data[stream.used] = 0;
}
}
#endif