ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
haas_mdc.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file haas_mdc.h
6 * @brief Haas Machine Data Collection (MDC) Q-command codec (PROTOCORE_ENABLE_HAAS_MDC) - a zero-heap codec
7 * for the documented Haas Automation MDC protocol, the `?Q` query set a Haas CNC mill / lathe
8 * control answers over RS-232 (7-E-1, XON/XOFF) or a raw TCP socket (Setting 143, default port
9 * 5051). A small, fully-documented CNC read source that fans machine status into HTTP / MQTT.
10 *
11 * The device is the collector: it sends `?Q###` queries and the control replies with a framed,
12 * comma-delimited payload. This codec builds the queries (@ref protocore_haas_mdc_build_q for the numbered
13 * commands and @ref protocore_haas_mdc_build_var for the `?Q600 <var>` macro/system-variable read) and
14 * parses the responses (@ref protocore_haas_mdc_parse extracts the CSV payload framed between STX and ETB,
15 * then @ref protocore_haas_mdc_value / @ref protocore_haas_mdc_parse_status / @ref protocore_haas_mdc_parse_macro read
16 * the typed forms). It also de-multiplexes the unprompted `DPRNT(...)` lines a running G-code program
17 * pushes on the same link (@ref protocore_haas_mdc_dprnt_line - raw text, no STX/ETB).
18 *
19 * Wire framing (byte-exact): a request is `?Q###` + CR (`\r`), UPPERCASE. A response payload lives
20 * strictly between STX (0x02) and ETB (0x17), followed by CR LF and a `>` (0x3E) prompt byte, e.g.
21 * `?Q100\r` -> `\x02SERIAL NUMBER, 1234567\x17\r\n>`. Parse defensively by scanning for the STX/ETB
22 * delimiters, not fixed offsets. Q500 has two branches (`PROGRAM, Oxxxxx, <status>, PARTS, n` when
23 * idle/running vs `STATUS, BUSY` mid-operation); an unsupported command returns `UNKNOWN`.
24 *
25 * Reference: Haas Automation service manual "Machine Data Collection" (Setting 143) + the Q-command
26 * table and DPRNT how-to; framing cross-checked byte-for-byte against a production Haas serial adapter.
27 *
28 * @author Douglas Quigg (dstroy0)
29 * @date 2026
30 */
31
32#ifndef PROTOCORE_HAAS_MDC_H
33#define PROTOCORE_HAAS_MDC_H
34
35#include "protocore_config.h" // the entry point: protocore_types.h for the widths
36
37#if PROTOCORE_ENABLE_HAAS_MDC
38
40
41/** @brief Default Haas MDC TCP port (Setting 143 "Machine Data Collect"). */
42#define PROTOCORE_HAAS_MDC_TCP_PORT 5051
43/** @brief Start-of-payload byte in an MDC response frame. */
44#define PROTOCORE_HAAS_MDC_STX 0x02
45/** @brief End-of-payload byte in an MDC response frame. */
46#define PROTOCORE_HAAS_MDC_ETB 0x17
47/** @brief Trailing ready/prompt byte the control emits after a response. */
48#define PROTOCORE_HAAS_MDC_PROMPT 0x3E
49/** @brief Maximum comma-separated fields kept from a response payload (Q500 has 5). */
50#define PROTOCORE_HAAS_MDC_MAX_FIELDS 8
51
52/** @brief The documented numbered Q queries (pass to @ref protocore_haas_mdc_build_q). Unscoped so a value
53 * converts to the `uint16_t` command number without a cast; `?Q600` takes an argument, so it has its
54 * own builder (@ref protocore_haas_mdc_build_var) rather than a member here. */
55enum HaasQ : uint16_t // NOSONAR(cpp:S3642): unscoped is the documented contract - the builder accepts any Haas
56 // command number, and this header has no second enum whose values could be confused
57{
58 HAAS_Q_SERIAL = 100, ///< machine serial number
59 HAAS_Q_SOFTWARE = 101, ///< control software version
60 HAAS_Q_MODEL = 102, ///< machine model number
61 HAAS_Q_MODE = 104, ///< mode (MDI / MEM / JOG / ZERORET / LIST PROG)
62 HAAS_Q_TOOL_CHANGES = 200, ///< total tool changes
63 HAAS_Q_TOOL_IN_USE = 201, ///< tool number in use
64 HAAS_Q_POWERON_TIME = 300, ///< total power-on time
65 HAAS_Q_CUTTING_TIME = 301, ///< total part-cutting (motion) time
66 HAAS_Q_LAST_CYCLE = 303, ///< last completed cycle time
67 HAAS_Q_PREV_CYCLE = 304, ///< previous cycle time
68 HAAS_Q_M30_COUNTER_1 = 402, ///< M30 parts counter #1
69 HAAS_Q_M30_COUNTER_2 = 403, ///< M30 parts counter #2
70 HAAS_Q_PROGRAM_STATUS = 500, ///< active program + run status + parts counter (three-in-one)
71};
72
73/** @brief A parsed response: the comma-separated payload fields, each trimmed of surrounding spaces
74 * and pointing INTO the caller's buffer (zero-copy; the buffer must outlive the struct). */
75typedef struct
76{
77 const char *field[PROTOCORE_HAAS_MDC_MAX_FIELDS];
78 size_t field_len[PROTOCORE_HAAS_MDC_MAX_FIELDS];
79 uint8_t n_fields;
80} HaasMdcResp;
81
82/** @brief The decoded Q500 (program + run status + parts) response. */
83typedef struct
84{
85 proto_bool busy; ///< true when the control returned `STATUS, BUSY` (no program/parts available)
86 const char *program; ///< selected program (`Oxxxxx` or a name); NULL when @ref busy
87 size_t program_len;
88 const char *status; ///< run-status token (IDLE / FEED HOLD / ALARM / ...); `BUSY` when @ref busy
89 size_t status_len;
90 uint32_t parts; ///< parts counter (valid only when @ref parts_valid)
91 proto_bool parts_valid; ///< false when @ref busy or the parts field was absent / non-numeric
92} HaasMdcStatus;
93
94/**
95 * @brief Build a numbered query line: `?Q<qnum>` + CR (e.g. `protocore_haas_mdc_build_q(..., HAAS_Q_SERIAL)`
96 * -> `"?Q100\r"`). Pass a @ref HaasQ value or a raw command number.
97 * @return characters written (excluding the NUL), or 0 on overflow / bad input.
98 */
99size_t protocore_haas_mdc_build_q(char *buf, size_t cap, uint16_t qnum);
100
101/**
102 * @brief Build a macro/system-variable read: `?Q600 <var>` + CR (e.g. `protocore_haas_mdc_build_var(..., 100)`
103 * -> `"?Q600 100\r"`). Reads any readable macro (e.g. #1-999) or system variable.
104 * @return characters written (excluding the NUL), or 0 on overflow / bad input.
105 */
106size_t protocore_haas_mdc_build_var(char *buf, size_t cap, uint32_t var);
107
108/**
109 * @brief Parse a response frame: locate the payload between STX (0x02) and ETB (0x17) - scanning, not
110 * by offset - and split it into comma-separated fields, each trimmed of surrounding spaces and
111 * pointing into @p buf. CR/LF and the `>` prompt outside the STX/ETB window are ignored.
112 * @return true if a complete `STX ... ETB` frame with at least one field was found; false otherwise
113 * (no frame yet - the caller should accumulate more bytes).
114 */
115proto_bool protocore_haas_mdc_parse(const char *buf, size_t len, HaasMdcResp *out);
116
117/**
118 * @brief Random-access a parsed field. @p p / @p l receive a pointer into the original buffer + length.
119 * @return true if @p idx < n_fields.
120 */
121proto_bool protocore_haas_mdc_field(const HaasMdcResp *r, size_t idx, const char **p, size_t *l);
122
123/**
124 * @brief The value of a simple `LABEL, value` response - field[1] (e.g. the serial for Q100, the
125 * version for Q101, the mode token for Q104).
126 * @return true if the response has at least two fields.
127 */
128proto_bool protocore_haas_mdc_value(const HaasMdcResp *r, const char **p, size_t *l);
129
130/**
131 * @brief True if the response is the control's `UNKNOWN` error (an unsupported / lowercase command).
132 */
133proto_bool protocore_haas_mdc_is_error(const HaasMdcResp *r);
134
135/**
136 * @brief Decode a Q500 response into @ref HaasMdcStatus, handling both branches: `PROGRAM, Oxxxxx,
137 * <status>, PARTS, n` (sets program / status / parts) and `STATUS, BUSY` (sets @ref
138 * HaasMdcStatus::busy). All string members point into the parsed buffer.
139 * @return true if the response is a recognizable Q500 form; false otherwise.
140 */
141proto_bool protocore_haas_mdc_parse_status(const HaasMdcResp *r, HaasMdcStatus *out);
142
143/**
144 * @brief Decode a Q600 response `MACRO, <var>, <value>`. @p var receives the variable number; @p value
145 * / @p value_len point at the value string (trimmed - it is a fixed-width, space-padded decimal
146 * on the wire; exposed as text so the caller keeps full precision without a float parse).
147 * @return true on a well-formed `MACRO, ...` response with a numeric variable field.
148 */
149proto_bool protocore_haas_mdc_parse_macro(const HaasMdcResp *r, uint32_t *var, const char **value, size_t *value_len);
150
151/**
152 * @brief Extract an unprompted `DPRNT(...)` line pushed by a running program: a raw ASCII text line
153 * with NO STX/ETB frame, terminated by CR/LF and optionally bracketed by DC2 (POPEN, 0x12) /
154 * DC4 (PCLOS, 0x14). Strips leading prompt/newline/DC2 and trailing CR/LF/DC4, preserving any
155 * interior spaces (a DPRNT `*` arrives as a space and may be significant).
156 * @return true for a non-empty pushed line; false if @p buf contains an STX (it is a framed Q
157 * response, not DPRNT) or is empty after stripping.
158 */
159proto_bool protocore_haas_mdc_dprnt_line(const char *buf, size_t len, const char **text, size_t *text_len);
160
162
163#endif // PROTOCORE_ENABLE_HAAS_MDC
164
165#endif // PROTOCORE_HAAS_MDC_H
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97