From 8488a6dab99c60b82f9a34e8763797dfc7359c7a Mon Sep 17 00:00:00 2001 From: HRS Date: Wed, 11 Feb 2026 07:00:38 +0100 Subject: [PATCH 1/5] Add implementation for missing list attribute --- examples/basic/src/main.cpp | 23 +++++- src/SlvCtrlArduinoSerialCommandsTransport.h | 4 +- src/SlvCtrlProtocol.h | 88 +++++++++++++++++++++ 3 files changed, 109 insertions(+), 6 deletions(-) diff --git a/examples/basic/src/main.cpp b/examples/basic/src/main.cpp index 7b7c81b..b6ce69d 100644 --- a/examples/basic/src/main.cpp +++ b/examples/basic/src/main.cpp @@ -4,10 +4,12 @@ #include // --------- your state ---------- -static int32_t speed = 0; -static int32_t maxSpeed = 100; -static int32_t state = 0; -static bool ready = false; +static int32_t speed = 0; +static int32_t maxSpeed = 100; +static int32_t state = 0; +static bool ready = false; +static int32_t baud = 9600; +static const char* mode = "off"; // --------- getters/setters (C function pointers) ---------- // Note: your attribute classes use function pointers: T(*)(void*), not lambdas. @@ -22,17 +24,30 @@ static SlvCtrlParseError setState(void*, int32_t v) { state = v; return SlvCtrlP static bool getReady(void*) { return ready; } +static int32_t getBaud(void*) { return baud; } + +static const char* getMode(void*) { return mode; } +static SlvCtrlParseError setMode(void*, const char* v) { mode = v; return SlvCtrlParseError::Ok; } + // --------- attributes ---------- static IntAttribute speedAttr("speed", &getSpeed, &setSpeed); static RangeAttribute maxAttr("max", &getMaxSpeed, &setMaxSpeed, 0, 100); static IntAttribute stateAttr("state", &getState, &setState); static BoolAttribute readyAttr("ready", &getReady, nullptr); +static const int32_t kBaudOpts[] = { 9600, 19200, 115200 }; +ListAttribute baudAttr("baud", getBaud, nullptr, kBaudOpts); + +static const char* kModeOpts[] = { "off", "on", "auto" }; +ListAttribute modeAttr("mode", getMode, setMode, kModeOpts); + static IAttribute* attrs[] = { &speedAttr, &maxAttr, &stateAttr, &readyAttr, + &baudAttr, + &modeAttr, }; // --------- protocol + transport ---------- diff --git a/src/SlvCtrlArduinoSerialCommandsTransport.h b/src/SlvCtrlArduinoSerialCommandsTransport.h index e88b5c5..a4fbff2 100644 --- a/src/SlvCtrlArduinoSerialCommandsTransport.h +++ b/src/SlvCtrlArduinoSerialCommandsTransport.h @@ -10,11 +10,11 @@ class SlvCtrlArduinoOut final : public ISlvCtrlOut { public: explicit SlvCtrlArduinoOut(Stream& s) : s_(s) {} - void send(const char* s) override { + void write(const char* s) override { s_.print(s ? s : ""); } - void send(char v) override { + void write(char v) override { s_.print(v); } diff --git a/src/SlvCtrlProtocol.h b/src/SlvCtrlProtocol.h index b68db36..ba7aa10 100644 --- a/src/SlvCtrlProtocol.h +++ b/src/SlvCtrlProtocol.h @@ -238,6 +238,94 @@ class RangeAttribute : public BaseAttribute { T max_; }; +template +class ListAttribute : public BaseAttribute { + using Base = BaseAttribute; + using Getter = typename Base::Getter; + using Setter = typename Base::Setter; + + static_assert( + std::is_same_v || std::is_same_v, + "ListAttribute: T must be int32_t or const char*" + ); + +public: + template + ListAttribute(const char* name, + Getter getter, + Setter setter, + const T (&options)[N], + void* ctx = nullptr) + : Base(name, getter, setter, ctx), + options_(options), + count_(N) { + static_assert(N > 0, "ListAttribute: options array must not be empty"); + } + + SlvCtrlParseError setFromCString(const char* s) override { + if (!s) return SlvCtrlParseError::MissingValue; + + if constexpr (std::is_same_v) { + char* end = nullptr; + long v = strtol(s, &end, 10); + if (end == s || *end != '\0') return SlvCtrlParseError::NotANumber; + if (v < (long)INT32_MIN || v > (long)INT32_MAX) return SlvCtrlParseError::OutOfRange; + + int32_t iv = (int32_t)v; + if (!containsInt_(iv)) return SlvCtrlParseError::InvalidValue; + return this->setValue(iv); + } + + if constexpr (std::is_same_v) { + // Store canonical pointer from options_ (safer than storing token buffer) + const char* canonical = findStr_(s); + if (!canonical) return SlvCtrlParseError::InvalidValue; + return this->setValue(canonical); + } + + __builtin_trap(); + } + + void describe(ISlvCtrlOut& out) const override { + out.send(slvCtrlAccessToString(this->access())); + out.send("["); + + if constexpr (std::is_same_v) out.send("int("); + else out.send("str("); + + for (size_t i = 0; i < count_; ++i) { + if (i) out.send("|"); + sendOption_(out, options_[i]); + } + + out.send(")]"); + } + +private: + bool containsInt_(int32_t v) const { + for (size_t i = 0; i < count_; ++i) { + if (options_[i] == v) return true; + } + return false; + } + + const char* findStr_(const char* s) const { + for (size_t i = 0; i < count_; ++i) { + const char* opt = options_[i]; + if (!opt) continue; + if (strcmp(opt, s) == 0) return opt; + } + return nullptr; + } + + static void sendOption_(ISlvCtrlOut& o, int32_t v) { o.send(v); } + static void sendOption_(ISlvCtrlOut& o, const char* s) { o.send(s ? s : ""); } + +private: + const T* options_; + size_t count_; +}; + class StrAttribute : public BaseAttribute { public: StrAttribute(const char* name, Getter getter, Setter setter, void* ctx = nullptr) From 1801db95945aace7bcb526ef29f99e5dd461d703 Mon Sep 17 00:00:00 2001 From: HRS Date: Wed, 11 Feb 2026 07:02:10 +0100 Subject: [PATCH 2/5] Rename send() to write() --- src/SlvCtrlArduinoSerialCommandsTransport.h | 8 +- src/SlvCtrlProtocol.h | 166 ++++++++++---------- 2 files changed, 87 insertions(+), 87 deletions(-) diff --git a/src/SlvCtrlArduinoSerialCommandsTransport.h b/src/SlvCtrlArduinoSerialCommandsTransport.h index a4fbff2..ca8e914 100644 --- a/src/SlvCtrlArduinoSerialCommandsTransport.h +++ b/src/SlvCtrlArduinoSerialCommandsTransport.h @@ -18,21 +18,21 @@ class SlvCtrlArduinoOut final : public ISlvCtrlOut { s_.print(v); } - void send(int32_t v) override { + void write(int32_t v) override { // Arduino Print uses long for signed integers s_.print((long)v); } - void send(uint32_t v) override { + void write(uint32_t v) override { // Arduino Print uses unsigned long for unsigned integers s_.print((unsigned long)v); } - void send(float v, uint8_t decimals = 3) override { + void write(float v, uint8_t decimals = 3) override { s_.print(v, decimals); } - void send(bool v) override { + void write(bool v) override { s_.print(v); } diff --git a/src/SlvCtrlProtocol.h b/src/SlvCtrlProtocol.h index ba7aa10..3c6155d 100644 --- a/src/SlvCtrlProtocol.h +++ b/src/SlvCtrlProtocol.h @@ -51,12 +51,12 @@ struct ISlvCtrlTransport { // ---- Output abstraction (no Stream) ---- struct ISlvCtrlOut { virtual ~ISlvCtrlOut() = default; - virtual void send(const char* s) = 0; - virtual void send(char c) = 0; - virtual void send(uint32_t v) = 0; - virtual void send(int32_t v) = 0; - virtual void send(float v, uint8_t decimals = 3) = 0; - virtual void send(bool v) = 0; + virtual void write(const char* s) = 0; + virtual void write(char c) = 0; + virtual void write(uint32_t v) = 0; + virtual void write(int32_t v) = 0; + virtual void write(float v, uint8_t decimals = 3) = 0; + virtual void write(bool v) = 0; }; // ---- Command context abstraction (no SerialCommands) ---- @@ -107,7 +107,7 @@ class BaseAttribute : public IAttribute { } void writeValue(ISlvCtrlOut& out) const override { - out.send(this->getValue()); + out.write(this->getValue()); } private: @@ -132,8 +132,8 @@ class IntAttribute : public BaseAttribute { } void describe(ISlvCtrlOut& out) const override { - out.send(slvCtrlAccessToString(access())); - out.send("[int]"); + out.write(slvCtrlAccessToString(access())); + out.write("[int]"); } }; @@ -151,8 +151,8 @@ class FloatAttribute : public BaseAttribute { } void describe(ISlvCtrlOut& out) const override { - out.send(slvCtrlAccessToString(access())); - out.send("[float]"); + out.write(slvCtrlAccessToString(access())); + out.write("[float]"); } }; @@ -169,12 +169,12 @@ class BoolAttribute : public BaseAttribute { } void describe(ISlvCtrlOut& out) const override { - out.send(slvCtrlAccessToString(access())); - out.send("[bool]"); + out.write(slvCtrlAccessToString(access())); + out.write("[bool]"); } void writeValue(ISlvCtrlOut& out) const override { - out.send(this->getValue() ? "true" : "false"); + out.write(this->getValue() ? "true" : "false"); } }; @@ -216,21 +216,21 @@ class RangeAttribute : public BaseAttribute { } void describe(ISlvCtrlOut& out) const override { - out.send(slvCtrlAccessToString(this->access())); - out.send("["); + out.write(slvCtrlAccessToString(this->access())); + out.write("["); if constexpr (std::is_same_v) { - out.send("int("); + out.write("int("); } else if constexpr (std::is_same_v) { - out.send("float("); + out.write("float("); } else { - out.send("?("); + out.write("?("); } - out.send(min_); - out.send(".."); - out.send(max_); - out.send(")]"); + out.write(min_); + out.write(".."); + out.write(max_); + out.write(")]"); } private: @@ -287,18 +287,18 @@ class ListAttribute : public BaseAttribute { } void describe(ISlvCtrlOut& out) const override { - out.send(slvCtrlAccessToString(this->access())); - out.send("["); + out.write(slvCtrlAccessToString(this->access())); + out.write("["); - if constexpr (std::is_same_v) out.send("int("); - else out.send("str("); + if constexpr (std::is_same_v) out.write("int("); + else out.write("str("); for (size_t i = 0; i < count_; ++i) { - if (i) out.send("|"); - sendOption_(out, options_[i]); + if (i) out.write("|"); + writeOption_(out, options_[i]); } - out.send(")]"); + out.write(")]"); } private: @@ -318,8 +318,8 @@ class ListAttribute : public BaseAttribute { return nullptr; } - static void sendOption_(ISlvCtrlOut& o, int32_t v) { o.send(v); } - static void sendOption_(ISlvCtrlOut& o, const char* s) { o.send(s ? s : ""); } + static void writeOption_(ISlvCtrlOut& o, int32_t v) { o.write(v); } + static void writeOption_(ISlvCtrlOut& o, const char* s) { o.write(s ? s : ""); } private: const T* options_; @@ -337,13 +337,13 @@ class StrAttribute : public BaseAttribute { } void describe(ISlvCtrlOut& out) const override { - out.send(slvCtrlAccessToString(access())); - out.send("[str]"); + out.write(slvCtrlAccessToString(access())); + out.write("[str]"); } void writeValue(ISlvCtrlOut& out) const override { const char* s = this->getValue(); - out.send(s ? s : ""); + out.write(s ? s : ""); } }; @@ -370,71 +370,71 @@ class SlvCtrlProtocol { void cmdIntroduce(ISlvCtrlCmdCtx& ctx) { auto& o = ctx.out(); - o.send("introduce;type:"); - o.send(deviceType_); - o.send(",fw:"); - o.send(fwVersion_); - o.send(",protocol:"); - o.send(protocolVersion); - o.send(";status:ok"); - o.send(ETX); + o.write("introduce;type:"); + o.write(deviceType_); + o.write(",fw:"); + o.write(fwVersion_); + o.write(",protocol:"); + o.write(protocolVersion); + o.write(";status:ok"); + o.write(ETX); } void cmdAttributes(ISlvCtrlCmdCtx& ctx) { auto& o = ctx.out(); - o.send("attributes;"); + o.write("attributes;"); bool first = true; for (size_t i = 0; i < attrCount_; ++i) { IAttribute* a = attrs_[i]; if (!a) continue; - if (!first) o.send(","); + if (!first) o.write(","); first = false; - o.send(a->name()); - o.send(":"); + o.write(a->name()); + o.write(":"); a->describe(o); } - o.send(";status:ok"); - o.send(ETX); + o.write(";status:ok"); + o.write(ETX); } void cmdStatus(ISlvCtrlCmdCtx& ctx) { auto& o = ctx.out(); - o.send("status;"); + o.write("status;"); bool first = true; for (size_t i = 0; i < attrCount_; ++i) { IAttribute* a = attrs_[i]; if (!a || !canRead(a->access())) continue; - if (!first) o.send(","); + if (!first) o.write(","); first = false; - o.send(a->name()); - o.send(":"); + o.write(a->name()); + o.write(":"); a->writeValue(o); } - o.send(";status:ok"); - o.send(ETX); + o.write(";status:ok"); + o.write(ETX); } void cmdGet(ISlvCtrlCmdCtx& ctx) { auto& o = ctx.out(); const char* name = ctx.next(); - if (!name) { o.send("get;;status:error,reason:missing_attribute_name_arg"); o.send(ETX); return; } + if (!name) { o.write("get;;status:error,reason:missing_attribute_name_arg"); o.write(ETX); return; } IAttribute* a = findAttr(name); - if (!a) { o.send("get "); o.send(name); o.send(";;status:error,reason:unknown_attribute"); o.send(ETX); return; } - if (!canRead(a->access())) { o.send("get "); o.send(name); o.send(";;status:error,reason:write_only_attribute"); o.send(ETX); return; } + if (!a) { o.write("get "); o.write(name); o.write(";;status:error,reason:unknown_attribute"); o.write(ETX); return; } + if (!canRead(a->access())) { o.write("get "); o.write(name); o.write(";;status:error,reason:write_only_attribute"); o.write(ETX); return; } - o.send("get "); - o.send(a->name()); - o.send(";value:"); + o.write("get "); + o.write(a->name()); + o.write(";value:"); a->writeValue(o); - o.send(";status:ok"); - o.send(ETX); + o.write(";status:ok"); + o.write(ETX); } void cmdSet(ISlvCtrlCmdCtx& ctx) { @@ -443,46 +443,46 @@ class SlvCtrlProtocol { const char* value = ctx.next(); if (!name) { - o.send("set;;status:error,reason:attribute_name_missing"); - o.send(ETX); + o.write("set;;status:error,reason:attribute_name_missing"); + o.write(ETX); return; } if (!value) { - o.send("set "); - o.send(name); - o.send(";;status:error,reason:attribute_value_missing"); - o.send(ETX); + o.write("set "); + o.write(name); + o.write(";;status:error,reason:attribute_value_missing"); + o.write(ETX); return; } IAttribute* a = findAttr(name); - if (!a) { o.send("set "); o.send(name); o.send(";;status:error,reason:unknown_attribute"); o.send(ETX); return; } - if (!canWrite(a->access())) { o.send("set "); o.send(name); o.send(";;status:error,reason:read_only_attribute"); o.send(ETX); return; } + if (!a) { o.write("set "); o.write(name); o.write(";;status:error,reason:unknown_attribute"); o.write(ETX); return; } + if (!canWrite(a->access())) { o.write("set "); o.write(name); o.write(";;status:error,reason:read_only_attribute"); o.write(ETX); return; } SlvCtrlParseError err = a->setFromCString(value); - o.send("set "); - o.send(name); - o.send(" "); - o.send(value); - o.send(";;"); + o.write("set "); + o.write(name); + o.write(" "); + o.write(value); + o.write(";;"); if (err == SlvCtrlParseError::Ok) { - o.send("status:ok"); + o.write("status:ok"); } else { - o.send("status:error,reason:"); - o.send(slvCtrlParseErrorToString(err)); + o.write("status:error,reason:"); + o.write(slvCtrlParseErrorToString(err)); } - o.send(ETX); + o.write(ETX); } void cmdUnrecognized(ISlvCtrlCmdCtx& ctx, const char* cmd) { auto& o = ctx.out(); - o.send(cmd ? cmd : ""); - o.send(";;status:error,reason:unknown_command"); - o.send(ETX); + o.write(cmd ? cmd : ""); + o.write(";;status:error,reason:unknown_command"); + o.write(ETX); } private: From d23b82d0533222ee64f23f2afc9ff414b8f2f9f1 Mon Sep 17 00:00:00 2001 From: HRS Date: Wed, 11 Feb 2026 07:04:25 +0100 Subject: [PATCH 3/5] Rename count_ to optionsCount_ for clarity --- src/SlvCtrlProtocol.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SlvCtrlProtocol.h b/src/SlvCtrlProtocol.h index 3c6155d..47b86d2 100644 --- a/src/SlvCtrlProtocol.h +++ b/src/SlvCtrlProtocol.h @@ -258,7 +258,7 @@ class ListAttribute : public BaseAttribute { void* ctx = nullptr) : Base(name, getter, setter, ctx), options_(options), - count_(N) { + optionsCount_(N) { static_assert(N > 0, "ListAttribute: options array must not be empty"); } @@ -293,7 +293,7 @@ class ListAttribute : public BaseAttribute { if constexpr (std::is_same_v) out.write("int("); else out.write("str("); - for (size_t i = 0; i < count_; ++i) { + for (size_t i = 0; i < optionsCount_; ++i) { if (i) out.write("|"); writeOption_(out, options_[i]); } @@ -303,14 +303,14 @@ class ListAttribute : public BaseAttribute { private: bool containsInt_(int32_t v) const { - for (size_t i = 0; i < count_; ++i) { + for (size_t i = 0; i < optionsCount_; ++i) { if (options_[i] == v) return true; } return false; } const char* findStr_(const char* s) const { - for (size_t i = 0; i < count_; ++i) { + for (size_t i = 0; i < optionsCount_; ++i) { const char* opt = options_[i]; if (!opt) continue; if (strcmp(opt, s) == 0) return opt; @@ -323,7 +323,7 @@ class ListAttribute : public BaseAttribute { private: const T* options_; - size_t count_; + size_t optionsCount_; }; class StrAttribute : public BaseAttribute { From 24dfa7a2ee53ce2c5bcb16808b26818dbd5ac810 Mon Sep 17 00:00:00 2001 From: HRS Date: Wed, 11 Feb 2026 07:04:55 +0100 Subject: [PATCH 4/5] Bump version --- library.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.json b/library.json index d0cd38e..c445805 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "slvctrl-protocol", - "version": "0.1.2", + "version": "0.1.3", "description": "SlvCtrl protocol implementation for embedded devices", "keywords": ["serial", "protocol", "arduino"], "repository": { From 08b4e7079d9cec92a8e806a0d5aec589813fea4a Mon Sep 17 00:00:00 2001 From: HRS Date: Wed, 11 Feb 2026 21:21:21 +0100 Subject: [PATCH 5/5] Fix formatting --- src/SlvCtrlProtocol.h | 118 +++++++++++++++++++++--------------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/src/SlvCtrlProtocol.h b/src/SlvCtrlProtocol.h index 47b86d2..07a206a 100644 --- a/src/SlvCtrlProtocol.h +++ b/src/SlvCtrlProtocol.h @@ -249,81 +249,81 @@ class ListAttribute : public BaseAttribute { "ListAttribute: T must be int32_t or const char*" ); -public: - template - ListAttribute(const char* name, - Getter getter, - Setter setter, - const T (&options)[N], - void* ctx = nullptr) - : Base(name, getter, setter, ctx), - options_(options), - optionsCount_(N) { - static_assert(N > 0, "ListAttribute: options array must not be empty"); - } + public: + template + ListAttribute(const char* name, + Getter getter, + Setter setter, + const T (&options)[N], + void* ctx = nullptr) + : Base(name, getter, setter, ctx), + options_(options), + optionsCount_(N) { + static_assert(N > 0, "ListAttribute: options array must not be empty"); + } - SlvCtrlParseError setFromCString(const char* s) override { - if (!s) return SlvCtrlParseError::MissingValue; + SlvCtrlParseError setFromCString(const char* s) override { + if (!s) return SlvCtrlParseError::MissingValue; - if constexpr (std::is_same_v) { - char* end = nullptr; - long v = strtol(s, &end, 10); - if (end == s || *end != '\0') return SlvCtrlParseError::NotANumber; - if (v < (long)INT32_MIN || v > (long)INT32_MAX) return SlvCtrlParseError::OutOfRange; + if constexpr (std::is_same_v) { + char* end = nullptr; + long v = strtol(s, &end, 10); + if (end == s || *end != '\0') return SlvCtrlParseError::NotANumber; + if (v < (long)INT32_MIN || v > (long)INT32_MAX) return SlvCtrlParseError::OutOfRange; - int32_t iv = (int32_t)v; - if (!containsInt_(iv)) return SlvCtrlParseError::InvalidValue; - return this->setValue(iv); - } + int32_t iv = (int32_t)v; + if (!containsInt_(iv)) return SlvCtrlParseError::InvalidValue; + return this->setValue(iv); + } + + if constexpr (std::is_same_v) { + // Store canonical pointer from options_ (safer than storing token buffer) + const char* canonical = containsStr_(s); + if (!canonical) return SlvCtrlParseError::InvalidValue; + return this->setValue(canonical); + } - if constexpr (std::is_same_v) { - // Store canonical pointer from options_ (safer than storing token buffer) - const char* canonical = findStr_(s); - if (!canonical) return SlvCtrlParseError::InvalidValue; - return this->setValue(canonical); + __builtin_trap(); } - __builtin_trap(); - } + void describe(ISlvCtrlOut& out) const override { + out.write(slvCtrlAccessToString(this->access())); + out.write("["); - void describe(ISlvCtrlOut& out) const override { - out.write(slvCtrlAccessToString(this->access())); - out.write("["); + if constexpr (std::is_same_v) out.write("int("); + else out.write("str("); - if constexpr (std::is_same_v) out.write("int("); - else out.write("str("); + for (size_t i = 0; i < optionsCount_; ++i) { + if (i) out.write("|"); + writeOption_(out, options_[i]); + } - for (size_t i = 0; i < optionsCount_; ++i) { - if (i) out.write("|"); - writeOption_(out, options_[i]); + out.write(")]"); } - out.write(")]"); - } - -private: - bool containsInt_(int32_t v) const { - for (size_t i = 0; i < optionsCount_; ++i) { - if (options_[i] == v) return true; + private: + bool containsInt_(int32_t v) const { + for (size_t i = 0; i < optionsCount_; ++i) { + if (options_[i] == v) return true; + } + return false; } - return false; - } - const char* findStr_(const char* s) const { - for (size_t i = 0; i < optionsCount_; ++i) { - const char* opt = options_[i]; - if (!opt) continue; - if (strcmp(opt, s) == 0) return opt; + const char* containsStr_(const char* s) const { + for (size_t i = 0; i < optionsCount_; ++i) { + const char* opt = options_[i]; + if (!opt) continue; + if (strcmp(opt, s) == 0) return opt; + } + return nullptr; } - return nullptr; - } - static void writeOption_(ISlvCtrlOut& o, int32_t v) { o.write(v); } - static void writeOption_(ISlvCtrlOut& o, const char* s) { o.write(s ? s : ""); } + static void writeOption_(ISlvCtrlOut& o, int32_t v) { o.write(v); } + static void writeOption_(ISlvCtrlOut& o, const char* s) { o.write(s ? s : ""); } -private: - const T* options_; - size_t optionsCount_; + private: + const T* options_; + size_t optionsCount_; }; class StrAttribute : public BaseAttribute {