ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
robotics.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 robotics.cpp
6 * @brief OPC UA for Robotics (OPC 40010-1) MotionDeviceSystem model - resolver implementation.
7 *
8 * A fixed node table (no heap): each container node (MotionDeviceSystem, the MotionDevices / Controllers
9 * / SafetyStates folders, MotionDevice, ParameterSet, Axes, each Axis_k, Controller, Software,
10 * SafetyState) answers a Browse with its child ReferenceDescriptions, and each leaf Variable answers a
11 * Read out of the bound RoboticsMotionDeviceSystem struct. The Objects folder (ns0 i=85) organizes the
12 * MotionDeviceSystem so a client discovers it from the root. Axes are parametric: Axis_k lives at
13 * AXIS_BASE + k*10 (k = 1..axis_count) and its four variables at +1..+4.
14 */
15
17
18#if PC_ENABLE_ROBOTICS
19
20#include <string.h> // strnlen (string.h is allowed; the no-stdlib rule is about stdlib.h/malloc)
21
22// ---------------------------------------------------------------------------
23// Node identifiers (namespace PC_ROBOTICS_NS). Objects end in 0; their variables count up from it.
24// ---------------------------------------------------------------------------
25namespace
26{
27enum : uint32_t // NOSONAR(cpp:S3642): anonymous table of OPC-UA node ids used as bare uint32_t (arithmetic + wire
28 // compares); enum class would force a cast at every use
29{
30 MOTIONDEVICESYSTEM = 6000,
31
32 MOTIONDEVICES = 6100, // Folder
33
34 MOTIONDEVICE = 6200,
35 MD_MANUFACTURER = 6201,
36 MD_MODEL = 6202,
37 MD_PRODUCTCODE = 6203,
38 MD_SERIAL = 6204,
39 MD_CATEGORY = 6205,
40 MD_PARAMSET = 6210,
41 MDP_ONPATH = 6211,
42 MDP_INCONTROL = 6212,
43 MDP_SPEEDOVERRIDE = 6213,
44 MD_AXES = 6300, // Folder
45
46 // Axes: Axis_k object = AXIS_BASE + k*10 (k = 1..axis_count); its vars at +1..+4.
47 AXIS_BASE = 6400,
48
49 CONTROLLERS = 6600, // Folder
50 CONTROLLER = 6610,
51 CT_MANUFACTURER = 6611,
52 CT_MODEL = 6612,
53 CT_PRODUCTCODE = 6613,
54 CT_SERIAL = 6614,
55 CT_SOFTWARE = 6620,
56 SW_MANUFACTURER = 6621,
57 SW_MODEL = 6622,
58 SW_REVISION = 6623,
59
60 SAFETYSTATES = 6700, // Folder
61 SAFETYSTATE = 6710,
62 SS_PARAMSET = 6720,
63 SSP_OPMODE = 6721,
64 SSP_ESTOP = 6722,
65 SSP_PSTOP = 6723,
66};
67
68// Axis variable sub-ids (offset from the Axis_k object id).
69enum : uint32_t // NOSONAR(cpp:S3642): anonymous table of OPC-UA sub-ids used as bare uint32_t offsets; enum class would
70 // force a cast at every use
71{
72 AXVAR_POSITION = 1,
73 AXVAR_SPEED = 2,
74 AXVAR_ACCEL = 3,
75 AXVAR_PROFILE = 4,
76};
77
78// All robotics model state, owned by one instance (internal linkage): the bound system pointer the
79// resolvers read from, plus the per-axis BrowseName strings (filled at bind, referenced by Browse).
80// Null until pc_robotics_bind(); a Read/Browse before binding is a clean miss (BadNodeIdUnknown), so
81// the server never dereferences a null model.
82struct RoboticsCtx
83{
84 const RoboticsMotionDeviceSystem *mds;
85 char axis_name[PC_ROBOTICS_AXES][12]; // "Axis_" + up to 2 digits + null
86};
87RoboticsCtx s_robotics = {nullptr, {{0}}};
88
89// Fill "Axis_k" (k = 1..PC_ROBOTICS_AXES) into the owned name table (no snprintf: a tiny fixed render).
90void build_axis_names(RoboticsCtx *c)
91{
92 for (uint32_t k = 0; k < PC_ROBOTICS_AXES; k++)
93 {
94 char *b = c->axis_name[k];
95 uint32_t idx = k + 1; // 1-based BrowseName index
96 b[0] = 'A';
97 b[1] = 'x';
98 b[2] = 'i';
99 b[3] = 's';
100 b[4] = '_';
101 size_t o = 5;
102 if (idx >= 10) // GCOVR_EXCL_LINE two-digit render; unreachable while PC_ROBOTICS_AXES (6) <= 9
103 {
104 b[o++] = (char)('0' + (idx / 10)); // GCOVR_EXCL_LINE
105 }
106 b[o++] = (char)('0' + (idx % 10));
107 b[o] = '\0';
108 }
109}
110
111// Decode an axis node id: ids AXIS_BASE + k*10 + sub, k = 1.., sub = 0 (the Axis object) / 1..4 (a var).
112// Returns true (with k 1-based + sub) only when k is within the bound axis_count; sub is the caller's to
113// range-check (0 for a Browse of the Axis object, 1..4 for a Read of an axis variable).
114bool decode_axis(uint32_t id, uint32_t axis_count, uint32_t *k_out, uint32_t *sub_out)
115{
116 if (id <= AXIS_BASE)
117 {
118 return false;
119 }
120 uint32_t rel = id - AXIS_BASE;
121 uint32_t k = rel / 10;
122 uint32_t sub = rel % 10;
123 if (k < 1 || k > axis_count)
124 {
125 return false;
126 }
127 *k_out = k;
128 *sub_out = sub;
129 return true;
130}
131
132// --- Variant fillers (leaf values) -----------------------------------------
133void set_str(OpcUaVariant *o, const char *s)
134{
135 o->type = OpcUaVariantType::OPCUA_VAR_STRING;
136 o->str = s ? s : "";
137 o->str_len = (int32_t)strnlen(o->str, 0xFFFF); // bound the scan: a model string is a caller-owned C string
138}
139void set_i32(OpcUaVariant *o, int32_t v)
140{
141 o->type = OpcUaVariantType::OPCUA_VAR_INT32;
142 o->i32 = v;
143}
144void set_f64(OpcUaVariant *o, double v)
145{
146 o->type = OpcUaVariantType::OPCUA_VAR_DOUBLE;
147 o->f64 = v;
148}
149void set_bool(OpcUaVariant *o, bool v)
150{
151 o->type = OpcUaVariantType::OPCUA_VAR_BOOL;
152 o->b = v;
153}
154
155// --- Browse helpers --------------------------------------------------------
156// Append one ReferenceDescription (bounded by @p max). BrowseName + DisplayName share @p name. A folder
157// points at its members with Organizes (35); an object points at its component objects/variables with
158// HasComponent (47) - the umati simplification (see robotics.h scope note).
159int32_t add_ref(OpcUaReference *out, int32_t n, uint32_t max, uint32_t target_id, const char *name, uint32_t node_class,
160 bool organizes)
161{
162 if ((uint32_t)n >= max)
163 {
164 return n;
165 }
166 OpcUaReference *r = &out[n];
167 r->ref_type_id = organizes ? OPCUA_REFTYPE_ORGANIZES : OPCUA_REFTYPE_HAS_COMPONENT;
168 r->is_forward = true;
169 r->target_ns = PC_ROBOTICS_NS;
170 r->target_id = target_id;
171 r->browse_name_ns = PC_ROBOTICS_NS;
172 r->browse_name = name;
173 r->display_name = name;
174 r->node_class = node_class;
175 r->type_def_id =
176 (node_class == OPCUA_NODECLASS_VARIABLE) ? OPCUA_TYPEDEF_BASE_DATA_VARIABLE : OPCUA_TYPEDEF_BASE_OBJECT;
177 return n + 1;
178}
179int32_t add_obj(OpcUaReference *out, int32_t n, uint32_t max, uint32_t id, const char *name)
180{
181 return add_ref(out, n, max, id, name, OPCUA_NODECLASS_OBJECT, /*organizes=*/false);
182}
183int32_t add_folder_member(OpcUaReference *out, int32_t n, uint32_t max, uint32_t id, const char *name)
184{
185 return add_ref(out, n, max, id, name, OPCUA_NODECLASS_OBJECT, /*organizes=*/true);
186}
187int32_t add_var(OpcUaReference *out, int32_t n, uint32_t max, uint32_t id, const char *name)
188{
189 return add_ref(out, n, max, id, name, OPCUA_NODECLASS_VARIABLE, /*organizes=*/false);
190}
191} // namespace
192
193// ---------------------------------------------------------------------------
194// Public API
195// ---------------------------------------------------------------------------
196void pc_robotics_bind(const RoboticsMotionDeviceSystem *mds)
197{
198 s_robotics.mds = mds;
199 build_axis_names(&s_robotics);
200}
201
202bool pc_robotics_read(uint16_t ns, uint32_t id, uint32_t attribute, OpcUaVariant *out)
203{
204 const RoboticsMotionDeviceSystem *mds = s_robotics.mds;
205 if (!mds || ns != PC_ROBOTICS_NS || attribute != OPCUA_ATTR_VALUE)
206 {
207 return false;
208 }
209
210 // Axis variables (parametric): AXIS_BASE + k*10 + {1..4}.
211 uint32_t k = 0;
212 uint32_t sub = 0;
213 if (decode_axis(id, mds->device.axis_count, &k, &sub) && sub >= AXVAR_POSITION && sub <= AXVAR_PROFILE)
214 {
215 const RoboticsAxis *ax = &mds->device.axes[k - 1];
216 // The guard above already pins sub to AXVAR_POSITION..AXVAR_PROFILE, so the default edge is
217 // unreachable; gcov cannot drop a single switch edge, so the dispatch line is excluded whole.
218 switch (sub) // GCOVR_EXCL_LINE
219 {
220 case AXVAR_POSITION:
221 set_f64(out, ax->actual_position);
222 return true;
223 case AXVAR_SPEED:
224 set_f64(out, ax->actual_speed);
225 return true;
226 case AXVAR_ACCEL:
227 set_f64(out, ax->actual_acceleration);
228 return true;
229 case AXVAR_PROFILE:
230 set_i32(out, (int32_t)ax->motion_profile);
231 return true;
232 default: // GCOVR_EXCL_LINE unreachable: sub is pinned to 1..4 by the guard above
233 return false; // GCOVR_EXCL_LINE
234 }
235 }
236
237 switch (id)
238 {
239 // MotionDevice identity
240 case MD_MANUFACTURER:
241 set_str(out, mds->device.manufacturer);
242 return true;
243 case MD_MODEL:
244 set_str(out, mds->device.model);
245 return true;
246 case MD_PRODUCTCODE:
247 set_str(out, mds->device.product_code);
248 return true;
249 case MD_SERIAL:
250 set_str(out, mds->device.serial_number);
251 return true;
252 case MD_CATEGORY:
253 set_i32(out, (int32_t)mds->device.category);
254 return true;
255 // MotionDevice ParameterSet
256 case MDP_ONPATH:
257 set_bool(out, mds->device.on_path);
258 return true;
259 case MDP_INCONTROL:
260 set_bool(out, mds->device.in_control);
261 return true;
262 case MDP_SPEEDOVERRIDE:
263 set_f64(out, mds->device.speed_override);
264 return true;
265 // Controller identity + Software
266 case CT_MANUFACTURER:
267 set_str(out, mds->controller.manufacturer);
268 return true;
269 case CT_MODEL:
270 set_str(out, mds->controller.model);
271 return true;
272 case CT_PRODUCTCODE:
273 set_str(out, mds->controller.product_code);
274 return true;
275 case CT_SERIAL:
276 set_str(out, mds->controller.serial_number);
277 return true;
278 case SW_MANUFACTURER:
279 set_str(out, mds->controller.sw_manufacturer);
280 return true;
281 case SW_MODEL:
282 set_str(out, mds->controller.sw_model);
283 return true;
284 case SW_REVISION:
285 set_str(out, mds->controller.sw_revision);
286 return true;
287 // SafetyState ParameterSet
288 case SSP_OPMODE:
289 set_i32(out, (int32_t)mds->safety.operational_mode);
290 return true;
291 case SSP_ESTOP:
292 set_bool(out, mds->safety.emergency_stop);
293 return true;
294 case SSP_PSTOP:
295 set_bool(out, mds->safety.protective_stop);
296 return true;
297 default:
298 return false;
299 }
300}
301
302int32_t pc_robotics_browse(uint16_t ns, uint32_t id, OpcUaReference *out, uint32_t max)
303{
304 const RoboticsMotionDeviceSystem *mds = s_robotics.mds;
305 if (!mds)
306 {
307 return -1;
308 }
309
310 // The Objects folder (ns0 i=85) organizes the MotionDeviceSystem so a client finds it from the root.
311 if (ns == 0 && id == 85)
312 {
313 return add_folder_member(out, 0, max, MOTIONDEVICESYSTEM, mds->name ? mds->name : "MotionDeviceSystem");
314 }
315
316 if (ns != PC_ROBOTICS_NS)
317 {
318 return -1;
319 }
320
321 // An Axis_k object: browse its four variables (before the switch, since ids are parametric).
322 uint32_t k = 0;
323 uint32_t sub = 0;
324 if (decode_axis(id, mds->device.axis_count, &k, &sub) && sub == 0)
325 {
326 uint32_t base = AXIS_BASE + k * 10;
327 int32_t n = 0;
328 n = add_var(out, n, max, base + AXVAR_POSITION, "ActualPosition");
329 n = add_var(out, n, max, base + AXVAR_SPEED, "ActualSpeed");
330 n = add_var(out, n, max, base + AXVAR_ACCEL, "ActualAcceleration");
331 n = add_var(out, n, max, base + AXVAR_PROFILE, "MotionProfile");
332 return n;
333 }
334
335 int32_t n = 0;
336 switch (id)
337 {
338 case MOTIONDEVICESYSTEM:
339 n = add_obj(out, n, max, MOTIONDEVICES, "MotionDevices");
340 n = add_obj(out, n, max, CONTROLLERS, "Controllers");
341 n = add_obj(out, n, max, SAFETYSTATES, "SafetyStates");
342 return n;
343 case MOTIONDEVICES:
344 return add_folder_member(out, 0, max, MOTIONDEVICE, "MotionDevice");
345 case MOTIONDEVICE:
346 n = add_var(out, n, max, MD_MANUFACTURER, "Manufacturer");
347 n = add_var(out, n, max, MD_MODEL, "Model");
348 n = add_var(out, n, max, MD_PRODUCTCODE, "ProductCode");
349 n = add_var(out, n, max, MD_SERIAL, "SerialNumber");
350 n = add_var(out, n, max, MD_CATEGORY, "MotionDeviceCategory");
351 n = add_obj(out, n, max, MD_PARAMSET, "ParameterSet");
352 n = add_obj(out, n, max, MD_AXES, "Axes");
353 return n;
354 case MD_PARAMSET:
355 n = add_var(out, n, max, MDP_ONPATH, "OnPath");
356 n = add_var(out, n, max, MDP_INCONTROL, "InControl");
357 n = add_var(out, n, max, MDP_SPEEDOVERRIDE, "SpeedOverride");
358 return n;
359 case MD_AXES:
360 for (uint32_t a = 1; a <= mds->device.axis_count && a <= PC_ROBOTICS_AXES; a++)
361 {
362 n = add_folder_member(out, n, max, AXIS_BASE + a * 10, s_robotics.axis_name[a - 1]);
363 }
364 return n;
365 case CONTROLLERS:
366 return add_folder_member(out, 0, max, CONTROLLER, "Controller");
367 case CONTROLLER:
368 n = add_var(out, n, max, CT_MANUFACTURER, "Manufacturer");
369 n = add_var(out, n, max, CT_MODEL, "Model");
370 n = add_var(out, n, max, CT_PRODUCTCODE, "ProductCode");
371 n = add_var(out, n, max, CT_SERIAL, "SerialNumber");
372 n = add_obj(out, n, max, CT_SOFTWARE, "Software");
373 return n;
374 case CT_SOFTWARE:
375 n = add_var(out, n, max, SW_MANUFACTURER, "Manufacturer");
376 n = add_var(out, n, max, SW_MODEL, "Model");
377 n = add_var(out, n, max, SW_REVISION, "SoftwareRevision");
378 return n;
379 case SAFETYSTATES:
380 return add_folder_member(out, 0, max, SAFETYSTATE, "SafetyState");
381 case SAFETYSTATE:
382 return add_obj(out, 0, max, SS_PARAMSET, "ParameterSet");
383 case SS_PARAMSET:
384 n = add_var(out, n, max, SSP_OPMODE, "OperationalMode");
385 n = add_var(out, n, max, SSP_ESTOP, "EmergencyStop");
386 n = add_var(out, n, max, SSP_PSTOP, "ProtectiveStop");
387 return n;
388 default:
389 return -1; // a leaf Variable (no children) or an unknown node
390 }
391}
392
393void pc_robotics_install(const RoboticsMotionDeviceSystem *mds)
394{
395 pc_robotics_bind(mds);
396 pc_opcua_set_read_handler(pc_robotics_read);
397 pc_opcua_set_browse_handler(pc_robotics_browse);
398}
399
400#endif // PC_ENABLE_ROBOTICS
#define PC_ROBOTICS_AXES
Number of Axes the robotics MotionDevice exposes (default 6; must fit PC_OPCUA_REF_MAX).
#define PC_ROBOTICS_NS
NamespaceIndex the robotics MotionDeviceSystem nodes live at (default 1).
OPC UA for Robotics (OPC 40010-1) MotionDevice information model (PC_ENABLE_ROBOTICS).