ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
dnc.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 dnc.cpp
6 * @brief CNC RS-232 DNC drip-feed codec implementation (see dnc.h).
7 */
8
9#include "dnc.h"
10
11#if PC_ENABLE_DNC
12
13// EIA RS-244 punched-tape code. One source of truth for both translation directions.
14// Each EIA byte is the 8-track hole pattern (bit 0 = channel 1 .. bit 7 = channel 8) with
15// odd parity in channel 5 (0x10): every entry has an odd number of set bits. The digit and
16// letter values follow the standard zone (6+7 / 7 / 6) + BCD-digit + parity construction and
17// are individually parity-checked by the host tests.
18struct DncEiaPair
19{
20 char ascii;
21 uint8_t eia;
22};
23
24static const DncEiaPair DNC_EIA_MAP[] = {
25 // digits (BCD in channels 1-4; 0 is channel 6)
26 {'0', 0x20},
27 {'1', 0x01},
28 {'2', 0x02},
29 {'3', 0x13},
30 {'4', 0x04},
31 {'5', 0x15},
32 {'6', 0x16},
33 {'7', 0x07},
34 {'8', 0x08},
35 {'9', 0x19},
36 // A-I (channels 6+7 zone + digit 1-9)
37 {'A', 0x61},
38 {'B', 0x62},
39 {'C', 0x73},
40 {'D', 0x64},
41 {'E', 0x75},
42 {'F', 0x76},
43 {'G', 0x67},
44 {'H', 0x68},
45 {'I', 0x79},
46 // J-R (channel 7 zone + digit 1-9)
47 {'J', 0x51},
48 {'K', 0x52},
49 {'L', 0x43},
50 {'M', 0x54},
51 {'N', 0x45},
52 {'O', 0x46},
53 {'P', 0x57},
54 {'Q', 0x58},
55 {'R', 0x49},
56 // S-Z (channel 6 zone + digit 2-9)
57 {'S', 0x32},
58 {'T', 0x23},
59 {'U', 0x34},
60 {'V', 0x25},
61 {'W', 0x26},
62 {'X', 0x37},
63 {'Y', 0x38},
64 {'Z', 0x29},
65 // NC punctuation + controls (EIA has no lowercase, no ':' '(' ')')
66 {' ', 0x10},
67 {'.', 0x6B},
68 {'-', 0x40},
69 {'+', 0x70},
70 {'/', 0x31},
71 {'\t', 0x3E},
72 // the '%' rewind-stop is EIA End-of-Record
73 {'%', (uint8_t)DncEiaCode::DNC_EIA_EOR},
74};
75
76static const size_t DNC_EIA_MAP_LEN = sizeof(DNC_EIA_MAP) / sizeof(DNC_EIA_MAP[0]);
77
78uint8_t pc_dnc_iso_to_eia(char c)
79{
80 for (size_t i = 0; i < DNC_EIA_MAP_LEN; i++)
81 {
82 if (DNC_EIA_MAP[i].ascii == c)
83 {
84 return DNC_EIA_MAP[i].eia;
85 }
86 }
87 return 0xFF; // no EIA representation - fail closed
88}
89
90char pc_dnc_eia_to_iso(uint8_t b)
91{
92 for (size_t i = 0; i < DNC_EIA_MAP_LEN; i++)
93 {
94 if (DNC_EIA_MAP[i].eia == b)
95 {
96 return DNC_EIA_MAP[i].ascii;
97 }
98 }
99 return 0; // unknown EIA code (e.g. blank / runout)
100}
101
102uint8_t pc_dnc_iso_add_parity(uint8_t ascii7)
103{
104 uint8_t v = ascii7 & 0x7F;
105 uint8_t x = v;
106 uint8_t p = 0;
107 while (x) // p = 1 when v has an odd number of set bits
108 {
109 p ^= 1;
110 x &= (uint8_t)(x - 1);
111 }
112 return (uint8_t)(v | (p ? 0x80 : 0x00)); // even parity: set bit 7 to make the total even
113}
114
115void pc_dnc_flow_init(DncFlow *f)
116{
117 f->paused = false;
118}
119
120bool pc_dnc_flow_feed(DncFlow *f, uint8_t rx)
121{
122 if (rx == (uint8_t)DncFlowByte::DNC_XOFF)
123 {
124 f->paused = true;
125 return true;
126 }
127 if (rx == (uint8_t)DncFlowByte::DNC_XON)
128 {
129 f->paused = false;
130 return true;
131 }
132 return false;
133}
134
135// Append one End-of-Block to out[n..]; returns the new count or 0 on overflow.
136static size_t dnc_put_eob(const DncCfg *cfg, uint8_t *out, size_t cap, size_t n)
137{
138 if (cfg->code == DncCode::DNC_CODE_EIA)
139 {
140 if (n >= cap)
141 {
142 return 0;
143 }
144 out[n++] = (uint8_t)DncEiaCode::DNC_EIA_EOB;
145 return n;
146 }
147 if (cfg->crlf)
148 {
149 uint8_t cr = cfg->even_parity ? pc_dnc_iso_add_parity(0x0D) : 0x0D;
150 if (n >= cap)
151 {
152 return 0;
153 }
154 out[n++] = cr;
155 }
156 uint8_t lf = cfg->even_parity ? pc_dnc_iso_add_parity(0x0A) : 0x0A;
157 if (n >= cap)
158 {
159 return 0;
160 }
161 out[n++] = lf;
162 return n;
163}
164
165size_t pc_dnc_encode_block(const DncCfg *cfg, const char *line, size_t line_len, uint8_t *out, size_t out_cap)
166{
167 size_t n = 0;
168 for (size_t i = 0; i < line_len; i++)
169 {
170 uint8_t b;
171 if (cfg->code == DncCode::DNC_CODE_EIA)
172 {
173 uint8_t e = pc_dnc_iso_to_eia(line[i]);
174 if (e == 0xFF)
175 {
176 return 0; // non-representable character - fail closed
177 }
178 b = e;
179 }
180 else
181 {
182 b = (uint8_t)line[i] & 0x7F;
183 if (cfg->even_parity)
184 {
185 b = pc_dnc_iso_add_parity(b);
186 }
187 }
188 if (n >= out_cap)
189 {
190 return 0;
191 }
192 out[n++] = b;
193 }
194 return dnc_put_eob(cfg, out, out_cap, n);
195}
196
197size_t pc_dnc_encode_marker(const DncCfg *cfg, uint8_t *out, size_t out_cap)
198{
199 size_t n = 0;
200 if (cfg->code == DncCode::DNC_CODE_EIA)
201 {
202 if (n >= out_cap)
203 {
204 return 0;
205 }
206 out[n++] = (uint8_t)DncEiaCode::DNC_EIA_EOR;
207 }
208 else
209 {
210 uint8_t pct = cfg->even_parity ? pc_dnc_iso_add_parity(0x25) : 0x25;
211 if (n >= out_cap)
212 {
213 return 0;
214 }
215 out[n++] = pct;
216 }
217 return dnc_put_eob(cfg, out, out_cap, n);
218}
219
220size_t pc_dnc_encode_leader(const DncCfg *cfg, uint8_t *out, size_t out_cap)
221{
222 uint16_t n = cfg->leader_len;
223 if ((size_t)n > out_cap)
224 {
225 return 0;
226 }
227 for (uint16_t i = 0; i < n; i++)
228 {
229 out[i] = 0x00; // NUL runout - skipped by the reader until the first '%'
230 }
231 return n;
232}
233
234void pc_dnc_decode_init(DncDecoder *d, DncCode code)
235{
236 d->code = code;
237 d->len = 0;
238 d->overflow = false;
239 d->in_program = false;
240 d->line_ready = false;
241 d->line[0] = 0;
242}
243
244DncEvent pc_dnc_decode_feed(DncDecoder *d, uint8_t wire)
245{
246 // The line delivered by the previous call is now consumed; start fresh.
247 if (d->line_ready)
248 {
249 d->line_ready = false;
250 d->len = 0;
251 d->line[0] = 0;
252 }
253
254 // Note: XON/XOFF are NOT filtered here. Flow control rides the reverse channel
255 // (controller -> sender), handled by pc_dnc_flow_feed; this decodes the forward program
256 // stream, where 0x13 is the EIA data character '3', not DC3.
257 bool is_eob = false;
258 bool is_marker = false;
259 uint8_t ascii = 0;
260
261 if (d->code == DncCode::DNC_CODE_EIA)
262 {
263 if (wire == (uint8_t)DncEiaCode::DNC_EIA_EOB)
264 {
265 is_eob = true;
266 }
267 else if (wire == (uint8_t)DncEiaCode::DNC_EIA_EOR)
268 {
269 is_marker = true;
270 }
271 else
272 {
273 ascii = (uint8_t)pc_dnc_eia_to_iso(wire); // 0 for blank/runout/unknown -> ignored
274 }
275 }
276 else
277 {
278 uint8_t v = wire & 0x7F; // strip even parity
279 if (v == '\n')
280 {
281 is_eob = true;
282 }
283 else if (v == '%')
284 {
285 is_marker = true;
286 }
287 else if (v == '\r' || v == 0x00 || v == (uint8_t)DncEiaCode::DNC_EIA_DEL)
288 {
289 ascii = 0; // CR / NUL / DEL runout - ignored
290 }
291 else
292 {
293 ascii = v;
294 }
295 }
296
297 if (is_marker)
298 {
299 d->len = 0; // a marker stands alone; discard any partial block
300 d->overflow = false;
301 d->line[0] = 0;
302 if (!d->in_program)
303 {
304 d->in_program = true;
305 return DncEvent::DNC_EV_PROG_START;
306 }
307 d->in_program = false;
308 return DncEvent::DNC_EV_PROG_END;
309 }
310
311 if (is_eob)
312 {
313 if (d->overflow)
314 {
315 d->overflow = false;
316 d->len = 0;
317 d->line[0] = 0;
318 return DncEvent::DNC_EV_OVERFLOW;
319 }
320 if (d->len == 0)
321 {
322 return DncEvent::DNC_EV_NONE; // blank block (leader / CR LF pair) - nothing to report
323 }
324 d->line[d->len] = 0;
325 d->line_ready = true; // keep line/len valid for the caller until the next feed
326 return DncEvent::DNC_EV_LINE;
327 }
328
329 if (ascii == 0)
330 {
331 return DncEvent::DNC_EV_NONE; // ignored character
332 }
333
334 if (d->overflow)
335 {
336 return DncEvent::DNC_EV_NONE; // dropping the rest of an over-long block until its EOB
337 }
338 if (d->len >= PC_DNC_LINE_MAX)
339 {
340 d->overflow = true;
341 return DncEvent::DNC_EV_NONE;
342 }
343 d->line[d->len++] = (char)ascii;
344 return DncEvent::DNC_EV_NONE;
345}
346
347#endif // PC_ENABLE_DNC
CNC RS-232 DNC (Distributed Numerical Control) drip-feed codec (PC_ENABLE_DNC).
#define PC_DNC_LINE_MAX
Largest G-code block (one line) the DNC decoder reassembles (PC_ENABLE_DNC).