DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
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 DETWS_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 dnc_iso_to_eia(char c)
79{
80 for (size_t i = 0; i < DNC_EIA_MAP_LEN; i++)
81 if (DNC_EIA_MAP[i].ascii == c)
82 return DNC_EIA_MAP[i].eia;
83 return 0xFF; // no EIA representation - fail closed
84}
85
86char dnc_eia_to_iso(uint8_t b)
87{
88 for (size_t i = 0; i < DNC_EIA_MAP_LEN; i++)
89 if (DNC_EIA_MAP[i].eia == b)
90 return DNC_EIA_MAP[i].ascii;
91 return 0; // unknown EIA code (e.g. blank / runout)
92}
93
94uint8_t dnc_iso_add_parity(uint8_t ascii7)
95{
96 uint8_t v = ascii7 & 0x7F;
97 uint8_t x = v;
98 uint8_t p = 0;
99 while (x) // p = 1 when v has an odd number of set bits
100 {
101 p ^= 1;
102 x &= (uint8_t)(x - 1);
103 }
104 return (uint8_t)(v | (p ? 0x80 : 0x00)); // even parity: set bit 7 to make the total even
105}
106
107void dnc_flow_init(DncFlow *f)
108{
109 f->paused = false;
110}
111
112bool dnc_flow_feed(DncFlow *f, uint8_t rx)
113{
114 if (rx == (uint8_t)DncFlowByte::DNC_XOFF)
115 {
116 f->paused = true;
117 return true;
118 }
119 if (rx == (uint8_t)DncFlowByte::DNC_XON)
120 {
121 f->paused = false;
122 return true;
123 }
124 return false;
125}
126
127// Append one End-of-Block to out[n..]; returns the new count or 0 on overflow.
128static size_t dnc_put_eob(const DncCfg *cfg, uint8_t *out, size_t cap, size_t n)
129{
130 if (cfg->code == DncCode::DNC_CODE_EIA)
131 {
132 if (n >= cap)
133 return 0;
134 out[n++] = (uint8_t)DncEiaCode::DNC_EIA_EOB;
135 return n;
136 }
137 if (cfg->crlf)
138 {
139 uint8_t cr = cfg->even_parity ? dnc_iso_add_parity(0x0D) : 0x0D;
140 if (n >= cap)
141 return 0;
142 out[n++] = cr;
143 }
144 uint8_t lf = cfg->even_parity ? dnc_iso_add_parity(0x0A) : 0x0A;
145 if (n >= cap)
146 return 0;
147 out[n++] = lf;
148 return n;
149}
150
151size_t dnc_encode_block(const DncCfg *cfg, const char *line, size_t line_len, uint8_t *out, size_t out_cap)
152{
153 size_t n = 0;
154 for (size_t i = 0; i < line_len; i++)
155 {
156 uint8_t b;
157 if (cfg->code == DncCode::DNC_CODE_EIA)
158 {
159 uint8_t e = dnc_iso_to_eia(line[i]);
160 if (e == 0xFF)
161 return 0; // non-representable character - fail closed
162 b = e;
163 }
164 else
165 {
166 b = (uint8_t)line[i] & 0x7F;
167 if (cfg->even_parity)
168 b = dnc_iso_add_parity(b);
169 }
170 if (n >= out_cap)
171 return 0;
172 out[n++] = b;
173 }
174 return dnc_put_eob(cfg, out, out_cap, n);
175}
176
177size_t dnc_encode_marker(const DncCfg *cfg, uint8_t *out, size_t out_cap)
178{
179 size_t n = 0;
180 if (cfg->code == DncCode::DNC_CODE_EIA)
181 {
182 if (n >= out_cap)
183 return 0;
184 out[n++] = (uint8_t)DncEiaCode::DNC_EIA_EOR;
185 }
186 else
187 {
188 uint8_t pct = cfg->even_parity ? dnc_iso_add_parity(0x25) : 0x25;
189 if (n >= out_cap)
190 return 0;
191 out[n++] = pct;
192 }
193 return dnc_put_eob(cfg, out, out_cap, n);
194}
195
196size_t dnc_encode_leader(const DncCfg *cfg, uint8_t *out, size_t out_cap)
197{
198 uint16_t n = cfg->leader_len;
199 if ((size_t)n > out_cap)
200 return 0;
201 for (uint16_t i = 0; i < n; i++)
202 out[i] = 0x00; // NUL runout - skipped by the reader until the first '%'
203 return n;
204}
205
206void dnc_decode_init(DncDecoder *d, DncCode code)
207{
208 d->code = code;
209 d->len = 0;
210 d->overflow = false;
211 d->in_program = false;
212 d->line_ready = false;
213 d->line[0] = 0;
214}
215
216DncEvent dnc_decode_feed(DncDecoder *d, uint8_t wire)
217{
218 // The line delivered by the previous call is now consumed; start fresh.
219 if (d->line_ready)
220 {
221 d->line_ready = false;
222 d->len = 0;
223 d->line[0] = 0;
224 }
225
226 // Note: XON/XOFF are NOT filtered here. Flow control rides the reverse channel
227 // (controller -> sender), handled by dnc_flow_feed; this decodes the forward program
228 // stream, where 0x13 is the EIA data character '3', not DC3.
229 bool is_eob = false;
230 bool is_marker = false;
231 uint8_t ascii = 0;
232
233 if (d->code == DncCode::DNC_CODE_EIA)
234 {
235 if (wire == (uint8_t)DncEiaCode::DNC_EIA_EOB)
236 is_eob = true;
237 else if (wire == (uint8_t)DncEiaCode::DNC_EIA_EOR)
238 is_marker = true;
239 else
240 ascii = (uint8_t)dnc_eia_to_iso(wire); // 0 for blank/runout/unknown -> ignored
241 }
242 else
243 {
244 uint8_t v = wire & 0x7F; // strip even parity
245 if (v == '\n')
246 is_eob = true;
247 else if (v == '%')
248 is_marker = true;
249 else if (v == '\r' || v == 0x00 || v == (uint8_t)DncEiaCode::DNC_EIA_DEL)
250 ascii = 0; // CR / NUL / DEL runout - ignored
251 else
252 ascii = v;
253 }
254
255 if (is_marker)
256 {
257 d->len = 0; // a marker stands alone; discard any partial block
258 d->overflow = false;
259 d->line[0] = 0;
260 if (!d->in_program)
261 {
262 d->in_program = true;
263 return DncEvent::DNC_EV_PROG_START;
264 }
265 d->in_program = false;
266 return DncEvent::DNC_EV_PROG_END;
267 }
268
269 if (is_eob)
270 {
271 if (d->overflow)
272 {
273 d->overflow = false;
274 d->len = 0;
275 d->line[0] = 0;
276 return DncEvent::DNC_EV_OVERFLOW;
277 }
278 if (d->len == 0)
279 return DncEvent::DNC_EV_NONE; // blank block (leader / CR LF pair) - nothing to report
280 d->line[d->len] = 0;
281 d->line_ready = true; // keep line/len valid for the caller until the next feed
282 return DncEvent::DNC_EV_LINE;
283 }
284
285 if (ascii == 0)
286 return DncEvent::DNC_EV_NONE; // ignored character
287
288 if (d->overflow)
289 return DncEvent::DNC_EV_NONE; // dropping the rest of an over-long block until its EOB
290 if (d->len >= DETWS_DNC_LINE_MAX)
291 {
292 d->overflow = true;
293 return DncEvent::DNC_EV_NONE;
294 }
295 d->line[d->len++] = (char)ascii;
296 return DncEvent::DNC_EV_NONE;
297}
298
299#endif // DETWS_ENABLE_DNC
#define DETWS_DNC_LINE_MAX
Largest G-code block (one line) the DNC decoder reassembles (DETWS_ENABLE_DNC).
CNC RS-232 DNC (Distributed Numerical Control) drip-feed codec (DETWS_ENABLE_DNC).