Branch data Line data Source code
1 : : /*
2 : : * Copyright 2025 Jinwoo Sung
3 : : *
4 : : * Licensed under the Apache License, Version 2.0 (the "License");
5 : : * you may not use this file except in compliance with the License.
6 : : * You may obtain a copy of the License at
7 : : *
8 : : * http://www.apache.org/licenses/LICENSE-2.0
9 : : *
10 : : * Unless required by applicable law or agreed to in writing, software
11 : : * distributed under the License is distributed on an "AS IS" BASIS,
12 : : * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 : : * See the License for the specific language governing permissions and
14 : : * limitations under the License.
15 : : */
16 : :
17 : : #pragma once
18 : :
19 : : #include <optional>
20 : : #include <string>
21 : :
22 : : #include "wirestead/base/constants.hpp"
23 : : #include "wirestead/util/input_validator.hpp"
24 : :
25 : : namespace wirestead {
26 : : namespace config {
27 : :
28 : : struct SerialConfig {
29 : : #ifdef _WIN32
30 : : std::string device = "COM1";
31 : : #else
32 : : std::string device = "/dev/ttyUSB0";
33 : : #endif
34 : : unsigned baud_rate = 115200;
35 : : unsigned char_size = 8; // 5,6,7,8
36 : : enum class Parity { None, Even, Odd } parity = Parity::None;
37 : : unsigned stop_bits = 1; // 1 or 2
38 : : enum class Flow { None, Software, Hardware } flow = Flow::None;
39 : :
40 : : size_t read_chunk = base::constants::DEFAULT_READ_BUFFER_SIZE;
41 : :
42 : : // Ask the kernel driver to hand bytes up as soon as they arrive instead of
43 : : // waiting out its buffering timer. On Linux this is ASYNC_LOW_LATENCY, and
44 : : // it matters most on USB serial adapters: an FTDI defaults to a 16 ms
45 : : // latency timer, so a 1 ms packet at 115200 baud can still reach the
46 : : // callback 16 ms late — enough to break a control loop on its own.
47 : : //
48 : : // Best effort by design. Drivers that have no such timer (CDC-ACM, most
49 : : // native UARTs) reject the request, and that is not an error: the port opens
50 : : // and runs normally either way. Set false to leave the driver's default
51 : : // alone, which trades latency for fewer wakeups.
52 : : bool low_latency = true;
53 : :
54 : : // Half-duplex RS-485, the wiring behind Dynamixel servos, Modbus RTU and a
55 : : // good deal of industrial sensing. The driver has to drive the transceiver's
56 : : // direction pin around each write, which is not something a caller can do
57 : : // from userspace at the right instant - hence a setting rather than advice.
58 : : //
59 : : // Linux only (TIOCSRS485). Adapters that switch direction in hardware need
60 : : // none of this and will refuse it; that refusal is not an error.
61 : : struct Rs485 {
62 : : bool enabled = false;
63 : : // Logical level RTS takes while transmitting. The usual wiring wants it
64 : : // high to send, but the opposite exists and produces a link that looks
65 : : // dead in one direction only.
66 : : bool rts_on_send = true;
67 : : // Whether the receiver stays on during transmit. Off for true half duplex,
68 : : // which is what stops a device hearing its own echo.
69 : : bool rx_during_tx = false;
70 : : // Milliseconds the driver holds the direction pin either side of the
71 : : // frame. Both default to 0 because most transceivers need nothing; slow
72 : : // ones need a millisecond or two, and no model can guess which - this is
73 : : // the knob for the hardware in front of you.
74 : : unsigned delay_rts_before_send_ms = 0;
75 : : unsigned delay_rts_after_send_ms = 0;
76 : : } rs485;
77 : :
78 : : // Modem control lines, engaged only when set. std::nullopt means "leave the
79 : : // driver's default alone", which is a different request from "drive it low":
80 : : // an Arduino resets when DTR is asserted at open, so a driver that must not
81 : : // reboot the board sets dtr=false explicitly, while one that has no opinion
82 : : // leaves it unset.
83 : : std::optional<bool> dtr;
84 : : std::optional<bool> rts;
85 : :
86 : : bool reopen_on_error = true; // Attempt to reopen on device disconnection/error
87 : :
88 : : // Reopen the port when no data has been *received* for this long. 0 disables
89 : : // it, which is the default.
90 : : //
91 : : // The failure this catches is a device that stops streaming without ever
92 : : // reporting an error - a wedged USB adapter, a sensor that stopped talking -
93 : : // where every other mechanism here sees a perfectly healthy open port and
94 : : // waits forever.
95 : : //
96 : : // Receive-only on purpose, unlike the TCP idle timeout, which any traffic in
97 : : // either direction resets. A driver polling a mute device writes on schedule
98 : : // and would keep a bidirectional timer alive forever, which is exactly the
99 : : // case worth catching.
100 : : //
101 : : // Expiry runs the same path as a read error, so reopen_on_error decides
102 : : // whether the port is reopened or the link goes to Error.
103 : : unsigned rx_idle_timeout_ms = 0;
104 : : size_t backpressure_threshold = base::constants::DEFAULT_BACKPRESSURE_THRESHOLD;
105 : : base::constants::BackpressureStrategy backpressure_strategy = base::constants::BackpressureStrategy::Reliable;
106 : : bool enable_memory_pool = true;
107 : : // Controls whether callback exceptions halt the link (true) or trigger the normal retry flow (false)
108 : : bool stop_on_callback_exception = false;
109 : :
110 : : unsigned retry_interval_ms = base::constants::DEFAULT_RETRY_INTERVAL_MS;
111 : : int max_retries = base::constants::DEFAULT_MAX_RETRIES;
112 : :
113 : : // Opt into the shared IoContextManager singleton instead of a dedicated
114 : : // io_context + thread (the default since #440). Only meaningful for
115 : : // deliberately trading per-instance parallelism for reduced thread/memory
116 : : // overhead across many instances in one process.
117 : : bool use_shared_context = false;
118 : :
119 : : // Validation methods
120 : 19 : bool is_valid() const {
121 [ + - ]: 18 : return read_chunk >= base::constants::MIN_READ_BUFFER_SIZE && read_chunk <= base::constants::MAX_READ_BUFFER_SIZE &&
122 [ + + + + ]: 18 : util::InputValidator::is_valid_device_path(device) && baud_rate >= base::constants::MIN_BAUD_RATE &&
123 [ + + + + : 14 : baud_rate <= base::constants::MAX_BAUD_RATE && char_size >= 5 && char_size <= 8 &&
+ - ]
124 [ + + + + : 12 : (stop_bits == 1 || stop_bits == 2) && retry_interval_ms >= base::constants::MIN_RETRY_INTERVAL_MS &&
+ + ]
125 [ + + ]: 10 : retry_interval_ms <= base::constants::MAX_RETRY_INTERVAL_MS &&
126 [ + + ]: 9 : backpressure_threshold >= base::constants::MIN_BACKPRESSURE_THRESHOLD &&
127 [ + + ]: 8 : backpressure_threshold <= base::constants::MAX_BACKPRESSURE_THRESHOLD &&
128 [ - + - - ]: 7 : (rx_idle_timeout_ms == 0 || (rx_idle_timeout_ms >= base::constants::MIN_IDLE_TIMEOUT_MS &&
129 [ + + - - ]: 37 : rx_idle_timeout_ms <= base::constants::MAX_IDLE_TIMEOUT_MS)) &&
130 [ + + + + : 26 : (max_retries == -1 || (max_retries >= 0 && max_retries <= base::constants::MAX_RETRIES_LIMIT));
- + ]
131 : : }
132 : :
133 : : // Apply validation and clamp values to valid ranges
134 : 56 : void validate_and_clamp() {
135 : : // Same bounds the TCP and UDS configs apply to read_buffer_size: this is
136 : : // the per-connection userspace read buffer under a different name, and an
137 : : // unclamped 0 reaches the transport as rx_.resize(0).
138 [ + + ]: 56 : if (read_chunk < base::constants::MIN_READ_BUFFER_SIZE) {
139 : 1 : read_chunk = base::constants::MIN_READ_BUFFER_SIZE;
140 [ + + ]: 55 : } else if (read_chunk > base::constants::MAX_READ_BUFFER_SIZE) {
141 : 1 : read_chunk = base::constants::MAX_READ_BUFFER_SIZE;
142 : : }
143 : :
144 [ + + ]: 56 : if (baud_rate < base::constants::MIN_BAUD_RATE) {
145 : 1 : baud_rate = base::constants::MIN_BAUD_RATE;
146 [ + + ]: 55 : } else if (baud_rate > base::constants::MAX_BAUD_RATE) {
147 : 1 : baud_rate = base::constants::MAX_BAUD_RATE;
148 : : }
149 : :
150 [ + + ]: 56 : if (char_size < 5)
151 : 1 : char_size = 5;
152 [ + + ]: 55 : else if (char_size > 8)
153 : 1 : char_size = 8;
154 : :
155 [ + + + + ]: 56 : if (stop_bits != 1 && stop_bits != 2) stop_bits = 1;
156 : :
157 [ + + ]: 56 : if (retry_interval_ms < base::constants::MIN_RETRY_INTERVAL_MS) {
158 : 9 : retry_interval_ms = base::constants::MIN_RETRY_INTERVAL_MS;
159 [ + + ]: 47 : } else if (retry_interval_ms > base::constants::MAX_RETRY_INTERVAL_MS) {
160 : 1 : retry_interval_ms = base::constants::MAX_RETRY_INTERVAL_MS;
161 : : }
162 : :
163 [ + + ]: 56 : if (backpressure_threshold < base::constants::MIN_BACKPRESSURE_THRESHOLD) {
164 : 1 : backpressure_threshold = base::constants::MIN_BACKPRESSURE_THRESHOLD;
165 [ + + ]: 55 : } else if (backpressure_threshold > base::constants::MAX_BACKPRESSURE_THRESHOLD) {
166 : 1 : backpressure_threshold = base::constants::MAX_BACKPRESSURE_THRESHOLD;
167 : : }
168 : :
169 [ + + - + ]: 56 : if (rx_idle_timeout_ms != 0 && rx_idle_timeout_ms > base::constants::MAX_IDLE_TIMEOUT_MS) {
170 : 0 : rx_idle_timeout_ms = base::constants::MAX_IDLE_TIMEOUT_MS;
171 : : }
172 : :
173 [ + + + - ]: 56 : if (max_retries != -1 && max_retries > base::constants::MAX_RETRIES_LIMIT) {
174 : 1 : max_retries = base::constants::MAX_RETRIES_LIMIT;
175 : : }
176 : 56 : }
177 : : };
178 : :
179 : : } // namespace config
180 : : } // namespace wirestead
|