Skip to content

Commit ae6929d

Browse files
committed
Merge branch 'develop' into bugfix/serial-subcommands
2 parents 1114ef6 + 0292cb2 commit ae6929d

File tree

9 files changed

+91
-61
lines changed

9 files changed

+91
-61
lines changed

.github/scripts/pnpm-lock.yaml

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/CommandHandler.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const char* const TAG = "CommandHandler";
2525
const int64_t KEEP_ALIVE_INTERVAL = 60'000;
2626
const uint16_t KEEP_ALIVE_DURATION = 300;
2727

28-
uint32_t calculateEepyTime(int64_t timeToKeepAlive)
28+
static uint32_t calculateEepyTime(int64_t timeToKeepAlive)
2929
{
3030
int64_t now = OpenShock::millis();
3131
return static_cast<uint32_t>(std::clamp(timeToKeepAlive - now, 0LL, KEEP_ALIVE_INTERVAL));
@@ -316,7 +316,7 @@ bool CommandHandler::HandleCommand(ShockerModelType model, uint16_t shockerId, S
316316
ScopedReadLock lock__ka(&s_keepAliveMutex);
317317

318318
if (ok && s_keepAliveQueue != nullptr) {
319-
KnownShocker cmd {.model = model, .shockerId = shockerId, .lastActivityTimestamp = OpenShock::millis() + durationMs};
319+
KnownShocker cmd {.killTask = false, .model = model, .shockerId = shockerId, .lastActivityTimestamp = OpenShock::millis() + durationMs};
320320
if (xQueueSend(s_keepAliveQueue, &cmd, pdMS_TO_TICKS(10)) != pdTRUE) {
321321
OS_LOGE(TAG, "Failed to send keep-alive command to queue");
322322
}

src/Convert.cpp

Lines changed: 59 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,36 +13,35 @@ using namespace std::string_view_literals;
1313

1414
// Base converter
1515
template<typename T>
16-
void fromNonZeroT(T val, std::string& str)
16+
void fromNonZeroT(T val, std::string& out)
1717
{
18-
static_assert(std::is_integral_v<T>);
18+
// Ensure the template type T is an integral type
19+
static_assert(std::is_integral_v<T>, "T must be an integral type");
1920
constexpr std::size_t MaxDigits = OpenShock::Util::Digits10CountMax<T>;
2021

21-
char buf[MaxDigits + 1]; // +1 for null terminator
22+
char buf[MaxDigits];
2223

24+
// Start from the end of the buffer to construct the number in reverse (from least to most significant digit)
2325
char* ptr = buf + MaxDigits;
2426

25-
// Null terminator
26-
*ptr-- = '\0';
27-
28-
// Handle negative numbers
2927
bool negative = val < 0;
3028
if (negative) {
31-
val = -val;
29+
val = -val; // Make the value positive for digit extraction
3230
}
3331

34-
// Convert to string
32+
// Extract digits and store them in reverse order in the buffer
3533
while (val > 0) {
36-
*ptr-- = '0' + (val % 10);
34+
*--ptr = '0' + (val % 10);
3735
val /= 10;
3836
}
3937

38+
// If the number was negative, add the negative sign
4039
if (negative) {
41-
*ptr-- = '-';
40+
*--ptr = '-';
4241
}
4342

44-
// Append to string with length
45-
str.append(ptr + 1, buf + MaxDigits + 1);
43+
// Append the resulting string to the output
44+
out.append(ptr, buf + MaxDigits);
4645
}
4746

4847
// Base converter
@@ -59,54 +58,88 @@ void fromT(T val, std::string& str)
5958
fromNonZeroT(val, str);
6059
}
6160

61+
/**
62+
* @brief Converts a string view to an integral value of type T.
63+
*
64+
* This function attempts to parse a string representation of a number into an integer of the specified integral type T.
65+
* It performs validation to ensure the string is a valid representation of a number within the range of the target type and checks for overflow or invalid input.
66+
*
67+
* @tparam T The integral type to which the string should be converted.
68+
* @param[in] str The input string view representing the number.
69+
* @param[out] out The reference where the converted value will be stored, if successful.
70+
* @return true if the conversion was successful and `out` contains the parsed value.
71+
* @return false if the conversion failed due to invalid input, overflow, or out-of-range value.
72+
*
73+
* @note The function handles both signed and unsigned types, for signed types, it processes negative numbers correctly.
74+
* @note It also validates the input for edge cases like empty strings, leading zeros, or excessive length.
75+
*
76+
* @warning Ensure the input string is sanitized if sourced from untrusted input, as this function does not strip whitespace or handle non-numeric characters beyond validation.
77+
*/
6278
template<typename T>
6379
constexpr bool spanToT(std::string_view str, T& out)
6480
{
65-
static_assert(std::is_integral_v<T>);
81+
static_assert(std::is_integral_v<T>, "Template type T must be an integral type.");
6682

83+
// Define the unsigned type equivalent for T.
6784
using UT = typename std::make_unsigned<T>::type;
6885

6986
constexpr bool IsSigned = std::is_signed_v<T>;
70-
constexpr T Threshold = std::numeric_limits<T>::max() / 10;
7187

88+
// Define an overflow threshold for type T.
89+
// If an integer of type T exceeds this value, multiplying it by 10 will cause an overflow.
90+
constexpr T Threshold = std::numeric_limits<T>::max() / 10;
91+
92+
// Fail on empty input strings.
7293
if (str.empty()) {
7394
return false;
7495
}
7596

76-
// Handle negative for signed types
97+
// Handle negative numbers for signed types.
7798
bool isNegative = false;
7899
if constexpr (IsSigned) {
79100
if (str.front() == '-') {
80-
str.remove_prefix(1);
101+
str.remove_prefix(1); // Remove the negative sign.
81102
isNegative = true;
82-
if (str.empty()) return false;
103+
104+
// Fail if the string only contained a negative sign with no numeric characters
105+
if (str.empty()) {
106+
return false;
107+
}
83108
}
84109
}
85110

111+
// Fail on strings that are too long to be valid integers.
86112
if (str.length() > OpenShock::Util::Digits10CountMax<T>) {
87113
return false;
88114
}
89115

90116
// Special case for zero, also handles leading zeros and negative zero
91117
if (str.front() == '0') {
118+
// Fail if string has leading or negative zero's
92119
if (str.length() > 1 || isNegative) return false;
120+
121+
// Simple "0" string
93122
out = 0;
94123
return true;
95124
}
96125

126+
// Value accumulator.
97127
UT val = 0;
98128

99129
for (char c : str) {
130+
// Return false if the character is not a digit.
100131
if (c < '0' || c > '9') {
101132
return false;
102133
}
103134

135+
// Check for multiplication overflow before multiplying by 10.
104136
if (val > Threshold) {
105137
return false;
106138
}
107139

108140
val *= 10;
109141

142+
// Convert the character to a digit and check for addition overflow.
110143
uint8_t digit = c - '0';
111144
if (digit > std::numeric_limits<UT>::max() - val) {
112145
return false;
@@ -115,30 +148,24 @@ constexpr bool spanToT(std::string_view str, T& out)
115148
val += digit;
116149
}
117150

151+
// Special case handling for signed integers.
118152
if constexpr (IsSigned) {
119153
if (isNegative) {
120-
constexpr UT LowerLimit = static_cast<UT>(std::numeric_limits<T>::max()) + 1;
121-
122-
if (val > LowerLimit) {
154+
// Signed cast undeflow check, checks against the inverse lower limit for the signed type (e.g., 128 for int8_t).
155+
if (val > static_cast<UT>(std::numeric_limits<T>::max()) + 1) {
123156
return false;
124157
}
125158

126-
if (val == LowerLimit) {
127-
out = std::numeric_limits<T>::min();
128-
return true;
129-
}
130-
131-
out = -static_cast<T>(val);
132-
return true;
133-
}
134-
135-
if (val > std::numeric_limits<T>::max()) {
159+
// Assign negative value bits
160+
val = ~val + 1;
161+
} else if (val > std::numeric_limits<T>::max()) {
162+
// Fail on signed cast overflow
136163
return false;
137164
}
138165
}
139166

167+
// Cast result to output and return
140168
out = static_cast<T>(val);
141-
142169
return true;
143170
}
144171

src/GatewayClient.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,10 @@ void GatewayClient::_sendBootStatus()
137137
return;
138138
}
139139

140+
using namespace std::string_view_literals;
141+
140142
OpenShock::SemVer version;
141-
if (!OpenShock::TryParseSemVer(OPENSHOCK_FW_VERSION, version)) {
143+
if (!OpenShock::TryParseSemVer(OPENSHOCK_FW_VERSION ""sv, version)) {
142144
OS_LOGE(TAG, "Failed to parse firmware version");
143145
return;
144146
}

src/OtaUpdateManager.cpp

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -320,8 +320,6 @@ static void _otaUpdateTask(void* arg)
320320
OS_LOGE(TAG, "Failed to get requested version");
321321
continue;
322322
}
323-
324-
OS_LOGD(TAG, "Update requested for version %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this
325323
} else {
326324
OS_LOGD(TAG, "Checking for updates");
327325

@@ -330,15 +328,17 @@ static void _otaUpdateTask(void* arg)
330328
OS_LOGE(TAG, "Failed to fetch firmware version");
331329
continue;
332330
}
333-
334-
OS_LOGD(TAG, "Remote version: %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this
335331
}
336332

337-
if (version.toString() == OPENSHOCK_FW_VERSION) { // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this
333+
std::string versionStr = version.toString(); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this
334+
335+
if (versionStr == OPENSHOCK_FW_VERSION ""sv) {
338336
OS_LOGI(TAG, "Requested version is already installed");
339337
continue;
340338
}
341339

340+
OS_LOGD(TAG, "Updating to version: %.*s", versionStr.length(), versionStr.data());
341+
342342
// Generate random int32_t for this update.
343343
int32_t updateId = static_cast<int32_t>(esp_random());
344344
if (!Config::SetOtaUpdateId(updateId)) {
@@ -369,24 +369,24 @@ static void _otaUpdateTask(void* arg)
369369

370370
// Print release.
371371
OS_LOGD(TAG, "Firmware release:");
372-
OS_LOGD(TAG, " Version: %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this
373-
OS_LOGD(TAG, " App binary URL: %s", release.appBinaryUrl.c_str());
372+
OS_LOGD(TAG, " Version: %.*s", versionStr.length(), versionStr.data());
373+
OS_LOGD(TAG, " App binary URL: %.*s", release.appBinaryUrl.length(), release.appBinaryUrl.data());
374374
OS_LOGD(TAG, " App binary hash: %s", HexUtils::ToHex<32>(release.appBinaryHash).data());
375-
OS_LOGD(TAG, " Filesystem binary URL: %s", release.filesystemBinaryUrl.c_str());
375+
OS_LOGD(TAG, " Filesystem binary URL: %.*s", release.filesystemBinaryUrl.length(), release.filesystemBinaryUrl.data());
376376
OS_LOGD(TAG, " Filesystem binary hash: %s", HexUtils::ToHex<32>(release.filesystemBinaryHash).data());
377377

378378
// Get available app update partition.
379379
const esp_partition_t* appPartition = esp_ota_get_next_update_partition(nullptr);
380380
if (appPartition == nullptr) {
381-
OS_LOGE(TAG, "Failed to get app update partition"); // TODO: Send error message to server
381+
OS_LOGE(TAG, "Failed to get app update partition");
382382
_sendFailureMessage("Failed to get app update partition"sv);
383383
continue;
384384
}
385385

386386
// Get filesystem partition.
387387
const esp_partition_t* filesystemPartition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "static0");
388388
if (filesystemPartition == nullptr) {
389-
OS_LOGE(TAG, "Failed to find filesystem partition"); // TODO: Send error message to server
389+
OS_LOGE(TAG, "Failed to find filesystem partition");
390390
_sendFailureMessage("Failed to find filesystem partition"sv);
391391
continue;
392392
}
@@ -583,7 +583,7 @@ bool OtaUpdateManager::TryGetFirmwareBoards(const OpenShock::SemVer& version, st
583583

584584
static bool _tryParseIntoHash(std::string_view hash, uint8_t (&hashBytes)[32])
585585
{
586-
if (!HexUtils::TryParseHex(hash.data(), hash.size(), hashBytes, 32) != 16) {
586+
if (HexUtils::TryParseHex(hash.data(), hash.size(), hashBytes, 32) != 32) {
587587
OS_LOGE(TAG, "Failed to parse hash: %.*s", hash.size(), hash.data());
588588
return false;
589589
}

src/SemVer.cpp

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -245,19 +245,19 @@ std::string SemVer::toString() const
245245
str.reserve(length);
246246

247247
Convert::FromUint16(major, str);
248-
str += '.';
248+
str.push_back('.');
249249
Convert::FromUint16(minor, str);
250-
str += '.';
250+
str.push_back('.');
251251
Convert::FromUint16(patch, str);
252252

253253
if (!prerelease.empty()) {
254-
str += '-';
255-
str.append(prerelease.c_str(), prerelease.length());
254+
str.push_back('-');
255+
str.append(prerelease);
256256
}
257257

258258
if (!build.empty()) {
259-
str += '+';
260-
str.append(build.c_str(), build.length());
259+
str.push_back('+');
260+
str.append(build);
261261
}
262262

263263
return str;
@@ -355,7 +355,7 @@ bool OpenShock::TryParseSemVer(std::string_view semverStr, SemVer& semver)
355355
if (!restStr.empty()) {
356356
if (plusIdx != std::string_view::npos) {
357357
semver.build = restStr.substr((plusIdx - patchStr.length()) + 1);
358-
patchStr.remove_suffix(semver.build.length() + 1);
358+
restStr.remove_suffix(semver.build.length() + 1);
359359

360360
if (!semver.build.empty() && !_semverIsBuild(semver.build)) {
361361
OS_LOGE(TAG, "Invalid build: %s", semver.build.c_str());
@@ -364,7 +364,7 @@ bool OpenShock::TryParseSemVer(std::string_view semverStr, SemVer& semver)
364364
}
365365

366366
if (dashIdx != std::string_view::npos) {
367-
semver.prerelease = patchStr.substr(1);
367+
semver.prerelease = restStr.substr(1);
368368

369369
if (!semver.prerelease.empty() && !_semverIsPrerelease(semver.prerelease)) {
370370
OS_LOGE(TAG, "Invalid prerelease: %s", semver.prerelease.c_str());

src/http/HTTPRequestManager.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ HTTP::Response<std::size_t> _doGetStream(
368368
int64_t begin = OpenShock::millis();
369369
if (!client.begin(OpenShock::StringToArduinoString(url))) {
370370
OS_LOGE(TAG, "Failed to begin HTTP request");
371-
return {HTTP::RequestResult::RequestFailed, 0};
371+
return {HTTP::RequestResult::RequestFailed, 0, 0};
372372
}
373373

374374
for (auto& header : headers) {
@@ -434,7 +434,7 @@ HTTP::Response<std::size_t> _doGetStream(
434434
WiFiClient* stream = client.getStreamPtr();
435435
if (stream == nullptr) {
436436
OS_LOGE(TAG, "Failed to get stream");
437-
return {HTTP::RequestResult::RequestFailed, 0};
437+
return {HTTP::RequestResult::RequestFailed, 0, 0};
438438
}
439439

440440
StreamReaderResult result;

src/radio/RFTransmitter.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ void RFTransmitter::TransmitTask()
159159
auto it = std::find_if(sequences.begin(), sequences.end(), [&cmd](const sequence_t& seq) { return seq.encoder.shockerId() == cmd.shockerId; });
160160
if (it != sequences.end()) {
161161
it->encoder.fillSequence(cmd.type, cmd.intensity);
162+
it->until = cmd.until;
162163
continue;
163164
}
164165
}

src/serialization/JsonSerial.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ bool JsonSerial::ParseShockerCommand(const cJSON* root, JsonSerial::ShockerComma
5353
OS_LOGE(TAG, "value at 'type' is not a string");
5454
return false;
5555
}
56-
ShockerCommandType commandType;
56+
ShockerCommandType commandType = ShockerCommandType::Stop;
5757
if (!ShockerCommandTypeFromString(command->valuestring, commandType)) {
5858
OS_LOGE(TAG, "value at 'type' is not a valid shocker command (shock, vibrate, sound, light, stop)");
5959
return false;

0 commit comments

Comments
 (0)