DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
dnc.h
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.h
6 * @brief CNC RS-232 DNC (Distributed Numerical Control) drip-feed codec (DETWS_ENABLE_DNC).
7 *
8 * DNC is the classic way a program is streamed to a machine-tool controller: a G-code
9 * program (RS-274 / ISO 6983) is punched as blocks (one line each), framed with a
10 * `%` rewind-stop at the start and end, and drip-fed over an RS-232 link with XON/XOFF
11 * software flow control so the sender pauses when the controller's small input buffer
12 * fills. This codec is the transport-agnostic framing + tape-code layer only - the same
13 * bytes ride RS-232, a raw TCP socket, or a WebSocket; the app owns the wire.
14 *
15 * Two tape codes are supported (the historical split every DNC package still carries):
16 * - **ISO** (::DNC_CODE_ISO): ISO 7-bit / ASCII, End-of-Block = LF, program marker = `%`.
17 * Optional even parity in bit 7 (the ISO tape convention, RS-358).
18 * - **EIA** (::DNC_CODE_EIA): the EIA RS-244 punched-tape code - a distinct, odd-parity
19 * 8-track encoding (parity in channel 5). End-of-Block = 0x80 (channel 8, used only for
20 * EOB), the rewind-stop is EIA End-of-Record 0x0B (the `%` equivalent), and there are no
21 * lowercase letters. The full character table is odd-parity-verified.
22 *
23 * Three pieces, all pure and zero-heap:
24 * 1. character translation (::dnc_iso_to_eia / ::dnc_eia_to_iso) + ISO even-parity helper;
25 * 2. XON/XOFF flow state (::DncFlow) the send pump consults before each write;
26 * 3. a streaming block encoder (::dnc_encode_block + the `%`/leader framing) and a
27 * byte-at-a-time block decoder (::DncDecoder) that reassembles wire bytes back into
28 * ASCII G-code lines and reports the `%` program start/end.
29 *
30 * @author Douglas Quigg (dstroy0)
31 * @date 2026
32 */
33
34#ifndef DETERMINISTICESPASYNCWEBSERVER_DNC_H
35#define DETERMINISTICESPASYNCWEBSERVER_DNC_H
36
37#include "ServerConfig.h"
38
39#if DETWS_ENABLE_DNC
40
41#include <stddef.h>
42#include <stdint.h>
43
44/** @brief Which punched-tape character code the wire stream uses. */
45enum class DncCode : uint8_t
46{
47 DNC_CODE_ISO = 0, ///< ISO 7-bit / ASCII (RS-358), EOB = LF, marker = '%'.
48 DNC_CODE_EIA = 1, ///< EIA RS-244 odd-parity tape code, EOB = 0x80, marker = EOR 0x0B.
49};
50
51/** @brief Software flow-control bytes (both tape codes; these are the raw ASCII controls). */
52enum class DncFlowByte : uint8_t
53{
54 DNC_XON = 0x11, ///< DC1 - resume sending.
55 DNC_XOFF = 0x13, ///< DC3 - pause sending.
56};
57
58/** @brief EIA RS-244 special codes (not general text characters). */
59enum class DncEiaCode : uint8_t
60{
61 DNC_EIA_EOB = 0x80, ///< EIA End-of-Block (channel 8 only; the ISO LF equivalent).
62 DNC_EIA_EOR = 0x0B, ///< EIA End-of-Record (the ISO '%' rewind-stop equivalent).
63 DNC_EIA_DEL = 0x7F, ///< EIA Delete / rubout (leader/trailer runout; skipped on read).
64};
65
66/**
67 * @brief Translate one ISO/ASCII character to its EIA RS-244 byte.
68 *
69 * Covers the NC character set: digits, uppercase A-Z, space, `. - + / %` and Tab, plus the
70 * `%` rewind-stop (mapped to EIA End-of-Record 0x0B). Every returned byte carries EIA odd
71 * parity (channel 5).
72 *
73 * @return the EIA byte, or 0xFF if @p c has no EIA representation (fail-closed).
74 */
75uint8_t dnc_iso_to_eia(char c);
76
77/**
78 * @brief Translate one EIA RS-244 byte back to its ISO/ASCII character.
79 *
80 * The inverse of ::dnc_iso_to_eia. EIA End-of-Record (0x0B) maps back to '%'.
81 *
82 * @return the ASCII character, or 0 if @p b is not a known EIA code (e.g. blank/runout).
83 */
84char dnc_eia_to_iso(uint8_t b);
85
86/**
87 * @brief Set even parity in bit 7 of a 7-bit ASCII value (the ISO tape convention).
88 * @param ascii7 a value in 0x00-0x7F (bit 7 is ignored on input).
89 * @return @p ascii7 with bit 7 set so the byte has an even number of 1 bits.
90 */
91uint8_t dnc_iso_add_parity(uint8_t ascii7);
92
93/** @brief XON/XOFF software flow-control state for the send side. */
94struct DncFlow
95{
96 bool paused; ///< true after XOFF (DC3), cleared by XON (DC1).
97};
98
99/** @brief Reset flow state to "clear to send". */
100void dnc_flow_init(DncFlow *f);
101
102/**
103 * @brief Feed one received byte to the flow-control state machine.
104 * @return true if @p rx was a flow-control byte (XON/XOFF) and was consumed; false otherwise
105 * (the byte is ordinary inbound data the caller still owns).
106 */
107bool dnc_flow_feed(DncFlow *f, uint8_t rx);
108
109/** @brief Whether the send pump may transmit (i.e. not paused by an XOFF). */
110static inline bool dnc_flow_can_send(const DncFlow *f)
111{
112 return !f->paused;
113}
114
115/** @brief Encoder configuration - the tape code and its framing options. */
116struct DncCfg
117{
118 DncCode code; ///< ISO or EIA.
119 bool even_parity; ///< ISO only: emit even parity in bit 7 (ignored for EIA, which is always odd).
120 bool crlf; ///< ISO only: emit CR before the LF End-of-Block (some controllers want CR LF).
121 uint16_t leader_len; ///< leader/trailer runout length in bytes (::dnc_encode_leader / _trailer).
122};
123
124/**
125 * @brief Frame one G-code source line as a block (its characters + an End-of-Block).
126 *
127 * The source is plain ASCII with no terminator. Each character is translated to the
128 * configured tape code (ISO passes 7-bit through, adding even parity if requested; EIA maps
129 * via ::dnc_iso_to_eia), then the End-of-Block is appended (ISO: optional CR then LF; EIA: 0x80).
130 *
131 * @return bytes written to @p out, or 0 on overflow or a character with no EIA
132 * representation (fail-closed - nothing partial is emitted as a complete block).
133 */
134size_t dnc_encode_block(const DncCfg *cfg, const char *line, size_t line_len, uint8_t *out, size_t out_cap);
135
136/**
137 * @brief Emit the `%` program-start (or -end) marker followed by an End-of-Block.
138 *
139 * ISO writes '%' (with parity if configured); EIA writes End-of-Record (0x0B). Both then
140 * write the End-of-Block. Start and end are byte-identical; call it at both ends of the program.
141 *
142 * @return bytes written, or 0 on overflow.
143 */
144size_t dnc_encode_marker(const DncCfg *cfg, uint8_t *out, size_t out_cap);
145
146/**
147 * @brief Emit @ref DncCfg::leader_len runout bytes (NUL - skipped by the reader until `%`).
148 * @return bytes written (== leader_len), or 0 if @p out_cap is too small.
149 */
150size_t dnc_encode_leader(const DncCfg *cfg, uint8_t *out, size_t out_cap);
151
152/** @brief What ::dnc_decode_feed produced for the byte just fed. */
153enum class DncEvent : uint8_t
154{
155 DNC_EV_NONE = 0, ///< byte absorbed (mid-block, runout, or flow/ignored); nothing to report.
156 DNC_EV_LINE, ///< a complete non-empty block is ready in DncDecoder::line (NUL-terminated).
157 DNC_EV_PROG_START, ///< the first `%` / EOR was seen (program start).
158 DNC_EV_PROG_END, ///< a later `%` / EOR was seen (program end).
159 DNC_EV_OVERFLOW, ///< the current block exceeded DETWS_DNC_LINE_MAX; it was dropped.
160};
161
162/** @brief Streaming block reassembler: wire bytes in, ASCII G-code lines out. */
163struct DncDecoder
164{
165 DncCode code; ///< the tape code being decoded.
166 char line[DETWS_DNC_LINE_MAX + 1]; ///< the current block, NUL-terminated when DncEvent::DNC_EV_LINE fires.
167 uint16_t len; ///< bytes accumulated in @ref line so far (the line length on DncEvent::DNC_EV_LINE).
168 bool overflow; ///< the current block overran; drop until the next End-of-Block.
169 bool in_program; ///< a program-start `%` has been seen (so the next `%` is the end).
170 bool line_ready; ///< internal: the previous feed delivered a line; reset on the next feed.
171};
172
173/** @brief Reset a decoder for a given tape code. */
174void dnc_decode_init(DncDecoder *d, DncCode code);
175
176/**
177 * @brief Feed one wire byte to the block reassembler.
178 *
179 * Strips parity (ISO) / translates (EIA), skips runout (NUL / DEL / CR), accumulates a
180 * block until its End-of-Block, and reports the `%` program markers. XON/XOFF are not filtered
181 * here - flow control rides the reverse channel (see ::dnc_flow_feed); in the forward program
182 * stream 0x13 is the EIA data character '3', not DC3. On ::DNC_EV_LINE the
183 * completed line is in @ref DncDecoder::line (NUL-terminated) and @ref DncDecoder::len is its
184 * length; both are reset on the next call.
185 *
186 * @return the event for this byte (see ::DncEvent).
187 */
188DncEvent dnc_decode_feed(DncDecoder *d, uint8_t wire);
189
190#endif // DETWS_ENABLE_DNC
191
192#endif // DETERMINISTICESPASYNCWEBSERVER_DNC_H
User-facing configuration for DeterministicESPAsyncWebServer.
#define DETWS_DNC_LINE_MAX
Largest G-code block (one line) the DNC decoder reassembles (DETWS_ENABLE_DNC).