ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
sse.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file sse.cpp
6 * @brief Server-Sent Events connection pool implementation.
7 */
8
9#include "sse.h"
11#include <stdio.h>
12#include <string.h>
13
15
17{
18 for (int i = 0; i < MAX_SSE_CONNS; i++)
19 {
20 pc_sse_pool[i] = {};
21 pc_sse_pool[i].pc_sse_id = (uint8_t)i;
22 }
23}
24
25SseConn *pc_sse_alloc(uint8_t slot_id, const char *path)
26{
27 for (int i = 0; i < MAX_SSE_CONNS; i++)
28 {
29 if (!pc_sse_pool[i].active)
30 {
31 pc_sse_pool[i] = {};
32 pc_sse_pool[i].pc_sse_id = (uint8_t)i;
33 pc_sse_pool[i].slot_id = slot_id;
34 pc_sse_pool[i].active = true;
35 strncpy(pc_sse_pool[i].path, path, MAX_PATH_LEN - 1);
36 pc_sse_pool[i].path[MAX_PATH_LEN - 1] = '\0';
37 return &pc_sse_pool[i];
38 }
39 }
40 return nullptr;
41}
42
43SseConn *pc_sse_find(uint8_t slot_id)
44{
45 for (int i = 0; i < MAX_SSE_CONNS; i++)
46 {
47 if (pc_sse_pool[i].active && pc_sse_pool[i].slot_id == slot_id)
48 {
49 return &pc_sse_pool[i];
50 }
51 }
52 return nullptr;
53}
54
55void pc_sse_free(uint8_t slot_id)
56{
57 for (int i = 0; i < MAX_SSE_CONNS; i++)
58 {
59 if (pc_sse_pool[i].active && pc_sse_pool[i].slot_id == slot_id)
60 {
61 pc_sse_pool[i] = {};
62 pc_sse_pool[i].pc_sse_id = (uint8_t)i;
63 return;
64 }
65 }
66}
67
68// Append `len` bytes of `src` at *pos if the whole record still leaves room for a trailing NUL (content must
69// fit in n-1). Returns false the moment it would not fit, so a record that overflows reports 0 rather than a
70// truncated frame. memcpy of a known-length span, no format parsing.
71static inline bool sse_append(char *buf, size_t n, size_t *pos, const char *src, size_t len)
72{
73 if (*pos + len > n - 1)
74 {
75 return false;
76 }
77 memcpy(buf + *pos, src, len);
78 *pos += len;
79 return true;
80}
81
82int pc_sse_format(char *buf, size_t n, const char *data, const char *event, const char *id)
83{
84 if (!data || n == 0)
85 {
86 return 0;
87 }
88
89 // WHATWG event-stream field order: event, then id, then data (blank line terminates the record). A
90 // branchless memcpy framer - fixed prefixes + strnlen/memcpy of each value + the terminators - instead of
91 // three snprintf("%s") calls; ~an order of magnitude cheaper on the Xtensa vsnprintf path, which matters
92 // for a high-rate broadcast fan-out (many subscribers). Byte-identical output (test_sse_format).
93 // Bounded lengths (strnlen, cap n): a field can never exceed the output buffer (an over-long value makes
94 // the append fail and the record report 0), and strnlen never reads past `n` if a value is unterminated.
95 size_t pos = 0;
96 if (event && (!sse_append(buf, n, &pos, "event: ", 7) || !sse_append(buf, n, &pos, event, strnlen(event, n)) ||
97 !sse_append(buf, n, &pos, "\n", 1)))
98 {
99 return 0;
100 }
101 if (id && (!sse_append(buf, n, &pos, "id: ", 4) || !sse_append(buf, n, &pos, id, strnlen(id, n)) ||
102 !sse_append(buf, n, &pos, "\n", 1)))
103 {
104 return 0;
105 }
106 if (!sse_append(buf, n, &pos, "data: ", 6) || !sse_append(buf, n, &pos, data, strnlen(data, n)) ||
107 !sse_append(buf, n, &pos, "\n\n", 2))
108 {
109 return 0;
110 }
111
112 buf[pos] = '\0'; // pos <= n-1 by construction, so the NUL always fits
113 return (int)pos;
114}
115
116bool pc_sse_write(SseConn *sse, const char *data, const char *event, const char *id)
117{
118 if (!pc_conn_active(sse->slot_id))
119 {
120 return false;
121 }
122
123 char buf[SSE_BUF_SIZE];
124 int pos = pc_sse_format(buf, sizeof(buf), data, event, id);
125 if (pos <= 0)
126 {
127 return false;
128 }
129
130 pc_conn_send(sse->slot_id, buf, (u16_t)pos);
131 return true;
132}
#define MAX_SSE_CONNS
Definition c2_defaults.h:75
#define MAX_PATH_LEN
Maximum URL path length (including leading /).
#define SSE_BUF_SIZE
Output buffer size in bytes for a single SSE event.
SseConn * pc_sse_alloc(uint8_t slot_id, const char *path)
Allocate an SseConn and bind it to a TCP slot.
Definition sse.cpp:25
bool pc_sse_write(SseConn *sse, const char *data, const char *event, const char *id)
Write one SSE event record to a client.
Definition sse.cpp:116
void pc_sse_init()
Initialize all SSE pool slots to inactive.
Definition sse.cpp:16
int pc_sse_format(char *buf, size_t n, const char *data, const char *event, const char *id)
Format one SSE event record into a caller buffer (no transport).
Definition sse.cpp:82
SseConn pc_sse_pool[MAX_SSE_CONNS]
Pool of SSE connection state, one per MAX_SSE_CONNS.
Definition sse.cpp:14
void pc_sse_free(uint8_t slot_id)
Free the SseConn associated with a TCP slot.
Definition sse.cpp:55
SseConn * pc_sse_find(uint8_t slot_id)
Find the SseConn for a given TCP slot, or nullptr.
Definition sse.cpp:43
Layer 6 (Presentation) – Server-Sent Events connection pool.
SSE connection state stored in pc_sse_pool[].
Definition sse.h:46
char path[MAX_PATH_LEN]
Definition sse.h:52
uint8_t pc_sse_id
Index into pc_sse_pool[] (set at init).
Definition sse.h:47
bool active
True when this entry is in use.
Definition sse.h:49
uint8_t slot_id
Owning TCP slot in conn_pool[].
Definition sse.h:48
bool pc_conn_send(uint8_t slot, const void *data, u16_t len)
Send len bytes on connection slot (copies data; TLS-aware).
Definition tcp.cpp:500
Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.