477 lines
15 KiB
C++
477 lines
15 KiB
C++
#include "UserGpio.h"
|
|
|
|
#include <Arduino.h>
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
namespace {
|
|
|
|
const uint32_t MAX_TIMER_MILLIS = 24UL * 60UL * 60UL * 1000UL;
|
|
|
|
const char* skipSpaces(const char* text) {
|
|
while (*text == ' ') text++;
|
|
return text;
|
|
}
|
|
|
|
bool atEnd(const char* text) {
|
|
return *skipSpaces(text) == '\0';
|
|
}
|
|
|
|
bool parseUnsigned(const char*& cursor, uint32_t& value) {
|
|
const char* p = skipSpaces(cursor);
|
|
if (*p < '0' || *p > '9') return false;
|
|
|
|
uint32_t parsed = 0;
|
|
do {
|
|
const uint8_t digit = (uint8_t)(*p - '0');
|
|
if (parsed > (UINT32_MAX - digit) / 10U) return false;
|
|
parsed = parsed * 10U + digit;
|
|
p++;
|
|
} while (*p >= '0' && *p <= '9');
|
|
|
|
if (*p != '\0' && *p != ' ') return false;
|
|
cursor = p;
|
|
value = parsed;
|
|
return true;
|
|
}
|
|
|
|
bool parseDuration(const char*& cursor, uint32_t& duration_ms,
|
|
bool& display_milliseconds) {
|
|
const char* p = skipSpaces(cursor);
|
|
if (*p < '0' || *p > '9') return false;
|
|
|
|
uint32_t parsed = 0;
|
|
do {
|
|
const uint8_t digit = (uint8_t)(*p - '0');
|
|
if (parsed > (UINT32_MAX - digit) / 10U) return false;
|
|
parsed = parsed * 10U + digit;
|
|
p++;
|
|
} while (*p >= '0' && *p <= '9');
|
|
|
|
display_milliseconds = false;
|
|
if ((p[0] == 'm' || p[0] == 'M') && (p[1] == 's' || p[1] == 'S')) {
|
|
display_milliseconds = true;
|
|
p += 2;
|
|
} else if (*p == 's' || *p == 'S') {
|
|
p++;
|
|
}
|
|
|
|
if (*p != '\0' && *p != ' ') return false;
|
|
if (display_milliseconds) {
|
|
duration_ms = parsed;
|
|
} else {
|
|
if (parsed > UINT32_MAX / 1000UL) return false;
|
|
duration_ms = parsed * 1000UL;
|
|
}
|
|
|
|
cursor = p;
|
|
return true;
|
|
}
|
|
|
|
bool tokenEquals(const char* start, size_t length, const char* expected) {
|
|
size_t i = 0;
|
|
for (; i < length && expected[i] != '\0'; i++) {
|
|
if (tolower((unsigned char)start[i]) != tolower((unsigned char)expected[i])) return false;
|
|
}
|
|
return i == length && expected[i] == '\0';
|
|
}
|
|
|
|
bool parseState(const char*& cursor, UserGpio::State& state) {
|
|
const char* start = skipSpaces(cursor);
|
|
const char* end = start;
|
|
while (*end != '\0' && *end != ' ') end++;
|
|
const size_t length = (size_t)(end - start);
|
|
|
|
if (tokenEquals(start, length, "on")) {
|
|
state = UserGpio::STATE_ON;
|
|
} else if (tokenEquals(start, length, "off")) {
|
|
state = UserGpio::STATE_OFF;
|
|
} else if (tokenEquals(start, length, "reset")) {
|
|
state = UserGpio::STATE_RESET;
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
cursor = end;
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
UserGpio::UserGpio(mesh::MainBoard& board)
|
|
: _board(&board),
|
|
_controlled_mask(0),
|
|
_timer_mask(0),
|
|
_timer_final_on_mask(0),
|
|
_timer_final_reset_mask(0),
|
|
_timer_millisecond_mask(0),
|
|
_completion_mask(0),
|
|
_completion_on_mask(0),
|
|
_completion_reset_mask(0),
|
|
_next_recent_request(0) {
|
|
memset(_timer_deadline, 0, sizeof(_timer_deadline));
|
|
memset(_timer_request_id, 0, sizeof(_timer_request_id));
|
|
memset(_completion_request_id, 0, sizeof(_completion_request_id));
|
|
memset(_recent_requests, 0, sizeof(_recent_requests));
|
|
}
|
|
|
|
const char* UserGpio::stateName(State state) {
|
|
switch (state) {
|
|
case STATE_ON: return "on";
|
|
case STATE_OFF: return "off";
|
|
default: return "reset";
|
|
}
|
|
}
|
|
|
|
bool UserGpio::isAvailable(uint32_t pin) const {
|
|
return pin <= MAX_GPIO_PIN && _board->isUserGpioAvailable((uint8_t)pin);
|
|
}
|
|
|
|
UserGpio::State UserGpio::currentState(uint8_t pin) const {
|
|
if ((_controlled_mask & pinMask(pin)) == 0) return STATE_RESET;
|
|
return digitalRead(pin) == HIGH ? STATE_ON : STATE_OFF;
|
|
}
|
|
|
|
UserGpio::State UserGpio::timerFinalState(uint8_t pin) const {
|
|
const uint64_t mask = pinMask(pin);
|
|
if (_timer_final_reset_mask & mask) return STATE_RESET;
|
|
if (_timer_final_on_mask & mask) return STATE_ON;
|
|
return STATE_OFF;
|
|
}
|
|
|
|
void UserGpio::writeState(uint8_t pin, State state) {
|
|
const uint64_t mask = pinMask(pin);
|
|
if (state == STATE_RESET) {
|
|
pinMode(pin, INPUT);
|
|
_controlled_mask &= ~mask;
|
|
return;
|
|
}
|
|
|
|
const uint8_t level = state == STATE_ON ? HIGH : LOW;
|
|
// Prime the output latch before changing direction to minimize relay glitches.
|
|
digitalWrite(pin, level);
|
|
pinMode(pin, OUTPUT);
|
|
digitalWrite(pin, level);
|
|
_controlled_mask |= mask;
|
|
}
|
|
|
|
void UserGpio::cancelTimer(uint8_t pin) {
|
|
const uint64_t mask = pinMask(pin);
|
|
_timer_mask &= ~mask;
|
|
_timer_final_on_mask &= ~mask;
|
|
_timer_final_reset_mask &= ~mask;
|
|
_timer_millisecond_mask &= ~mask;
|
|
_timer_deadline[pin] = 0;
|
|
_timer_request_id[pin] = 0;
|
|
}
|
|
|
|
void UserGpio::scheduleTimer(uint8_t pin, uint32_t duration_ms,
|
|
bool display_milliseconds, State final_state,
|
|
uint32_t request_id) {
|
|
const uint64_t mask = pinMask(pin);
|
|
_timer_deadline[pin] = millis() + duration_ms;
|
|
_timer_request_id[pin] = request_id;
|
|
_timer_mask |= mask;
|
|
|
|
if (display_milliseconds) {
|
|
_timer_millisecond_mask |= mask;
|
|
} else {
|
|
_timer_millisecond_mask &= ~mask;
|
|
}
|
|
|
|
if (final_state == STATE_ON) {
|
|
_timer_final_on_mask |= mask;
|
|
} else {
|
|
_timer_final_on_mask &= ~mask;
|
|
}
|
|
|
|
if (final_state == STATE_RESET) {
|
|
_timer_final_reset_mask |= mask;
|
|
} else {
|
|
_timer_final_reset_mask &= ~mask;
|
|
}
|
|
}
|
|
|
|
bool UserGpio::isDuplicateTimedRequest(uint8_t pin, uint32_t request_id,
|
|
uint32_t request_source,
|
|
uint32_t command_spec) const {
|
|
if (request_id == 0 || request_source == 0) return false;
|
|
for (uint8_t i = 0; i < RECENT_REQUEST_COUNT; i++) {
|
|
const RecentTimedRequest& recent = _recent_requests[i];
|
|
if (recent.valid && recent.pin == pin && recent.request_id == request_id &&
|
|
recent.request_source == request_source &&
|
|
recent.command_spec == command_spec) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void UserGpio::rememberTimedRequest(uint8_t pin, uint32_t request_id,
|
|
uint32_t request_source,
|
|
uint32_t command_spec) {
|
|
if (request_id == 0 || request_source == 0) return;
|
|
RecentTimedRequest& recent = _recent_requests[_next_recent_request];
|
|
recent.request_id = request_id;
|
|
recent.request_source = request_source;
|
|
recent.command_spec = command_spec;
|
|
recent.pin = pin;
|
|
recent.valid = true;
|
|
_next_recent_request = (uint8_t)((_next_recent_request + 1U) % RECENT_REQUEST_COUNT);
|
|
}
|
|
|
|
void UserGpio::loop() {
|
|
if (_timer_mask == 0) return;
|
|
|
|
const uint32_t now = millis();
|
|
for (uint8_t pin = 0; pin <= MAX_GPIO_PIN; pin++) {
|
|
const uint64_t mask = pinMask(pin);
|
|
if ((_timer_mask & mask) == 0) continue;
|
|
if ((int32_t)(now - _timer_deadline[pin]) < 0) continue;
|
|
|
|
const State final_state = timerFinalState(pin);
|
|
const uint32_t request_id = _timer_request_id[pin];
|
|
cancelTimer(pin);
|
|
writeState(pin, final_state);
|
|
|
|
_completion_request_id[pin] = request_id;
|
|
_completion_mask |= mask;
|
|
if (final_state == STATE_ON) {
|
|
_completion_on_mask |= mask;
|
|
} else {
|
|
_completion_on_mask &= ~mask;
|
|
}
|
|
if (final_state == STATE_RESET) {
|
|
_completion_reset_mask |= mask;
|
|
} else {
|
|
_completion_reset_mask &= ~mask;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool UserGpio::takeCompletion(Completion& completion) {
|
|
if (_completion_mask == 0) return false;
|
|
|
|
for (uint8_t pin = 0; pin <= MAX_GPIO_PIN; pin++) {
|
|
const uint64_t mask = pinMask(pin);
|
|
if ((_completion_mask & mask) == 0) continue;
|
|
|
|
completion.pin = pin;
|
|
completion.request_id = _completion_request_id[pin];
|
|
if (_completion_reset_mask & mask) {
|
|
completion.state = STATE_RESET;
|
|
} else if (_completion_on_mask & mask) {
|
|
completion.state = STATE_ON;
|
|
} else {
|
|
completion.state = STATE_OFF;
|
|
}
|
|
|
|
_completion_mask &= ~mask;
|
|
_completion_on_mask &= ~mask;
|
|
_completion_reset_mask &= ~mask;
|
|
_completion_request_id[pin] = 0;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void UserGpio::formatPinStatus(uint8_t pin, const char* prefix, char* reply,
|
|
size_t reply_size) const {
|
|
const State current = currentState(pin);
|
|
const uint64_t mask = pinMask(pin);
|
|
if ((_timer_mask & mask) == 0) {
|
|
snprintf(reply, reply_size, "%sGPIO %u %s", prefix, pin, stateName(current));
|
|
return;
|
|
}
|
|
|
|
const int32_t remaining_delta = (int32_t)(_timer_deadline[pin] - millis());
|
|
const uint32_t remaining_ms = remaining_delta > 0
|
|
? (uint32_t)remaining_delta : 0;
|
|
if (_timer_millisecond_mask & mask) {
|
|
snprintf(reply, reply_size, "%sGPIO %u %s, %lums -> %s", prefix, pin,
|
|
stateName(current), (unsigned long)remaining_ms,
|
|
stateName(timerFinalState(pin)));
|
|
} else {
|
|
const uint32_t remaining_seconds = (remaining_ms + 999UL) / 1000UL;
|
|
snprintf(reply, reply_size, "%sGPIO %u %s, %lus -> %s", prefix, pin,
|
|
stateName(current), (unsigned long)remaining_seconds,
|
|
stateName(timerFinalState(pin)));
|
|
}
|
|
}
|
|
|
|
void UserGpio::formatAvailablePins(char* reply, size_t reply_size) const {
|
|
size_t used = (size_t)snprintf(reply, reply_size, "> available GPIOs:");
|
|
bool any = false;
|
|
for (uint8_t pin = 0; pin <= MAX_GPIO_PIN; pin++) {
|
|
if (!_board->isUserGpioAvailable(pin)) continue;
|
|
if (used >= reply_size) break;
|
|
const int written = snprintf(reply + used, reply_size - used, "%s%u",
|
|
any ? "," : " ", pin);
|
|
if (written < 0) break;
|
|
used += (size_t)written;
|
|
any = true;
|
|
}
|
|
if (!any) snprintf(reply, reply_size, "> available GPIOs: none");
|
|
}
|
|
|
|
void UserGpio::formatActivePins(char* reply, size_t reply_size) const {
|
|
size_t used = (size_t)snprintf(reply, reply_size, "> active GPIOs:");
|
|
bool any = false;
|
|
for (uint8_t pin = 0; pin <= MAX_GPIO_PIN; pin++) {
|
|
const uint64_t mask = pinMask(pin);
|
|
if ((_controlled_mask & mask) == 0 || !_board->isUserGpioAvailable(pin)) continue;
|
|
if (used >= reply_size) break;
|
|
const State current = currentState(pin);
|
|
int written;
|
|
if ((_timer_mask & mask) == 0) {
|
|
written = snprintf(reply + used, reply_size - used, "%s%u=%s",
|
|
any ? "," : " ", pin, stateName(current));
|
|
} else {
|
|
const int32_t remaining_delta = (int32_t)(_timer_deadline[pin] - millis());
|
|
const uint32_t remaining_ms = remaining_delta > 0
|
|
? (uint32_t)remaining_delta : 0;
|
|
if (_timer_millisecond_mask & mask) {
|
|
written = snprintf(reply + used, reply_size - used, "%s%u=%s(%lums->%s)",
|
|
any ? "," : " ", pin, stateName(current),
|
|
(unsigned long)remaining_ms,
|
|
stateName(timerFinalState(pin)));
|
|
} else {
|
|
const uint32_t remaining_seconds = (remaining_ms + 999UL) / 1000UL;
|
|
written = snprintf(reply + used, reply_size - used, "%s%u=%s(%lus->%s)",
|
|
any ? "," : " ", pin, stateName(current),
|
|
(unsigned long)remaining_seconds,
|
|
stateName(timerFinalState(pin)));
|
|
}
|
|
}
|
|
if (written < 0) break;
|
|
used += (size_t)written;
|
|
any = true;
|
|
}
|
|
if (!any) snprintf(reply, reply_size, "> active GPIOs: none");
|
|
}
|
|
|
|
void UserGpio::handleGet(const char* args, char* reply, size_t reply_size) {
|
|
loop();
|
|
args = skipSpaces(args);
|
|
|
|
if (*args == '\0') {
|
|
formatAvailablePins(reply, reply_size);
|
|
return;
|
|
}
|
|
|
|
const char* token_end = args;
|
|
while (*token_end != '\0' && *token_end != ' ') token_end++;
|
|
if (tokenEquals(args, (size_t)(token_end - args), "available") && atEnd(token_end)) {
|
|
formatAvailablePins(reply, reply_size);
|
|
return;
|
|
}
|
|
|
|
const size_t token_length = (size_t)(token_end - args);
|
|
const bool is_state_query = tokenEquals(args, token_length, "state") ||
|
|
tokenEquals(args, token_length, "states") ||
|
|
tokenEquals(args, token_length, "status");
|
|
if (is_state_query) {
|
|
const char* state_args = skipSpaces(token_end);
|
|
if (*state_args == '\0') {
|
|
formatActivePins(reply, reply_size);
|
|
return;
|
|
}
|
|
|
|
uint32_t state_pin;
|
|
if (!parseUnsigned(state_args, state_pin) || !atEnd(state_args)) {
|
|
snprintf(reply, reply_size, "Error: use get gpio state [pin]");
|
|
return;
|
|
}
|
|
if (!isAvailable(state_pin)) {
|
|
snprintf(reply, reply_size, "Error: GPIO %lu is unavailable",
|
|
(unsigned long)state_pin);
|
|
return;
|
|
}
|
|
formatPinStatus((uint8_t)state_pin, "> ", reply, reply_size);
|
|
return;
|
|
}
|
|
|
|
uint32_t parsed_pin;
|
|
if (!parseUnsigned(args, parsed_pin) || !atEnd(args)) {
|
|
snprintf(reply, reply_size, "Error: use get gpio [state|states|status [pin]|pin]");
|
|
return;
|
|
}
|
|
if (!isAvailable(parsed_pin)) {
|
|
snprintf(reply, reply_size, "Error: GPIO %lu is unavailable", (unsigned long)parsed_pin);
|
|
return;
|
|
}
|
|
|
|
formatPinStatus((uint8_t)parsed_pin, "> ", reply, reply_size);
|
|
}
|
|
|
|
UserGpio::SetResult UserGpio::handleSet(const char* args, char* reply,
|
|
size_t reply_size, uint32_t request_id,
|
|
uint32_t request_source) {
|
|
SetResult result = {SetResult::ERROR, 0, false};
|
|
loop();
|
|
|
|
uint32_t parsed_pin;
|
|
State initial_state;
|
|
if (!parseUnsigned(args, parsed_pin) || !parseState(args, initial_state)) {
|
|
snprintf(reply, reply_size, "Error: use set gpio <pin> on|off|reset [duration on|off|reset]");
|
|
return result;
|
|
}
|
|
if (!isAvailable(parsed_pin)) {
|
|
snprintf(reply, reply_size, "Error: GPIO %lu is unavailable", (unsigned long)parsed_pin);
|
|
return result;
|
|
}
|
|
|
|
const uint8_t pin = (uint8_t)parsed_pin;
|
|
result.pin = pin;
|
|
args = skipSpaces(args);
|
|
if (*args == '\0') {
|
|
result.cancelled_timer = (_timer_mask & pinMask(pin)) != 0;
|
|
cancelTimer(pin);
|
|
writeState(pin, initial_state);
|
|
snprintf(reply, reply_size, "OK - GPIO %u %s", pin, stateName(initial_state));
|
|
result.outcome = SetResult::APPLIED;
|
|
return result;
|
|
}
|
|
|
|
uint32_t duration_ms;
|
|
bool display_milliseconds;
|
|
State final_state;
|
|
if (initial_state == STATE_RESET || !parseDuration(args, duration_ms, display_milliseconds) ||
|
|
!parseState(args, final_state) || !atEnd(args)) {
|
|
snprintf(reply, reply_size, "Error: use set gpio <pin> on|off [duration on|off|reset]");
|
|
return result;
|
|
}
|
|
if (duration_ms == 0 || duration_ms > MAX_TIMER_MILLIS) {
|
|
snprintf(reply, reply_size, "Error: timer must be 1ms-24h");
|
|
return result;
|
|
}
|
|
|
|
const uint32_t command_spec = duration_ms
|
|
| ((uint32_t)(initial_state == STATE_ON) << 27)
|
|
| ((uint32_t)final_state << 28)
|
|
| ((uint32_t)display_milliseconds << 30);
|
|
if (isDuplicateTimedRequest(pin, request_id, request_source, command_spec)) {
|
|
formatPinStatus(pin, "OK - duplicate ignored; ", reply, reply_size);
|
|
result.outcome = SetResult::DUPLICATE_IGNORED;
|
|
return result;
|
|
}
|
|
|
|
result.cancelled_timer = (_timer_mask & pinMask(pin)) != 0;
|
|
cancelTimer(pin);
|
|
writeState(pin, initial_state);
|
|
scheduleTimer(pin, duration_ms, display_milliseconds, final_state, request_id);
|
|
rememberTimedRequest(pin, request_id, request_source, command_spec);
|
|
if (display_milliseconds) {
|
|
snprintf(reply, reply_size, "OK - GPIO %u %s for %lums, then %s", pin,
|
|
stateName(initial_state), (unsigned long)duration_ms,
|
|
stateName(final_state));
|
|
} else {
|
|
snprintf(reply, reply_size, "OK - GPIO %u %s for %lus, then %s", pin,
|
|
stateName(initial_state), (unsigned long)(duration_ms / 1000UL),
|
|
stateName(final_state));
|
|
}
|
|
result.outcome = SetResult::TIMER_STARTED;
|
|
return result;
|
|
}
|