ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
haas_mdc.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 haas_mdc.h
6 * @brief Haas Machine Data Collection (MDC) Q-command codec (PC_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 pc_haas_mdc_build_q for the numbered
13 * commands and @ref pc_haas_mdc_build_var for the `?Q600 <var>` macro/system-variable read) and
14 * parses the responses (@ref pc_haas_mdc_parse extracts the CSV payload framed between STX and ETB,
15 * then @ref pc_haas_mdc_value / @ref pc_haas_mdc_parse_status / @ref pc_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 pc_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"
36
37#if PC_ENABLE_HAAS_MDC
38
39#include <stddef.h>
40#include <stdint.h>
41
42/** @brief Default Haas MDC TCP port (Setting 143 "Machine Data Collect"). */
43#define PC_HAAS_MDC_TCP_PORT 5051
44/** @brief Start-of-payload byte in an MDC response frame. */
45#define PC_HAAS_MDC_STX 0x02
46/** @brief End-of-payload byte in an MDC response frame. */
47#define PC_HAAS_MDC_ETB 0x17
48/** @brief Trailing ready/prompt byte the control emits after a response. */
49#define PC_HAAS_MDC_PROMPT 0x3E
50/** @brief Maximum comma-separated fields kept from a response payload (Q500 has 5). */
51#define PC_HAAS_MDC_MAX_FIELDS 8
52
53/** @brief The documented numbered Q queries (pass to @ref pc_haas_mdc_build_q). Unscoped so a value
54 * converts to the `uint16_t` command number without a cast; `?Q600` takes an argument, so it has its
55 * own builder (@ref pc_haas_mdc_build_var) rather than a member here. */
56enum HaasQ : uint16_t // NOSONAR(cpp:S3642): unscoped is the documented contract - the builder accepts any Haas
57 // command number, and this header has no second enum whose values could be confused
58{
59 HAAS_Q_SERIAL = 100, ///< machine serial number
60 HAAS_Q_SOFTWARE = 101, ///< control software version
61 HAAS_Q_MODEL = 102, ///< machine model number
62 HAAS_Q_MODE = 104, ///< mode (MDI / MEM / JOG / ZERORET / LIST PROG)
63 HAAS_Q_TOOL_CHANGES = 200, ///< total tool changes
64 HAAS_Q_TOOL_IN_USE = 201, ///< tool number in use
65 HAAS_Q_POWERON_TIME = 300, ///< total power-on time
66 HAAS_Q_CUTTING_TIME = 301, ///< total part-cutting (motion) time
67 HAAS_Q_LAST_CYCLE = 303, ///< last completed cycle time
68 HAAS_Q_PREV_CYCLE = 304, ///< previous cycle time
69 HAAS_Q_M30_COUNTER_1 = 402, ///< M30 parts counter #1
70 HAAS_Q_M30_COUNTER_2 = 403, ///< M30 parts counter #2
71 HAAS_Q_PROGRAM_STATUS = 500, ///< active program + run status + parts counter (three-in-one)
72};
73
74/** @brief A parsed response: the comma-separated payload fields, each trimmed of surrounding spaces
75 * and pointing INTO the caller's buffer (zero-copy; the buffer must outlive the struct). */
76struct HaasMdcResp
77{
78 const char *field[PC_HAAS_MDC_MAX_FIELDS];
79 size_t field_len[PC_HAAS_MDC_MAX_FIELDS];
80 uint8_t n_fields;
81};
82
83/** @brief The decoded Q500 (program + run status + parts) response. */
84struct HaasMdcStatus
85{
86 bool busy; ///< true when the control returned `STATUS, BUSY` (no program/parts available)
87 const char *program; ///< selected program (`Oxxxxx` or a name); NULL when @ref busy
88 size_t program_len;
89 const char *status; ///< run-status token (IDLE / FEED HOLD / ALARM / ...); `BUSY` when @ref busy
90 size_t status_len;
91 uint32_t parts; ///< parts counter (valid only when @ref parts_valid)
92 bool parts_valid; ///< false when @ref busy or the parts field was absent / non-numeric
93};
94
95/**
96 * @brief Build a numbered query line: `?Q<qnum>` + CR (e.g. `pc_haas_mdc_build_q(..., HAAS_Q_SERIAL)`
97 * -> `"?Q100\r"`). Pass a @ref HaasQ value or a raw command number.
98 * @return characters written (excluding the NUL), or 0 on overflow / bad input.
99 */
100size_t pc_haas_mdc_build_q(char *buf, size_t cap, uint16_t qnum);
101
102/**
103 * @brief Build a macro/system-variable read: `?Q600 <var>` + CR (e.g. `pc_haas_mdc_build_var(..., 100)`
104 * -> `"?Q600 100\r"`). Reads any readable macro (e.g. #1-999) or system variable.
105 * @return characters written (excluding the NUL), or 0 on overflow / bad input.
106 */
107size_t pc_haas_mdc_build_var(char *buf, size_t cap, uint32_t var);
108
109/**
110 * @brief Parse a response frame: locate the payload between STX (0x02) and ETB (0x17) - scanning, not
111 * by offset - and split it into comma-separated fields, each trimmed of surrounding spaces and
112 * pointing into @p buf. CR/LF and the `>` prompt outside the STX/ETB window are ignored.
113 * @return true if a complete `STX ... ETB` frame with at least one field was found; false otherwise
114 * (no frame yet - the caller should accumulate more bytes).
115 */
116bool pc_haas_mdc_parse(const char *buf, size_t len, HaasMdcResp *out);
117
118/**
119 * @brief Random-access a parsed field. @p p / @p l receive a pointer into the original buffer + length.
120 * @return true if @p idx < n_fields.
121 */
122bool pc_haas_mdc_field(const HaasMdcResp *r, size_t idx, const char **p, size_t *l);
123
124/**
125 * @brief The value of a simple `LABEL, value` response - field[1] (e.g. the serial for Q100, the
126 * version for Q101, the mode token for Q104).
127 * @return true if the response has at least two fields.
128 */
129bool pc_haas_mdc_value(const HaasMdcResp *r, const char **p, size_t *l);
130
131/**
132 * @brief True if the response is the control's `UNKNOWN` error (an unsupported / lowercase command).
133 */
134bool pc_haas_mdc_is_error(const HaasMdcResp *r);
135
136/**
137 * @brief Decode a Q500 response into @ref HaasMdcStatus, handling both branches: `PROGRAM, Oxxxxx,
138 * <status>, PARTS, n` (sets program / status / parts) and `STATUS, BUSY` (sets @ref
139 * HaasMdcStatus::busy). All string members point into the parsed buffer.
140 * @return true if the response is a recognizable Q500 form; false otherwise.
141 */
142bool pc_haas_mdc_parse_status(const HaasMdcResp *r, HaasMdcStatus *out);
143
144/**
145 * @brief Decode a Q600 response `MACRO, <var>, <value>`. @p var receives the variable number; @p value
146 * / @p value_len point at the value string (trimmed - it is a fixed-width, space-padded decimal
147 * on the wire; exposed as text so the caller keeps full precision without a float parse).
148 * @return true on a well-formed `MACRO, ...` response with a numeric variable field.
149 */
150bool pc_haas_mdc_parse_macro(const HaasMdcResp *r, uint32_t *var, const char **value, size_t *value_len);
151
152/**
153 * @brief Extract an unprompted `DPRNT(...)` line pushed by a running program: a raw ASCII text line
154 * with NO STX/ETB frame, terminated by CR/LF and optionally bracketed by DC2 (POPEN, 0x12) /
155 * DC4 (PCLOS, 0x14). Strips leading prompt/newline/DC2 and trailing CR/LF/DC4, preserving any
156 * interior spaces (a DPRNT `*` arrives as a space and may be significant).
157 * @return true for a non-empty pushed line; false if @p buf contains an STX (it is a framed Q
158 * response, not DPRNT) or is empty after stripping.
159 */
160bool pc_haas_mdc_dprnt_line(const char *buf, size_t len, const char **text, size_t *text_len);
161
162#endif // PC_ENABLE_HAAS_MDC
163
164#endif // PROTOCORE_HAAS_MDC_H
User-facing configuration for ProtoCore.