ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
hostlink.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 hostlink.cpp
6 * @brief Omron Host Link (C-mode) frame builder + parser (pure, host-tested).
7 */
8
10
11#if PC_ENABLE_HOSTLINK
12
13#include <string.h>
14
15uint8_t pc_hostlink_fcs(const char *data, size_t len)
16{
17 uint8_t f = 0;
18 for (size_t i = 0; i < len; i++)
19 {
20 f ^= (uint8_t)data[i];
21 }
22 return f;
23}
24
25static char hex_digit(uint8_t v)
26{
27 return (char)(v < 10 ? '0' + v : 'A' + (v - 10));
28}
29
30// Parse one hex digit (0-9 A-F a-f) into 0..15, or -1 if invalid.
31static int hex_val(char c)
32{
33 if (c >= '0' && c <= '9')
34 {
35 return c - '0';
36 }
37 if (c >= 'A' && c <= 'F')
38 {
39 return c - 'A' + 10;
40 }
41 if (c >= 'a' && c <= 'f')
42 {
43 return c - 'a' + 10;
44 }
45 return -1;
46}
47
48size_t pc_hostlink_build(char *buf, size_t cap, uint8_t node, const char *header_code, const char *text,
49 size_t text_len)
50{
51 if (!buf || !header_code || node > 99 || (text_len && !text))
52 {
53 return 0;
54 }
55 if (header_code[0] == '\0' || header_code[1] == '\0') // need exactly 2 header characters
56 {
57 return 0;
58 }
59 size_t total = 1 + 2 + 2 + text_len + 2 + 2; // @ + UU + XX + text + FCS + *CR
60 if (total >= cap) // need room for the frame + a NUL terminator
61 {
62 return 0;
63 }
64
65 size_t p = 0;
66 buf[p++] = '@';
67 buf[p++] = (char)('0' + node / 10);
68 buf[p++] = (char)('0' + node % 10);
69 buf[p++] = header_code[0];
70 buf[p++] = header_code[1];
71 if (text_len)
72 {
73 memcpy(buf + p, text, text_len);
74 p += text_len;
75 }
76 uint8_t f = pc_hostlink_fcs(buf, p); // XOR over '@' .. end of text
77 buf[p++] = hex_digit((uint8_t)(f >> 4));
78 buf[p++] = hex_digit((uint8_t)(f & 0x0F));
79 buf[p++] = '*';
80 buf[p++] = '\r';
81 buf[p] = '\0'; // NUL-terminate so callers may treat the ASCII frame as a string (matches pc_sdi12_build)
82 return p;
83}
84
85bool pc_hostlink_parse(const char *buf, size_t len, HostlinkFrame *out)
86{
87 // minimum: @ UU XX FF * CR = 1 + 2 + 2 + 2 + 1 + 1 = 9
88 if (!buf || !out || len < 9)
89 {
90 return false;
91 }
92 if (buf[0] != '@' || buf[len - 1] != '\r' || buf[len - 2] != '*')
93 {
94 return false;
95 }
96 if (buf[1] < '0' || buf[1] > '9' || buf[2] < '0' || buf[2] > '9')
97 {
98 return false;
99 }
100
101 size_t fcs_pos = len - 4; // the two FCS chars precede '*' CR
102 int hi = hex_val(buf[fcs_pos]);
103 int lo = hex_val(buf[fcs_pos + 1]);
104 if (hi < 0 || lo < 0)
105 {
106 return false;
107 }
108 uint8_t got = (uint8_t)((hi << 4) | lo);
109 if (pc_hostlink_fcs(buf, fcs_pos) != got) // XOR over '@' .. last text char
110 {
111 return false;
112 }
113
114 out->node = (uint8_t)((buf[1] - '0') * 10 + (buf[2] - '0'));
115 out->header_code[0] = buf[3];
116 out->header_code[1] = buf[4];
117 out->header_code[2] = '\0';
118 out->text = buf + 5;
119 out->text_len = fcs_pos - 5;
120 return true;
121}
122
123bool pc_hostlink_end_code(const HostlinkFrame *f, uint8_t *code)
124{
125 if (!f || f->text_len < 2)
126 {
127 return false;
128 }
129 int hi = hex_val(f->text[0]);
130 int lo = hex_val(f->text[1]);
131 if (hi < 0 || lo < 0)
132 {
133 return false;
134 }
135 if (code)
136 {
137 *code = (uint8_t)((hi << 4) | lo);
138 }
139 return true;
140}
141
142size_t pc_hostlink_build_read(char *buf, size_t cap, uint8_t node, uint16_t address, uint16_t count)
143{
144 if (address > 9999 || count > 9999 || count == 0)
145 {
146 return 0;
147 }
148 char text[8]; // 4-digit beginning word address + 4-digit word count, zero-padded decimal
149 text[0] = (char)('0' + (address / 1000) % 10);
150 text[1] = (char)('0' + (address / 100) % 10);
151 text[2] = (char)('0' + (address / 10) % 10);
152 text[3] = (char)('0' + address % 10);
153 text[4] = (char)('0' + (count / 1000) % 10);
154 text[5] = (char)('0' + (count / 100) % 10);
155 text[6] = (char)('0' + (count / 10) % 10);
156 text[7] = (char)('0' + count % 10);
157 return pc_hostlink_build(buf, cap, node, "RD", text, sizeof(text));
158}
159
160bool pc_hostlink_read_word(const HostlinkFrame *f, size_t index, uint16_t *out)
161{
162 if (!f || !out)
163 {
164 return false;
165 }
166 // A read response's text is the 2-char end code followed by 4-hex-char word values.
167 size_t pos = 2 + index * 4;
168 if (f->text_len < pos + 4)
169 {
170 return false;
171 }
172 int a = hex_val(f->text[pos]), b = hex_val(f->text[pos + 1]);
173 int c = hex_val(f->text[pos + 2]), d = hex_val(f->text[pos + 3]);
174 if (a < 0 || b < 0 || c < 0 || d < 0)
175 {
176 return false;
177 }
178 *out = (uint16_t)((a << 12) | (b << 8) | (c << 4) | d);
179 return true;
180}
181
182size_t pc_hostlink_build_write(char *buf, size_t cap, uint8_t node, uint16_t address, const uint16_t *words,
183 size_t word_count)
184{
185 if (!buf || node > 99 || address > 9999 || word_count == 0 || !words)
186 {
187 return 0;
188 }
189 if (word_count > cap / 4) // fail closed before word_count*4 could overflow / exceed the buffer
190 {
191 return 0;
192 }
193 // @ + UU + WR + addr(4) + word_count*4 hex + FCS(2) + *CR, plus a NUL terminator.
194 size_t total = 1 + 2 + 2 + 4 + word_count * 4 + 2 + 2;
195 if (total >= cap)
196 {
197 return 0;
198 }
199
200 size_t p = 0;
201 buf[p++] = '@';
202 buf[p++] = (char)('0' + node / 10);
203 buf[p++] = (char)('0' + node % 10);
204 buf[p++] = 'W';
205 buf[p++] = 'R';
206 buf[p++] = (char)('0' + (address / 1000) % 10);
207 buf[p++] = (char)('0' + (address / 100) % 10);
208 buf[p++] = (char)('0' + (address / 10) % 10);
209 buf[p++] = (char)('0' + address % 10);
210 for (size_t i = 0; i < word_count; i++)
211 {
212 uint16_t w = words[i];
213 buf[p++] = hex_digit((uint8_t)((w >> 12) & 0x0F));
214 buf[p++] = hex_digit((uint8_t)((w >> 8) & 0x0F));
215 buf[p++] = hex_digit((uint8_t)((w >> 4) & 0x0F));
216 buf[p++] = hex_digit((uint8_t)(w & 0x0F));
217 }
218 uint8_t f = pc_hostlink_fcs(buf, p); // XOR over '@' .. last data char
219 buf[p++] = hex_digit((uint8_t)(f >> 4));
220 buf[p++] = hex_digit((uint8_t)(f & 0x0F));
221 buf[p++] = '*';
222 buf[p++] = '\r';
223 buf[p] = '\0'; // NUL-terminate so callers may treat the ASCII frame as a string
224 return p;
225}
226
227#endif // PC_ENABLE_HOSTLINK