ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
s7comm.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 s7comm.cpp
6 * @brief Siemens S7comm PDU builder + parser (pure, host-tested; constants per Wireshark).
7 */
8
10
11#if PC_ENABLE_S7COMM
12
13#include <string.h>
14
15static size_t put16(uint8_t *p, uint16_t v)
16{
17 p[0] = (uint8_t)(v >> 8);
18 p[1] = (uint8_t)v;
19 return 2;
20}
21
22static uint16_t get16(const uint8_t *p)
23{
24 return (uint16_t)(((uint16_t)p[0] << 8) | p[1]);
25}
26
27// Write the 10-octet job/request header (protocol id, ROSCTR, redundancy, pdu-ref, lengths).
28static size_t write_job_header(uint8_t *buf, uint16_t pdu_ref, uint16_t param_len, uint16_t data_len)
29{
30 size_t p = 0;
31 buf[p++] = S7_PROTOCOL_ID;
32 buf[p++] = S7_ROSCTR_JOB;
33 p += put16(buf + p, 0); // redundancy id (reserved)
34 p += put16(buf + p, pdu_ref);
35 p += put16(buf + p, param_len);
36 p += put16(buf + p, data_len);
37 return p; // 10
38}
39
40size_t pc_s7_build_setup(uint8_t *buf, size_t cap, uint16_t pdu_ref, uint16_t max_amq_calling, uint16_t max_amq_called,
41 uint16_t pdu_size)
42{
43 if (!buf)
44 {
45 return 0;
46 }
47 const uint16_t param_len = 8;
48 size_t total = 10 + param_len;
49 if (total > cap)
50 {
51 return 0;
52 }
53 size_t p = write_job_header(buf, pdu_ref, param_len, 0);
54 buf[p++] = S7_FUNC_SETUP_COMM;
55 buf[p++] = 0x00; // reserved
56 p += put16(buf + p, max_amq_calling);
57 p += put16(buf + p, max_amq_called);
58 p += put16(buf + p, pdu_size);
59 return p;
60}
61
62size_t pc_s7_build_read_request(uint8_t *buf, size_t cap, uint16_t pdu_ref, const S7ReadItem *items, size_t n)
63{
64 if (!buf || !items || n == 0 || n > 0xFF)
65 {
66 return 0;
67 }
68 uint16_t param_len = (uint16_t)(2 + 12 * n); // func + count + items
69 size_t total = 10 + param_len;
70 if (total > cap)
71 {
72 return 0;
73 }
74 size_t p = write_job_header(buf, pdu_ref, param_len, 0);
75 buf[p++] = S7_FUNC_READ_VAR;
76 buf[p++] = (uint8_t)n;
77 for (size_t i = 0; i < n; i++)
78 {
79 const S7ReadItem &it = items[i];
80 buf[p++] = 0x12; // variable specification
81 buf[p++] = 0x0A; // length of the address spec that follows
82 buf[p++] = S7_SYNTAX_S7ANY; // syntax id
83 buf[p++] = it.transport_size;
84 p += put16(buf + p, it.count);
85 p += put16(buf + p, it.db_number);
86 buf[p++] = it.area;
87 uint32_t addr = it.byte_address << 3; // bit address = byte * 8 (bit offset 0)
88 buf[p++] = (uint8_t)(addr >> 16);
89 buf[p++] = (uint8_t)(addr >> 8);
90 buf[p++] = (uint8_t)(addr & 0xFF);
91 }
92 return p;
93}
94
95// The 2-octet data-item length is expressed in bits for the bit/byte/int data transport sizes, else in bytes
96// (the inverse of pc_s7_read_next_item's decode).
97static uint16_t s7_data_wire_len(uint8_t data_transport_size, uint16_t data_len)
98{
99 if (data_transport_size == S7_DTS_BIT || data_transport_size == S7_DTS_BYTE || data_transport_size == S7_DTS_INT)
100 {
101 return (uint16_t)(data_len * 8);
102 }
103 return data_len;
104}
105
106size_t pc_s7_build_write_request(uint8_t *buf, size_t cap, uint16_t pdu_ref, const S7WriteItem *items, size_t n)
107{
108 if (!buf || !items || n == 0 || n > 0xFF)
109 {
110 return 0;
111 }
112 uint16_t param_len = (uint16_t)(2 + 12 * n); // func + count + item specs (same 12-octet spec as a read)
113 // Sum the data section: each item is a 4-octet data header + the value bytes, even-padded except the last.
114 size_t data_len = 0;
115 for (size_t i = 0; i < n; i++)
116 {
117 if (items[i].data_len && !items[i].data)
118 {
119 return 0;
120 }
121 size_t item_bytes = 4 + items[i].data_len;
122 if (i + 1 < n && (items[i].data_len & 1)) // pad every item but the last to an even length
123 {
124 item_bytes++;
125 }
126 data_len += item_bytes;
127 }
128 if (data_len > 0xFFFF)
129 {
130 return 0;
131 }
132 size_t total = 10 + param_len + data_len;
133 if (total > cap)
134 {
135 return 0;
136 }
137
138 size_t p = write_job_header(buf, pdu_ref, param_len, (uint16_t)data_len);
139 buf[p++] = S7_FUNC_WRITE_VAR;
140 buf[p++] = (uint8_t)n;
141 for (size_t i = 0; i < n; i++) // parameter: one S7-ANY item spec per item (identical layout to a read)
142 {
143 const S7WriteItem &it = items[i];
144 buf[p++] = 0x12;
145 buf[p++] = 0x0A;
146 buf[p++] = S7_SYNTAX_S7ANY;
147 buf[p++] = it.transport_size;
148 p += put16(buf + p, it.count);
149 p += put16(buf + p, it.db_number);
150 buf[p++] = it.area;
151 uint32_t addr = it.byte_address << 3; // bit address = byte * 8 (bit offset 0)
152 buf[p++] = (uint8_t)(addr >> 16);
153 buf[p++] = (uint8_t)(addr >> 8);
154 buf[p++] = (uint8_t)(addr & 0xFF);
155 }
156 for (size_t i = 0; i < n; i++) // data: one data item per item
157 {
158 const S7WriteItem &it = items[i];
159 buf[p++] = 0x00; // return code (reserved in a request)
160 buf[p++] = it.data_transport_size;
161 p += put16(buf + p, s7_data_wire_len(it.data_transport_size, it.data_len));
162 if (it.data_len)
163 {
164 memcpy(buf + p, it.data, it.data_len);
165 p += it.data_len;
166 }
167 if (i + 1 < n && (it.data_len & 1)) // even-pad all but the last
168 {
169 buf[p++] = 0x00;
170 }
171 }
172 return p;
173}
174
175bool pc_s7_parse_header(const uint8_t *buf, size_t len, S7Header *out)
176{
177 if (!buf || !out || len < 10)
178 {
179 return false;
180 }
181 if (buf[0] != S7_PROTOCOL_ID)
182 {
183 return false;
184 }
185 out->rosctr = buf[1];
186 out->pdu_ref = get16(buf + 4);
187 out->param_len = get16(buf + 6);
188 out->data_len = get16(buf + 8);
189 out->error_class = 0;
190 out->error_code = 0;
191 out->header_len = (out->rosctr == S7_ROSCTR_ACK_DATA) ? 12 : 10; // Ack_Data adds a 2-octet error code
192 if (out->header_len == 12)
193 {
194 if (len < 12)
195 {
196 return false;
197 }
198 out->error_class = buf[10];
199 out->error_code = buf[11];
200 }
201 if (out->header_len + (size_t)out->param_len + (size_t)out->data_len > len)
202 {
203 return false; // not fully buffered
204 }
205 out->param = buf + out->header_len;
206 out->data = out->param + out->param_len;
207 return true;
208}
209
210bool pc_s7_read_next_item(const uint8_t *data, size_t data_len, size_t *offset, S7DataItem *out)
211{
212 if (!data || !offset || !out)
213 {
214 return false;
215 }
216 size_t o = *offset;
217 if (o + 4 > data_len) // return code + transport size + 2-octet length
218 {
219 return false;
220 }
221 out->return_code = data[o];
222 out->transport_size = data[o + 1];
223 uint16_t raw_len = get16(data + o + 2);
224
225 // The length is in bits for the bit/byte/int transport sizes, otherwise in bytes.
226 size_t bytes;
227 if (out->transport_size == S7_DTS_BIT || out->transport_size == S7_DTS_BYTE || out->transport_size == S7_DTS_INT)
228 {
229 bytes = (raw_len + 7) / 8;
230 }
231 else
232 {
233 bytes = raw_len;
234 }
235
236 if (o + 4 + bytes > data_len)
237 {
238 return false;
239 }
240 out->data = data + o + 4;
241 out->data_len = bytes;
242
243 o += 4 + bytes;
244 // Each item except the last is padded to an even length; skip the fill byte.
245 if (o < data_len && (bytes & 1))
246 {
247 o++;
248 }
249 *offset = o;
250 return true;
251}
252
253#endif // PC_ENABLE_S7COMM
Siemens S7comm PDU codec (PC_ENABLE_S7COMM) - zero-heap builder + parser for the S7-300/400 communica...