ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
modbus.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 modbus.cpp
6 * @brief Modbus TCP slave: data model, MBAP/PDU codec, and the TCP transport.
7 */
8
10#include "shared_primitives/crc.h" // PC_CRC16_MODBUS
11
12#if PC_NEED_MODBUS
13
14#include <string.h>
15
16// ---------------------------------------------------------------------------
17// Data model (all BSS - no heap)
18// ---------------------------------------------------------------------------
19
20// All Modbus data-model state, owned by one instance (internal linkage): the coil / discrete
21// bitfields, the holding / input registers, and the write callback, grouped so it is one
22// named owner, unreachable from any other translation unit.
23#if defined(ARDUINO)
26#endif
27struct ModbusCtx
28{
29 uint8_t coils[(PC_MODBUS_COILS + 7) / 8];
30 uint8_t discrete[(PC_MODBUS_DISCRETE_INPUTS + 7) / 8];
31 uint16_t holding[PC_MODBUS_HOLDING_REGS];
32 uint16_t input[PC_MODBUS_INPUT_REGS];
33 ModbusWriteCb write_cb = nullptr;
34};
35static ModbusCtx s_modbus;
36
37static bool bit_get(const uint8_t *a, uint16_t i)
38{
39 return (a[i >> 3] >> (i & 7)) & 1u;
40}
41static void bit_set(uint8_t *a, uint16_t i, bool v)
42{
43 if (v)
44 {
45 a[i >> 3] |= (uint8_t)(1u << (i & 7));
46 }
47 else
48 {
49 a[i >> 3] &= (uint8_t)~(1u << (i & 7));
50 }
51}
52
53void pc_modbus_server_init()
54{
55 memset(s_modbus.coils, 0, sizeof(s_modbus.coils));
56 memset(s_modbus.discrete, 0, sizeof(s_modbus.discrete));
57 memset(s_modbus.holding, 0, sizeof(s_modbus.holding));
58 memset(s_modbus.input, 0, sizeof(s_modbus.input));
59 s_modbus.write_cb = nullptr;
60}
61
62void pc_modbus_on_write(ModbusWriteCb cb)
63{
64 s_modbus.write_cb = cb;
65}
66
67bool pc_modbus_get_coil(uint16_t addr)
68{
69 return (addr < PC_MODBUS_COILS) ? bit_get(s_modbus.coils, addr) : false;
70}
71void pc_modbus_set_coil(uint16_t addr, bool on)
72{
73 if (addr < PC_MODBUS_COILS)
74 {
75 bit_set(s_modbus.coils, addr, on);
76 }
77}
78bool pc_modbus_get_discrete_input(uint16_t addr)
79{
80 return (addr < PC_MODBUS_DISCRETE_INPUTS) ? bit_get(s_modbus.discrete, addr) : false;
81}
82void pc_modbus_set_discrete_input(uint16_t addr, bool on)
83{
85 {
86 bit_set(s_modbus.discrete, addr, on);
87 }
88}
89uint16_t pc_modbus_get_holding_reg(uint16_t addr)
90{
91 return (addr < PC_MODBUS_HOLDING_REGS) ? s_modbus.holding[addr] : 0;
92}
93void pc_modbus_set_holding_reg(uint16_t addr, uint16_t value)
94{
95 if (addr < PC_MODBUS_HOLDING_REGS)
96 {
97 s_modbus.holding[addr] = value;
98 }
99}
100uint16_t pc_modbus_get_input_reg(uint16_t addr)
101{
102 return (addr < PC_MODBUS_INPUT_REGS) ? s_modbus.input[addr] : 0;
103}
104void pc_modbus_set_input_reg(uint16_t addr, uint16_t value)
105{
106 if (addr < PC_MODBUS_INPUT_REGS)
107 {
108 s_modbus.input[addr] = value;
109 }
110}
111
112// ---------------------------------------------------------------------------
113// PDU codec (host-testable)
114// ---------------------------------------------------------------------------
115
116static uint16_t rd16(const uint8_t *p)
117{
118 return (uint16_t)((p[0] << 8) | p[1]);
119}
120static void wr16(uint8_t *p, uint16_t v)
121{
122 p[0] = (uint8_t)(v >> 8);
123 p[1] = (uint8_t)(v & 0xFF);
124}
125
126// Build an exception PDU (fc|0x80, code). Always 2 bytes.
127static size_t pdu_exception(ModbusFunction fc, ModbusException code, uint8_t *out)
128{
129 out[0] = (uint8_t)((uint8_t)fc | 0x80);
130 out[1] = (uint8_t)code;
131 return 2;
132}
133
134// Process one PDU (function code + data) against the data model. Returns the
135// response PDU length, or 0 if it cannot fit (caller treats 0 as "send nothing").
136static size_t pc_modbus_process_pdu(const uint8_t *pdu, size_t pdu_len, uint8_t *out, size_t out_cap)
137{
138 // Both callers already guarantee at least one PDU byte, so this guard cannot fire: the TCP path
139 // rejects len < 2 before computing pdu_len = len - 1, and the RTU path rejects req_len < 4 before
140 // computing pdu_len = req_len - 3. Kept as defense in depth for any future caller.
141 if (pdu_len < 1) // GCOVR_EXCL_LINE
142 {
143 return 0; // GCOVR_EXCL_LINE
144 }
145 ModbusFunction fc = static_cast<ModbusFunction>(pdu[0]);
146
147 // Dispatch on the function code; each case validates its own request length and
148 // address/quantity range, replying with the data or a Modbus exception PDU.
149 switch (fc)
150 {
151 // FC1/FC2: read up to 2000 single-bit coils / discrete inputs, packed 8 per byte.
152 case ModbusFunction::MODBUS_FC_READ_COILS:
153 case ModbusFunction::MODBUS_FC_READ_DISCRETE_INPUTS: {
154 if (pdu_len < 5)
155 {
156 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_VALUE, out);
157 }
158 uint16_t start = rd16(pdu + 1), qty = rd16(pdu + 3);
159 uint16_t limit = (fc == ModbusFunction::MODBUS_FC_READ_COILS) ? PC_MODBUS_COILS : PC_MODBUS_DISCRETE_INPUTS;
160 const uint8_t *src = (fc == ModbusFunction::MODBUS_FC_READ_COILS) ? s_modbus.coils : s_modbus.discrete;
161 if (qty < 1 || qty > 2000)
162 {
163 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_VALUE, out);
164 }
165 if ((uint32_t)start + qty > limit)
166 {
167 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_ADDRESS, out);
168 }
169 uint8_t bytes = (uint8_t)((qty + 7) / 8);
170 if ((size_t)2 + bytes > out_cap)
171 {
172 return 0;
173 }
174 out[0] = (uint8_t)fc;
175 out[1] = bytes;
176 memset(out + 2, 0, bytes);
177 for (uint16_t i = 0; i < qty; i++)
178 {
179 if (bit_get(src, (uint16_t)(start + i)))
180 {
181 out[2 + (i >> 3)] |= (uint8_t)(1u << (i & 7));
182 }
183 }
184 return (size_t)2 + bytes;
185 }
186
187 // FC3/FC4: read up to 125 16-bit holding / input registers, big-endian.
188 case ModbusFunction::MODBUS_FC_READ_HOLDING_REGS:
189 case ModbusFunction::MODBUS_FC_READ_INPUT_REGS: {
190 if (pdu_len < 5)
191 {
192 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_VALUE, out);
193 }
194 uint16_t start = rd16(pdu + 1), qty = rd16(pdu + 3);
195 uint16_t limit =
196 (fc == ModbusFunction::MODBUS_FC_READ_HOLDING_REGS) ? PC_MODBUS_HOLDING_REGS : PC_MODBUS_INPUT_REGS;
197 const uint16_t *src = (fc == ModbusFunction::MODBUS_FC_READ_HOLDING_REGS) ? s_modbus.holding : s_modbus.input;
198 if (qty < 1 || qty > 125)
199 {
200 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_VALUE, out);
201 }
202 if ((uint32_t)start + qty > limit)
203 {
204 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_ADDRESS, out);
205 }
206 uint8_t bytes = (uint8_t)(qty * 2);
207 if ((size_t)2 + bytes > out_cap)
208 {
209 return 0;
210 }
211 out[0] = (uint8_t)fc;
212 out[1] = bytes;
213 for (uint16_t i = 0; i < qty; i++)
214 {
215 wr16(out + 2 + i * 2, src[start + i]);
216 }
217 return (size_t)2 + bytes;
218 }
219
220 // FC5: write one coil (value 0xFF00 = on, 0x0000 = off); echo the request back.
221 case ModbusFunction::MODBUS_FC_WRITE_SINGLE_COIL: {
222 if (pdu_len < 5)
223 {
224 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_VALUE, out);
225 }
226 uint16_t addr = rd16(pdu + 1), value = rd16(pdu + 3);
227 if (value != 0x0000 && value != 0xFF00)
228 {
229 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_VALUE, out);
230 }
231 if (addr >= PC_MODBUS_COILS)
232 {
233 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_ADDRESS, out);
234 }
235 bit_set(s_modbus.coils, addr, value == 0xFF00);
236 if (s_modbus.write_cb)
237 {
238 s_modbus.write_cb((uint8_t)fc, addr, 1);
239 }
240 if (out_cap < 5)
241 {
242 return 0;
243 }
244 memcpy(out, pdu, 5); // echo request
245 return 5;
246 }
247
248 // FC6: write one holding register; echo the request back.
249 case ModbusFunction::MODBUS_FC_WRITE_SINGLE_REG: {
250 if (pdu_len < 5)
251 {
252 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_VALUE, out);
253 }
254 uint16_t addr = rd16(pdu + 1), value = rd16(pdu + 3);
255 if (addr >= PC_MODBUS_HOLDING_REGS)
256 {
257 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_ADDRESS, out);
258 }
259 s_modbus.holding[addr] = value;
260 if (s_modbus.write_cb)
261 {
262 s_modbus.write_cb((uint8_t)fc, addr, 1);
263 }
264 if (out_cap < 5)
265 {
266 return 0;
267 }
268 memcpy(out, pdu, 5);
269 return 5;
270 }
271
272 // FC15: write up to 1968 coils from a packed bitfield; reply start + quantity.
273 case ModbusFunction::MODBUS_FC_WRITE_MULTIPLE_COILS: {
274 if (pdu_len < 6)
275 {
276 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_VALUE, out);
277 }
278 uint16_t start = rd16(pdu + 1), qty = rd16(pdu + 3);
279 uint8_t bc = pdu[5];
280 if (qty < 1 || qty > 1968 || bc != (uint8_t)((qty + 7) / 8) || pdu_len < (size_t)6 + bc)
281 {
282 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_VALUE, out);
283 }
284 if ((uint32_t)start + qty > PC_MODBUS_COILS)
285 {
286 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_ADDRESS, out);
287 }
288 for (uint16_t i = 0; i < qty; i++)
289 {
290 bool v = (pdu[6 + (i >> 3)] >> (i & 7)) & 1u;
291 bit_set(s_modbus.coils, (uint16_t)(start + i), v);
292 }
293 if (s_modbus.write_cb)
294 {
295 s_modbus.write_cb((uint8_t)fc, start, qty);
296 }
297 if (out_cap < 5)
298 {
299 return 0;
300 }
301 out[0] = (uint8_t)fc;
302 wr16(out + 1, start);
303 wr16(out + 3, qty);
304 return 5;
305 }
306
307 // FC16: write up to 123 holding registers; reply start + quantity.
308 case ModbusFunction::MODBUS_FC_WRITE_MULTIPLE_REGS: {
309 if (pdu_len < 6)
310 {
311 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_VALUE, out);
312 }
313 uint16_t start = rd16(pdu + 1), qty = rd16(pdu + 3);
314 uint8_t bc = pdu[5];
315 if (qty < 1 || qty > 123 || bc != (uint8_t)(qty * 2) || pdu_len < (size_t)6 + bc)
316 {
317 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_VALUE, out);
318 }
319 if ((uint32_t)start + qty > PC_MODBUS_HOLDING_REGS)
320 {
321 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_ADDRESS, out);
322 }
323 for (uint16_t i = 0; i < qty; i++)
324 {
325 s_modbus.holding[start + i] = rd16(pdu + 6 + i * 2);
326 }
327 if (s_modbus.write_cb)
328 {
329 s_modbus.write_cb((uint8_t)fc, start, qty);
330 }
331 if (out_cap < 5)
332 {
333 return 0;
334 }
335 out[0] = (uint8_t)fc;
336 wr16(out + 1, start);
337 wr16(out + 3, qty);
338 return 5;
339 }
340
341 // FC22: mask-write one holding register: reg = (reg AND And_Mask) OR (Or_Mask AND NOT And_Mask). Echo.
342 case ModbusFunction::MODBUS_FC_MASK_WRITE_REG: {
343 if (pdu_len < 7)
344 {
345 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_VALUE, out);
346 }
347 uint16_t addr = rd16(pdu + 1);
348 uint16_t and_mask = rd16(pdu + 3);
349 uint16_t or_mask = rd16(pdu + 5);
350 if (addr >= PC_MODBUS_HOLDING_REGS)
351 {
352 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_ADDRESS, out);
353 }
354 uint16_t cur = s_modbus.holding[addr];
355 s_modbus.holding[addr] = (uint16_t)((cur & and_mask) | (or_mask & (uint16_t)~and_mask));
356 if (s_modbus.write_cb)
357 {
358 s_modbus.write_cb((uint8_t)fc, addr, 1);
359 }
360 if (out_cap < 7)
361 {
362 return 0;
363 }
364 memcpy(out, pdu, 7); // echo request (address + both masks)
365 return 7;
366 }
367
368 // FC23: read/write multiple holding registers in one transaction - the write is applied first, then
369 // the read reflects it (a contiguous read span + a contiguous write span). Reply is the read data.
370 case ModbusFunction::MODBUS_FC_READ_WRITE_MULTIPLE_REGS: {
371 if (pdu_len < 10)
372 {
373 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_VALUE, out);
374 }
375 uint16_t r_start = rd16(pdu + 1);
376 uint16_t r_qty = rd16(pdu + 3);
377 uint16_t w_start = rd16(pdu + 5);
378 uint16_t w_qty = rd16(pdu + 7);
379 uint8_t w_bc = pdu[9];
380 if (r_qty < 1 || r_qty > 125 || w_qty < 1 || w_qty > 121 || w_bc != (uint8_t)(w_qty * 2) ||
381 pdu_len < (size_t)10 + w_bc)
382 {
383 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_VALUE, out);
384 }
385 if ((uint32_t)r_start + r_qty > PC_MODBUS_HOLDING_REGS || (uint32_t)w_start + w_qty > PC_MODBUS_HOLDING_REGS)
386 {
387 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_DATA_ADDRESS, out);
388 }
389 for (uint16_t i = 0; i < w_qty; i++) // write first (§6.17)
390 {
391 s_modbus.holding[w_start + i] = rd16(pdu + 10 + i * 2);
392 }
393 if (s_modbus.write_cb)
394 {
395 s_modbus.write_cb((uint8_t)fc, w_start, w_qty);
396 }
397 uint8_t r_bytes = (uint8_t)(r_qty * 2);
398 if ((size_t)2 + r_bytes > out_cap)
399 {
400 return 0;
401 }
402 out[0] = (uint8_t)fc;
403 out[1] = r_bytes;
404 for (uint16_t i = 0; i < r_qty; i++)
405 {
406 wr16(out + 2 + i * 2, s_modbus.holding[r_start + i]);
407 }
408 return (size_t)2 + r_bytes;
409 }
410
411 // Any unsupported function code: reply with the ILLEGAL FUNCTION exception.
412 default:
413 return pdu_exception(fc, ModbusException::MODBUS_EX_ILLEGAL_FUNCTION, out);
414 }
415}
416
417size_t pc_modbus_process_adu(const uint8_t *req, size_t req_len, uint8_t *resp, size_t pc_resp_cap)
418{
419 if (req_len < 8 || pc_resp_cap < 8)
420 {
421 return 0; // need MBAP (7) + at least a function code
422 }
423
424 uint16_t tid = rd16(req);
425 uint16_t pid = rd16(req + 2);
426 uint16_t len = rd16(req + 4);
427 uint8_t uid = req[6];
428
429 if (pid != 0)
430 {
431 return 0; // not Modbus
432 }
433 if (len < 2 || (size_t)6 + len > req_len)
434 {
435 return 0; // length field disagrees with the frame
436 }
437
438 const uint8_t *pdu = req + 7;
439 size_t pdu_len = (size_t)len - 1; // len counts the unit id + the PDU
440
441 size_t rlen = pc_modbus_process_pdu(pdu, pdu_len, resp + 7, pc_resp_cap - 7);
442 if (rlen == 0)
443 {
444 return 0;
445 }
446
447 wr16(resp, tid); // echo transaction id
448 wr16(resp + 2, 0); // protocol id = 0
449 wr16(resp + 4, (uint16_t)(1 + rlen)); // length = unit id + response PDU
450 resp[6] = uid; // echo unit id
451 return 7 + rlen;
452}
453
454#if PC_ENABLE_MODBUS_RTU
455// CRC16-Modbus (init 0xFFFF, reflected poly 0xA001); transmitted low byte first. test_crc diffs the
456// shared engine against the loop that used to live here over every length 0..64.
457static uint16_t pc_modbus_crc16(const uint8_t *data, size_t len)
458{
459 return (uint16_t)pc_crc(&PC_CRC16_MODBUS, data, len);
460}
461
462size_t pc_modbus_rtu_process_adu(const uint8_t *req, size_t req_len, uint8_t *resp, size_t pc_resp_cap, uint8_t my_addr)
463{
464 if (req_len < 4 || pc_resp_cap < 4) // addr(1) + min PDU(1) + CRC(2)
465 {
466 return 0;
467 }
468
469 // Validate the trailing CRC over [addr .. last PDU byte] (low byte first).
470 uint16_t want = pc_modbus_crc16(req, req_len - 2);
471 uint16_t got = (uint16_t)(req[req_len - 2] | (req[req_len - 1] << 8));
472 if (want != got)
473 {
474 return 0; // corrupt frame - drop silently (no response), per Modbus RTU
475 }
476
477 uint8_t addr = req[0];
478 bool broadcast = (addr == 0);
479 if (!broadcast && addr != my_addr)
480 {
481 return 0; // not addressed to this slave
482 }
483
484 const uint8_t *pdu = req + 1;
485 size_t pdu_len = req_len - 3; // strip addr + 2 CRC bytes
486
487 size_t rlen = pc_modbus_process_pdu(pdu, pdu_len, resp + 1, pc_resp_cap - 3); // leave addr + CRC room
488 if (rlen == 0)
489 {
490 return 0;
491 }
492 if (broadcast)
493 {
494 return 0; // executed, but a broadcast gets no reply
495 }
496
497 resp[0] = my_addr;
498 uint16_t crc = pc_modbus_crc16(resp, 1 + rlen);
499 resp[1 + rlen] = (uint8_t)(crc & 0xFFu);
500 resp[2 + rlen] = (uint8_t)(crc >> 8);
501 return 1 + rlen + 2;
502}
503#endif // PC_ENABLE_MODBUS_RTU
504
505// ---------------------------------------------------------------------------
506// TCP transport (ESP32-only; the core above is host-tested standalone)
507// ---------------------------------------------------------------------------
508
509#if defined(ARDUINO)
510
511// Bytes available in the slot's rx ring.
512// Thin adapters over the transport RX read API - the ring is owned by transport;
513// this service never indexes rx_buffer or advances rx_tail itself.
514static size_t ring_avail(const TcpConn *c)
515{
516 return pc_conn_available(c->id);
517}
518static void ring_peek(const TcpConn *c, size_t off, uint8_t *dst, size_t n)
519{
520 pc_conn_peek(c->id, off, dst, n);
521}
522static void ring_consume(TcpConn *c, size_t n)
523{
524 pc_conn_consume(c->id, n);
525}
526
527static void raw_send(uint8_t slot, const void *data, size_t n)
528{
529 if (!pc_conn_active(slot) || n == 0)
530 {
531 return;
532 }
533 pc_conn_send(slot, data, (u16_t)n);
534 pc_conn_flush(slot);
535}
536
537static void close_conn(uint8_t slot)
538{
539 pc_conn_close(slot); // transport owns detach + slot reset + close
540}
541
542void pc_modbus_rx(uint8_t slot)
543{
544 TcpConn *conn = &conn_pool[slot];
545
546 // Frame complete ADUs out of the rx ring; a partial frame stays buffered.
547 for (;;)
548 {
549 size_t avail = ring_avail(conn);
550 if (avail < 8) // MBAP (7) + at least a function code
551 {
552 break;
553 }
554
555 uint8_t hdr[6];
556 ring_peek(conn, 0, hdr, 6);
557 uint16_t pid = (uint16_t)((hdr[2] << 8) | hdr[3]);
558 uint16_t len = (uint16_t)((hdr[4] << 8) | hdr[5]);
559 if (pid != 0 || len < 2 || len > (MODBUS_ADU_MAX - 6))
560 {
561 close_conn(slot); // not a Modbus TCP frame - drop the connection
562 return;
563 }
564 size_t frame_total = (size_t)6 + len;
565 if (avail < frame_total)
566 {
567 break; // wait for the rest of the frame
568 }
569
570 uint8_t adu[MODBUS_ADU_MAX];
571 ring_peek(conn, 0, adu, frame_total);
572 ring_consume(conn, frame_total);
573
574 uint8_t resp[MODBUS_ADU_MAX];
575 size_t rl = pc_modbus_process_adu(adu, frame_total, resp, sizeof(resp));
576 if (rl)
577 {
578 raw_send(slot, resp, rl);
579 }
580 }
581}
582
583// The Modbus ProtoHandler (Layer 5 dispatch seam) - only a data handler; a partial ADU waits in the
584// rx ring, so there is no per-connection accept/close/poll state. Returned by accessor (no session
585// dependency); proto_register_builtins() installs it.
586static const ProtoHandler s_modbus_handler = {nullptr, pc_modbus_rx, nullptr, nullptr};
587const ProtoHandler *pc_modbus_proto_handler(void)
588{
589 return &s_modbus_handler;
590}
591
592#else // !ARDUINO
593
594// Host builds test the pure ADU codec; there is no TCP transport handler.
595const ProtoHandler *pc_modbus_proto_handler(void)
596{
597 return nullptr;
598}
599
600#endif // ARDUINO
601
602#endif // PC_NEED_MODBUS
Parameterized CRC engine - one source of truth for every cyclic redundancy check.
constexpr pc_crc_params PC_CRC16_MODBUS
CRC-16/MODBUS. check = 0x4B37.
Definition crc.h:182
uint32_t pc_crc(const pc_crc_params *p, const uint8_t *data, size_t len)
One-shot CRC of len octets at data.
Definition crc.h:158
Zero-heap Modbus TCP slave/server (Modbus Application Protocol v1.1b3).
Layer 5 (Session) - per-protocol connection handler dispatch table.
#define PC_MODBUS_DISCRETE_INPUTS
Number of Modbus discrete inputs (FC 2), single-bit read-only (BSS, bit-packed).
#define PC_MODBUS_COILS
Number of Modbus coils (FC 1/5/15), single-bit R/W (BSS, bit-packed).
#define PC_MODBUS_INPUT_REGS
Number of Modbus input registers (FC 4), 16-bit read-only (BSS).
#define PC_MODBUS_HOLDING_REGS
Number of Modbus holding registers (FC 3/6/16), 16-bit R/W (BSS).
Per-protocol connection event/poll callbacks (Layer 5 dispatch vtable).
A single TCP connection context.
Definition tcp.h:72
uint8_t id
Fixed slot index (0 … MAX_CONNS-1).
Definition tcp.h:73
TcpConn conn_pool[CONN_POOL_SLOTS]
Static pool of connection contexts. Defined in tcp.cpp. Sized CONN_POOL_SLOTS: MAX_CONNS TCP slots pl...
Definition tcp.cpp:425
void pc_conn_flush(uint8_t slot)
Flush queued bytes / finish the send on slot (TLS-aware).
Definition tcp.cpp:561
void pc_conn_close(uint8_t slot)
Close connection slot gracefully (tcp_close), aborting if the FIN cannot be queued....
Definition tcp.cpp:645
bool pc_conn_send(uint8_t slot, const void *data, u16_t len)
Send len bytes on connection slot (copies data; TLS-aware).
Definition tcp.cpp:500
Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.