@technoculture/safeserial 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,69 @@
1
+ #pragma once
2
+ #include <cstdlib>
3
+ #include <cstdint>
4
+ #include <string>
5
+
6
+ /**
7
+ * Configuration for SafeSerial protocol.
8
+ *
9
+ * Parameters can be set via environment variables.
10
+ * If not set, defaults are used.
11
+ *
12
+ * Environment variables:
13
+ * SAFESERIAL_MAX_RETRIES - Maximum retry attempts (default: 10)
14
+ * SAFESERIAL_RETRY_TIMEOUT_MS - Timeout between retries in ms (default: 2000)
15
+ * SAFESERIAL_FRAGMENT_SIZE - Maximum fragment payload size (default: 256)
16
+ * SAFESERIAL_ACK_TIMEOUT_MS - Timeout waiting for ACK in ms (default: 500)
17
+ */
18
+
19
+ namespace SafeSerialConfig {
20
+
21
+ inline int getEnvInt(const char* name, int defaultValue) {
22
+ const char* val = std::getenv(name);
23
+ if (val == nullptr) return defaultValue;
24
+ try {
25
+ return std::stoi(val);
26
+ } catch (...) {
27
+ return defaultValue;
28
+ }
29
+ }
30
+
31
+ // Maximum number of retry attempts before giving up
32
+ inline uint8_t maxRetries() {
33
+ static uint8_t value = static_cast<uint8_t>(
34
+ getEnvInt("SAFESERIAL_MAX_RETRIES", 10)
35
+ );
36
+ return value;
37
+ }
38
+
39
+ // Timeout between retries in milliseconds
40
+ inline uint16_t retryTimeoutMs() {
41
+ static uint16_t value = static_cast<uint16_t>(
42
+ getEnvInt("SAFESERIAL_RETRY_TIMEOUT_MS", 2000)
43
+ );
44
+ return value;
45
+ }
46
+
47
+ // Maximum payload size per fragment (for large message fragmentation)
48
+ inline uint16_t fragmentSize() {
49
+ static uint16_t value = static_cast<uint16_t>(
50
+ getEnvInt("SAFESERIAL_FRAGMENT_SIZE", 256)
51
+ );
52
+ return value;
53
+ }
54
+
55
+ // Timeout waiting for ACK before retry
56
+ inline uint16_t ackTimeoutMs() {
57
+ static uint16_t value = static_cast<uint16_t>(
58
+ getEnvInt("SAFESERIAL_ACK_TIMEOUT_MS", 500)
59
+ );
60
+ return value;
61
+ }
62
+
63
+ // Baud rate (default for most embedded devices)
64
+ inline int baudRate() {
65
+ static int value = getEnvInt("SAFESERIAL_BAUD_RATE", 115200);
66
+ return value;
67
+ }
68
+
69
+ } // namespace SafeSerialConfig
@@ -0,0 +1,19 @@
1
+ #pragma once
2
+ #include <cstdint>
3
+ #include <cstddef>
4
+
5
+ class CRC32 {
6
+ public:
7
+ static uint32_t calculate(const uint8_t* data, size_t length) {
8
+ uint32_t crc = 0xFFFFFFFF;
9
+ for (size_t i = 0; i < length; ++i) {
10
+ uint8_t byte = data[i];
11
+ crc = crc ^ byte;
12
+ for (int j = 0; j < 8; ++j) {
13
+ uint32_t mask = -(crc & 1);
14
+ crc = (crc >> 1) ^ (0xEDB88320 & mask);
15
+ }
16
+ }
17
+ return ~crc;
18
+ }
19
+ };
@@ -0,0 +1,175 @@
1
+ #pragma once
2
+ #include <vector>
3
+ #include <string>
4
+ #include <cstdint>
5
+ #include <utility>
6
+ #include <cstring>
7
+ #include <algorithm>
8
+ #include <safeserial/protocol/crc32.hpp>
9
+ #include <safeserial/config.hpp>
10
+
11
+ // Cross-platform packed struct support
12
+ #if defined(_MSC_VER)
13
+ #define PACKED_STRUCT_BEGIN __pragma(pack(push, 1))
14
+ #define PACKED_STRUCT_END __pragma(pack(pop))
15
+ #define PACKED_ATTR
16
+ #elif defined(__GNUC__) || defined(__clang__)
17
+ #define PACKED_STRUCT_BEGIN
18
+ #define PACKED_STRUCT_END
19
+ #define PACKED_ATTR __attribute__((packed))
20
+ #else
21
+ #define PACKED_STRUCT_BEGIN
22
+ #define PACKED_STRUCT_END
23
+ #define PACKED_ATTR
24
+ #endif
25
+
26
+ struct Packet {
27
+ PACKED_STRUCT_BEGIN
28
+ struct Header {
29
+ uint8_t type;
30
+ uint8_t seq_id;
31
+ uint16_t fragment_id;
32
+ uint16_t total_frags;
33
+ uint16_t payload_len;
34
+ uint32_t crc32;
35
+ } PACKED_ATTR;
36
+ PACKED_STRUCT_END
37
+
38
+ // Packet Types
39
+ static constexpr uint8_t TYPE_DATA = 0x10;
40
+ static constexpr uint8_t TYPE_ACK = 0x20;
41
+ static constexpr uint8_t TYPE_NACK = 0x30;
42
+ static constexpr uint8_t TYPE_SYN = 0x40;
43
+
44
+ // COBS delimiter
45
+ static constexpr uint8_t COBS_DELIMITER = 0x00;
46
+
47
+ // Configuration accessors (use env vars or defaults)
48
+ static uint8_t maxRetries() { return SafeSerialConfig::maxRetries(); }
49
+ static uint16_t retryTimeoutMs() { return SafeSerialConfig::retryTimeoutMs(); }
50
+ static uint16_t fragmentSize() { return SafeSerialConfig::fragmentSize(); }
51
+
52
+ struct Frame {
53
+ Header header;
54
+ std::vector<uint8_t> payload;
55
+ bool valid;
56
+ };
57
+
58
+ // Consistent Overhead Byte Stuffing (COBS) Encoding
59
+ static std::vector<uint8_t> cobs_encode(const std::vector<uint8_t>& data) {
60
+ std::vector<uint8_t> encoded;
61
+ encoded.reserve(data.size() + data.size() / 254 + 2);
62
+
63
+ size_t code_idx = 0;
64
+ encoded.push_back(0); // Placeholder for the first code
65
+ uint8_t code = 1;
66
+
67
+ for (uint8_t byte : data) {
68
+ if (byte == 0) {
69
+ encoded[code_idx] = code;
70
+ code_idx = encoded.size();
71
+ encoded.push_back(0); // Placeholder for next code
72
+ code = 1;
73
+ } else {
74
+ encoded.push_back(byte);
75
+ code++;
76
+ if (code == 0xFF) { // Max run length reached
77
+ encoded[code_idx] = code;
78
+ code_idx = encoded.size();
79
+ encoded.push_back(0);
80
+ code = 1;
81
+ }
82
+ }
83
+ }
84
+ encoded[code_idx] = code;
85
+ return encoded;
86
+ }
87
+
88
+ // COBS Decoding
89
+ static std::vector<uint8_t> cobs_decode(const std::vector<uint8_t>& data) {
90
+ std::vector<uint8_t> decoded;
91
+ decoded.reserve(data.size());
92
+
93
+ size_t i = 0;
94
+ while (i < data.size()) {
95
+ uint8_t code = data[i];
96
+ i++;
97
+ if (code == 0) break; // Should not happen in valid COBS before delimiter
98
+
99
+ for (uint8_t j = 1; j < code; j++) {
100
+ if (i >= data.size()) return {}; // Error: truncated
101
+ decoded.push_back(data[i++]);
102
+ }
103
+ if (code < 0xFF && i < data.size()) {
104
+ decoded.push_back(0);
105
+ }
106
+ }
107
+ return decoded;
108
+ }
109
+
110
+ static std::vector<uint8_t> serialize(uint8_t type, uint8_t seq, const std::string& payload, uint16_t frag_id = 0, uint16_t total_frags = 1) {
111
+ Header header;
112
+ header.type = type;
113
+ header.seq_id = seq;
114
+ header.fragment_id = frag_id;
115
+ header.total_frags = total_frags;
116
+ header.payload_len = static_cast<uint16_t>(payload.size());
117
+ header.crc32 = 0; // Calculated later
118
+
119
+ std::vector<uint8_t> raw_packet;
120
+ raw_packet.resize(sizeof(Header) + payload.size());
121
+
122
+ std::memcpy(raw_packet.data(), &header, sizeof(Header));
123
+ std::memcpy(raw_packet.data() + sizeof(Header), payload.data(), payload.size());
124
+
125
+ // Calculate CRC32 of Header + Payload (with CRC field zeroed)
126
+ uint32_t crc = CRC32::calculate(raw_packet.data(), raw_packet.size());
127
+
128
+ // Update header with calculated CRC
129
+ header.crc32 = crc;
130
+ std::memcpy(raw_packet.data(), &header, sizeof(Header));
131
+
132
+ // Encode with COBS
133
+ std::vector<uint8_t> encoded = cobs_encode(raw_packet);
134
+ encoded.push_back(COBS_DELIMITER);
135
+ return encoded;
136
+ }
137
+
138
+ static Frame deserialize(std::vector<uint8_t>& buffer) {
139
+ // Find delimiter
140
+ auto it = std::find(buffer.begin(), buffer.end(), COBS_DELIMITER);
141
+ if (it == buffer.end()) return {{}, {}, false}; // No complete frame yet
142
+
143
+ std::vector<uint8_t> frame_data(buffer.begin(), it);
144
+ buffer.erase(buffer.begin(), it + 1); // Remove processed frame + delimiter
145
+
146
+ if (frame_data.empty()) return {{}, {}, false};
147
+
148
+ std::vector<uint8_t> decoded = cobs_decode(frame_data);
149
+ if (decoded.size() < sizeof(Header)) return {{}, {}, false};
150
+
151
+ Header header;
152
+ std::memcpy(&header, decoded.data(), sizeof(Header));
153
+
154
+ // Validate Length
155
+ if (decoded.size() != sizeof(Header) + header.payload_len) return {{}, {}, false};
156
+
157
+ // Validate CRC
158
+ uint32_t received_crc = header.crc32;
159
+
160
+ // Zero out CRC in buffer to recompute
161
+ Header* header_ptr = reinterpret_cast<Header*>(decoded.data());
162
+ header_ptr->crc32 = 0;
163
+
164
+ uint32_t computed_crc = CRC32::calculate(decoded.data(), decoded.size());
165
+
166
+ if (received_crc == computed_crc) {
167
+ std::vector<uint8_t> payload(decoded.begin() + sizeof(Header), decoded.end());
168
+ // Retrieve original header
169
+ header.crc32 = received_crc;
170
+ return {header, payload, true};
171
+ }
172
+
173
+ return {{}, {}, false};
174
+ }
175
+ };
@@ -0,0 +1,80 @@
1
+ #pragma once
2
+ #include <safeserial/protocol/packet.hpp>
3
+ #include <vector>
4
+ #include <iostream>
5
+
6
+ class Reassembler {
7
+ public:
8
+ struct Result {
9
+ bool complete;
10
+ std::vector<uint8_t> payload;
11
+ };
12
+
13
+ Reassembler() : current_seq_id_(255), expected_frag_(0), active_(false) {}
14
+
15
+ // Returns true if fragment was accepted (in sequence).
16
+ // Caller should send ACK if this returns true.
17
+ // If returns false, caller might ignore or send NACK/ACK-of-last-good.
18
+ bool process_fragment(const Packet::Frame& frame) {
19
+ if (!frame.valid) return false;
20
+
21
+ uint8_t seq = frame.header.seq_id;
22
+ uint16_t frag = frame.header.fragment_id;
23
+
24
+ // New Sequence ?
25
+ if (seq != current_seq_id_) {
26
+ // Accept if it's a new message (logic can be more complex for strict strictness,
27
+ // e.g. only seq+1, but for now accept any new seq as new message start)
28
+ if (frag == 0) {
29
+ reset(seq);
30
+ } else {
31
+ return false; // Received middle of new message without start?
32
+ }
33
+ }
34
+
35
+ if (frag != expected_frag_) {
36
+ // Out of order fragment
37
+ return false;
38
+ }
39
+
40
+ // Valid fragment
41
+ buffer_.insert(buffer_.end(), frame.payload.begin(), frame.payload.end());
42
+ expected_frag_++;
43
+ return true;
44
+ }
45
+
46
+ bool is_complete(const Packet::Frame& last_frame) const {
47
+ if (!active_) return false;
48
+ return expected_frag_ == last_frame.header.total_frags;
49
+ }
50
+
51
+ bool is_duplicate(const Packet::Frame& frame) const {
52
+ if (!active_) return false;
53
+ uint8_t seq = frame.header.seq_id;
54
+ uint16_t frag = frame.header.fragment_id;
55
+ return (seq == current_seq_id_ && frag < expected_frag_);
56
+ }
57
+
58
+ std::vector<uint8_t> get_data() const {
59
+ return buffer_;
60
+ }
61
+
62
+ size_t get_buffered_size() const {
63
+ return buffer_.size();
64
+ }
65
+
66
+ uint8_t get_current_seq() const { return current_seq_id_; }
67
+
68
+ private:
69
+ void reset(uint8_t seq) {
70
+ buffer_.clear();
71
+ current_seq_id_ = seq;
72
+ expected_frag_ = 0;
73
+ active_ = true;
74
+ }
75
+
76
+ uint8_t current_seq_id_;
77
+ uint16_t expected_frag_;
78
+ std::vector<uint8_t> buffer_;
79
+ bool active_;
80
+ };
@@ -0,0 +1,106 @@
1
+ #pragma once
2
+
3
+ #include <atomic>
4
+ #include <condition_variable>
5
+ #include <cstddef>
6
+ #include <cstdint>
7
+ #include <functional>
8
+ #include <memory>
9
+ #include <mutex>
10
+ #include <random>
11
+ #include <string>
12
+ #include <thread>
13
+ #include <vector>
14
+
15
+ #include <safeserial/safeserial.hpp>
16
+
17
+ class ResilientDataBridge {
18
+ public:
19
+ struct Options {
20
+ DataBridge::Options bridge;
21
+ bool reconnect;
22
+ uint32_t reconnect_delay_ms;
23
+ uint32_t max_reconnect_delay_ms;
24
+ size_t max_queue_size;
25
+
26
+ static Options Defaults() {
27
+ Options options{};
28
+ options.bridge = DataBridge::Options::Defaults();
29
+ options.reconnect = true;
30
+ options.reconnect_delay_ms = 1000;
31
+ options.max_reconnect_delay_ms = 30000;
32
+ options.max_queue_size = 1000;
33
+ return options;
34
+ }
35
+ };
36
+
37
+ ResilientDataBridge();
38
+ explicit ResilientDataBridge(const Options& options);
39
+ ResilientDataBridge(const Options& options,
40
+ std::function<std::shared_ptr<ISerialPort>()> serial_factory);
41
+ ~ResilientDataBridge();
42
+
43
+ ResilientDataBridge(const ResilientDataBridge&) = delete;
44
+ ResilientDataBridge& operator=(const ResilientDataBridge&) = delete;
45
+
46
+ bool open(const std::string& port);
47
+ void close();
48
+
49
+ int send(const std::vector<uint8_t>& data);
50
+
51
+ bool is_connected() const;
52
+ size_t queue_length() const;
53
+
54
+ void set_on_data(std::function<void(const std::vector<uint8_t>&)> callback);
55
+ void set_on_error(std::function<void(const std::string&)> callback);
56
+ void set_on_disconnect(std::function<void()> callback);
57
+ void set_on_reconnecting(std::function<void(uint32_t, uint32_t)> callback);
58
+ void set_on_reconnected(std::function<void()> callback);
59
+ void set_on_close(std::function<void()> callback);
60
+
61
+ private:
62
+ struct QueuedMessage {
63
+ explicit QueuedMessage(std::vector<uint8_t> payload)
64
+ : data(std::move(payload)) {}
65
+
66
+ std::vector<uint8_t> data;
67
+ int bytes_written = 0;
68
+ bool done = false;
69
+ bool failed = false;
70
+ std::string error;
71
+ std::mutex mutex;
72
+ std::condition_variable cv;
73
+ };
74
+
75
+ bool connect();
76
+ void handle_disconnect(const std::string& reason);
77
+ void start_reconnect_thread();
78
+ void reconnect_loop();
79
+ void flush_queue();
80
+ void notify_error(const std::string& message);
81
+
82
+ std::string port_;
83
+ Options options_;
84
+ std::function<std::shared_ptr<ISerialPort>()> serial_factory_;
85
+
86
+ std::unique_ptr<DataBridge> bridge_;
87
+ std::atomic<bool> connected_{false};
88
+ std::atomic<bool> stop_{false};
89
+
90
+ std::mutex state_mutex_;
91
+ mutable std::mutex queue_mutex_;
92
+ std::vector<std::shared_ptr<QueuedMessage>> queue_;
93
+
94
+ std::thread reconnect_thread_;
95
+ std::atomic<bool> reconnect_thread_running_{false};
96
+ uint32_t reconnect_attempt_ = 0;
97
+ std::mt19937 rng_{std::random_device{}()};
98
+
99
+ std::mutex callback_mutex_;
100
+ std::function<void(const std::vector<uint8_t>&)> on_data_;
101
+ std::function<void(const std::string&)> on_error_;
102
+ std::function<void()> on_disconnect_;
103
+ std::function<void(uint32_t, uint32_t)> on_reconnecting_;
104
+ std::function<void()> on_reconnected_;
105
+ std::function<void()> on_close_;
106
+ };
@@ -0,0 +1,87 @@
1
+ #pragma once
2
+
3
+ #include <atomic>
4
+ #include <condition_variable>
5
+ #include <cstdint>
6
+ #include <functional>
7
+ #include <memory>
8
+ #include <mutex>
9
+ #include <string>
10
+ #include <thread>
11
+ #include <vector>
12
+
13
+ #include <safeserial/config.hpp>
14
+ #include <safeserial/protocol/packet.hpp>
15
+ #include <safeserial/protocol/reassembler.hpp>
16
+ #include <safeserial/transport/iserial_port.hpp>
17
+ #include <safeserial/transport/serial_port.hpp>
18
+
19
+ class DataBridge {
20
+ public:
21
+ struct Options {
22
+ int baud_rate;
23
+ uint8_t max_retries;
24
+ uint16_t ack_timeout_ms;
25
+ uint16_t fragment_size;
26
+
27
+ static Options Defaults() {
28
+ return Options{
29
+ SafeSerialConfig::baudRate(),
30
+ SafeSerialConfig::maxRetries(),
31
+ SafeSerialConfig::ackTimeoutMs(),
32
+ SafeSerialConfig::fragmentSize(),
33
+ };
34
+ }
35
+ };
36
+
37
+ DataBridge();
38
+ explicit DataBridge(const Options& options);
39
+ DataBridge(std::shared_ptr<ISerialPort> serial, const Options& options);
40
+ ~DataBridge();
41
+
42
+ DataBridge(const DataBridge&) = delete;
43
+ DataBridge& operator=(const DataBridge&) = delete;
44
+
45
+ bool open(const std::string& port, int baud_rate_override = -1);
46
+ void close();
47
+ bool is_open() const;
48
+
49
+ int send(const std::vector<uint8_t>& data,
50
+ uint16_t ack_timeout_ms_override = 0,
51
+ uint8_t max_retries_override = 0,
52
+ uint16_t fragment_size_override = 0);
53
+
54
+ void set_on_data(std::function<void(const std::vector<uint8_t>&)> callback);
55
+ size_t get_buffered_size() const;
56
+
57
+ private:
58
+ void receive_loop();
59
+ void handle_frame(const Packet::Frame& frame);
60
+ void send_ack(const Packet::Frame& frame);
61
+
62
+ std::shared_ptr<ISerialPort> serial_;
63
+ Options options_;
64
+
65
+ std::atomic<bool> stop_{false};
66
+ std::atomic<bool> is_open_{false};
67
+ std::thread receive_thread_;
68
+
69
+ std::vector<uint8_t> rx_buffer_;
70
+ Reassembler reassembler_;
71
+
72
+ std::mutex seq_mutex_;
73
+ uint8_t next_seq_{0};
74
+
75
+ std::mutex send_mutex_;
76
+ std::mutex write_mutex_;
77
+
78
+ std::mutex ack_mutex_;
79
+ std::condition_variable ack_cv_;
80
+ bool waiting_for_ack_{false};
81
+ bool acked_{false};
82
+ uint8_t waiting_seq_{0};
83
+ uint16_t waiting_frag_{0};
84
+
85
+ std::mutex callback_mutex_;
86
+ std::function<void(const std::vector<uint8_t>&)> on_data_;
87
+ };
File without changes
@@ -0,0 +1,15 @@
1
+ #pragma once
2
+ #include <vector>
3
+ #include <string>
4
+ #include <cstdint>
5
+
6
+ class ISerialPort {
7
+ public:
8
+ virtual ~ISerialPort() = default;
9
+
10
+ virtual bool open(const std::string& port_name, int baud_rate) = 0;
11
+ virtual void close() = 0;
12
+
13
+ virtual int write(const std::vector<uint8_t>& data) = 0;
14
+ virtual int read(uint8_t* buffer, size_t size) = 0;
15
+ };
@@ -0,0 +1,28 @@
1
+ #pragma once
2
+ #include <memory>
3
+ #include <vector>
4
+ #include <string>
5
+
6
+ #include <safeserial/transport/iserial_port.hpp>
7
+
8
+ class SerialPort : public ISerialPort {
9
+
10
+ public:
11
+ SerialPort();
12
+ ~SerialPort();
13
+
14
+ // Prevent copying (Serial ports are unique resources)
15
+ SerialPort(const SerialPort&) = delete;
16
+ SerialPort& operator=(const SerialPort&) = delete;
17
+
18
+ bool open(const std::string& port_name, int baud_rate) override;
19
+ void close() override;
20
+
21
+ int write(const std::vector<uint8_t>& data) override;
22
+ int read(uint8_t* buffer, size_t size) override;
23
+
24
+ private:
25
+ // The "Pimpl" - This struct is defined only in the .cpp files
26
+ struct Impl;
27
+ std::unique_ptr<Impl> pimpl;
28
+ };
@@ -0,0 +1,21 @@
1
+ add_library(safeserial STATIC
2
+ safeserial.cpp
3
+ resilient_bridge.cpp
4
+ )
5
+
6
+ safeserial_apply_sanitizers(safeserial)
7
+
8
+ # Platform specific sources
9
+ if(WIN32)
10
+ target_sources(safeserial PRIVATE transport/platform/windows/windows_serial.cpp)
11
+ else()
12
+ target_sources(safeserial PRIVATE transport/platform/linux/linux_serial.cpp)
13
+ endif()
14
+
15
+ target_include_directories(safeserial PUBLIC
16
+ $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
17
+ $<INSTALL_INTERFACE:include>
18
+ )
19
+
20
+ # Export the library name for other subdirectories
21
+ set(SAFESERIAL_LIB safeserial PARENT_SCOPE)
@@ -0,0 +1 @@
1
+ #include "protocol/packet.hpp"