ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
southbound.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file southbound.h
6 * @brief Southbound protocol-driver framework (PROTOCORE_ENABLE_SOUTHBOUND).
7 *
8 * The northbound surface of this library is HTTP/WS/SNMP/etc. to a controller; the *southbound* surface
9 * is the field devices it polls and drives (a Modbus slave, a BACnet controller, a raw sensor over
10 * SPI/I2C/UART). Today Modbus master is the one such driver, hand-wired by the app. This is the uniform
11 * seam every southbound driver plugs into, so the app addresses any field device the same way regardless
12 * of the wire protocol underneath: register a driver (a small vtable + its instance context), then
13 * read/write *points* (registers, coils, objects) by driver name through one facade.
14 *
15 * One owner, one API: the framework owns driver lookup + dispatch; each driver owns its own transport
16 * (it is handed the point id and returns the value, doing whatever Modbus/BACnet/GPIO I/O it needs). The
17 * block (matrix) read/write is the atomic multi-point path - a contiguous span of points moved in one
18 * driver call, which a protocol like Modbus can satisfy as a single request.
19 *
20 * Pure registry + dispatch: no heap, no stdlib, host-testable with a fake driver. Drivers are borrowed
21 * (the caller keeps the SouthboundDriver + its ctx alive for the registry's lifetime).
22 *
23 * A caller sets the members a call takes, invokes it through ::Southbound, and reads the outcome off the
24 * same handle. The module exports one symbol, @ref Southbound; everything in southbound.c has internal
25 * linkage.
26 */
27
28#ifndef PROTOCORE_SOUTHBOUND_H
29#define PROTOCORE_SOUTHBOUND_H
30
31#include "protocore_config.h" // the entry point: protocore_types.h for the widths
32
33#if PROTOCORE_ENABLE_SOUTHBOUND
34
36
37// Southbound result codes. A dispatch reports SB_OK / a count, or a negative code, through
38// SouthboundNs::i32, so a caller's `< 0` and `== SB_OK` checks stay cast-free. A driver may also
39// return its own negative transport error, which is passed through unchanged.
40#define SB_OK 0 ///< success.
41#define SB_ERR_NOT_FOUND -1 ///< no registered driver by that name.
42#define SB_ERR_UNSUPPORTED -2 ///< the driver does not implement that operation.
43#define SB_ERR_ARG -3 ///< a null / out-of-range argument.
44#define SB_ERR_FULL -4 ///< the registry is full (registration only).
45#define SB_ERR_DUP -5 ///< a driver with that name is already registered.
46
47/**
48 * @brief One southbound driver: a vtable over an application-owned transport context.
49 *
50 * @p read / @p write move a single point; @p read_block / @p write_block move a contiguous span of
51 * points atomically (may be null - the framework then reports SB_ERR_UNSUPPORTED). Every callback takes
52 * the driver's own @p ctx first. A callback returns SB_OK / a count on success, or a negative code on
53 * failure (its own transport error, propagated unchanged).
54 */
55typedef struct
56{
57 const char *name; ///< unique driver name (borrowed).
58 int (*read)(void *ctx, uint32_t point, int32_t *value_out); ///< read one point.
59 int (*write)(void *ctx, uint32_t point, int32_t value); ///< write one point.
60 int (*read_block)(void *ctx, uint32_t first, int32_t *out, size_t n); ///< read n points -> out (>=0 count).
61 int (*write_block)(void *ctx, uint32_t first, const int32_t *in, size_t n); ///< write n points (>=0 count).
62 void *ctx; ///< driver instance state (borrowed).
63} SouthboundDriver;
64
65/** @brief The one point a read or a write moves. */
66typedef struct
67{
68 uint32_t point; ///< the point id (register, coil, object) the call addresses
69 int32_t value; ///< the value a write carries
70 int32_t *value_out; ///< where a read lands the value it got
71} SouthboundPointArgs;
72
73/** @brief The contiguous span of points a block read or a block write moves in one driver call. */
74typedef struct
75{
76 uint32_t first; ///< the first point id of the span
77 int32_t *out; ///< where a block read lands the values it got
78 const int32_t *in; ///< the values a block write carries
79 size_t n; ///< how many points the span covers
80} SouthboundBlockArgs;
81
82/**
83 * @brief The southbound facade: register drivers, then move points by driver name.
84 *
85 * @c add takes the keyword-free name of the registration call; every other member carries the name of
86 * the operation it performs.
87 *
88 * @var SouthboundNs::name the driver a lookup or a dispatch addresses
89 * @var SouthboundNs::drv the driver an add registers (borrowed; must outlive the registry)
90 * @var SouthboundNs::point the one point a read or a write moves
91 * @var SouthboundNs::block the span of points a block read or a block write moves
92 * @var SouthboundNs::i32 SB_OK / a count from a registration or a dispatch, or a negative code
93 * @var SouthboundNs::n how many drivers the registry holds
94 * @var SouthboundNs::driver the driver a find matched, or null
95 * @var SouthboundNs::add register @c drv: SB_OK, SB_ERR_ARG, SB_ERR_DUP or SB_ERR_FULL
96 * @var SouthboundNs::clear drop all registrations
97 * @var SouthboundNs::count how many drivers are registered
98 * @var SouthboundNs::find look up @c name
99 * @var SouthboundNs::read read @c point.point from @c name
100 * @var SouthboundNs::write write @c point.value to @c point.point of @c name
101 * @var SouthboundNs::read_block read @c block.n points from @c name, starting at @c block.first
102 * @var SouthboundNs::write_block write @c block.n points to @c name, starting at @c block.first
103 */
104typedef struct
105{
106 const char *name; ///< the driver a lookup or a dispatch addresses
107 const SouthboundDriver *drv; ///< the driver an add registers
108 SouthboundPointArgs point; ///< the one point a read or a write moves
109 SouthboundBlockArgs block; ///< the span a block read or a block write moves
110 int32_t i32;
111 size_t n;
112 const SouthboundDriver *driver;
113} SouthboundVars;
114
115/** @brief The operands and the outcome. */
116extern SouthboundVars SouthboundV;
117
118/** @brief The entries. */
119typedef struct
120{
121 void (*const add)(uint8_t *work);
122 void (*const clear)(uint8_t *work);
123 void (*const count)(uint8_t *work);
124 void (*const find)(uint8_t *work);
125 void (*const read)(uint8_t *work);
126 void (*const write)(uint8_t *work);
127 void (*const read_block)(uint8_t *work);
128 void (*const write_block)(uint8_t *work);
129} SouthboundNs;
130
131// What the table binds, defined once in the .c and taking one parameter each: everything
132// else an entry needs is an operand in SouthboundV or a region of the borrow at a fixed offset.
133void protocore_southbound_add(uint8_t *work);
134void protocore_southbound_clear(uint8_t *work);
135void protocore_southbound_count(uint8_t *work);
136void protocore_southbound_find(uint8_t *work);
137void protocore_southbound_read(uint8_t *work);
138void protocore_southbound_write(uint8_t *work);
139void protocore_southbound_read_block(uint8_t *work);
140void protocore_southbound_write_block(uint8_t *work);
141
142// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
143// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
144// `Southbound.add(work)` resolves to a named function and becomes a DIRECT call. An extern table
145// leaves the call indirect and the symbol live at every level, -O2 -flto included.
146static const SouthboundNs Southbound __attribute__((unused)) = {
147 .add = protocore_southbound_add,
148 .clear = protocore_southbound_clear,
149 .count = protocore_southbound_count,
150 .find = protocore_southbound_find,
151 .read = protocore_southbound_read,
152 .write = protocore_southbound_write,
153 .read_block = protocore_southbound_read_block,
154 .write_block = protocore_southbound_write_block,
155};
156
157/**
158 * @brief The PROTOCORE_SOUTHBOUND_BORROW bytes this module's state lives in.
159 *
160 * Stated beside the namespace rather than on it: an entry takes a borrow, and this is where
161 * that borrow comes from. Taken once from the end of the pool, which no mark and no release
162 * walks, so the state lasts the life of the program.
163 *
164 * @return the span.
165 */
166uint8_t *protocore_southbound_span(void);
167
169
170#endif // PROTOCORE_ENABLE_SOUTHBOUND
171
172#endif // PROTOCORE_SOUTHBOUND_H
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
#define PROTOCORE_END_DECLS
Definition types.h:97