Browse Source

terminal: __FlashStringHelper -> StringView

- clean-up comparison routines and get rid of most of the casts
- reduces total number of calls to terminalRegisterCommand
  replace with a func accepting list of commands, which in
  turn get instantiated as constexpr PROGMEM arrays
- reduce ram fragmentation, since we don't need to alloc as much
- reduce flash consumption b/c of lambda -> standalone func conversion
pull/2552/head
Maxim Prokhorov 1 year ago
parent
commit
3d580f3577
37 changed files with 1556 additions and 977 deletions
  1. +34
    -22
      code/espurna/button.cpp
  2. +15
    -4
      code/espurna/crash.cpp
  3. +30
    -12
      code/espurna/debug.cpp
  4. +16
    -4
      code/espurna/gpio.cpp
  5. +23
    -5
      code/espurna/homeassistant.cpp
  6. +41
    -25
      code/espurna/i2c.cpp
  7. +30
    -10
      code/espurna/ifan.cpp
  8. +22
    -8
      code/espurna/influxdb.cpp
  9. +30
    -22
      code/espurna/ir.cpp
  10. +32
    -24
      code/espurna/led.cpp
  11. +167
    -134
      code/espurna/light.cpp
  12. +26
    -10
      code/espurna/lightfox.cpp
  13. +37
    -25
      code/espurna/mqtt.cpp
  14. +15
    -5
      code/espurna/network.cpp
  15. +12
    -6
      code/espurna/nofuss.cpp
  16. +13
    -3
      code/espurna/ntp.cpp
  17. +16
    -10
      code/espurna/ota_asynctcp.cpp
  18. +16
    -10
      code/espurna/ota_httpupdate.cpp
  19. +27
    -19
      code/espurna/pwm.cpp
  20. +55
    -44
      code/espurna/relay.cpp
  21. +66
    -47
      code/espurna/rfbridge.cpp
  22. +92
    -54
      code/espurna/rpnrules.cpp
  23. +88
    -51
      code/espurna/rtcmem.cpp
  24. +85
    -64
      code/espurna/sensor.cpp
  25. +86
    -69
      code/espurna/sensors/PZEM004TSensor.h
  26. +25
    -17
      code/espurna/sensors/PZEM004TV30Sensor.h
  27. +35
    -12
      code/espurna/settings.cpp
  28. +48
    -35
      code/espurna/storage_eeprom.cpp
  29. +38
    -27
      code/espurna/telnet.cpp
  30. +54
    -20
      code/espurna/terminal.cpp
  31. +2
    -1
      code/espurna/terminal.h
  32. +39
    -31
      code/espurna/terminal_commands.cpp
  33. +18
    -4
      code/espurna/terminal_commands.h
  34. +47
    -27
      code/espurna/tuya.cpp
  35. +5
    -0
      code/espurna/types.h
  36. +133
    -106
      code/espurna/wifi.cpp
  37. +38
    -10
      code/test/unit/src/terminal/terminal.cpp

+ 34
- 22
code/espurna/button.cpp View File

@ -767,6 +767,39 @@ void setup() {
} // namespace
} // namespace query
} // namespace settings
namespace terminal {
void button(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 2) {
size_t id;
if (!tryParseId(ctx.argv[1].c_str(), buttonCount, id)) {
terminalError(ctx, F("Invalid button ID"));
return;
}
settingsDump(ctx, espurna::button::settings::query::IndexedSettings, id);
terminalOK(ctx);
return;
}
size_t id { 0 };
for (const auto& button : espurna::button::internal::buttons) {
ctx.output.printf_P(
PSTR("button%u {%s}\n"), id++,
button.event_emitter
? (button.event_emitter->pin()->description().c_str())
: PSTR("Virtual"));
}
}
alignas(4) static constexpr char Button[] PROGMEM = "button";
static constexpr ::terminal::Command Commands[] PROGMEM {
{Button, button},
};
} // namespace terminal
} // namespace button
} // namespace espurna
@ -1414,28 +1447,7 @@ void buttonSetup() {
DEBUG_MSG_P(PSTR("[BUTTON] Number of buttons: %u\n"), count);
#if TERMINAL_SUPPORT
terminalRegisterCommand(F("BUTTON"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 2) {
size_t id;
if (!tryParseId(ctx.argv[1].c_str(), buttonCount, id)) {
terminalError(ctx, F("Invalid button ID"));
return;
}
settingsDump(ctx, espurna::button::settings::query::IndexedSettings, id);
terminalOK(ctx);
return;
}
size_t id { 0 };
for (const auto& button : espurna::button::internal::buttons) {
ctx.output.printf_P(
PSTR("button%u {%s}\n"), id++,
button.event_emitter
? (button.event_emitter->pin()->description().c_str())
: PSTR("Virtual"));
}
});
espurna::terminal::add(espurna::button::terminal::Commands);
#endif
if (count) {


+ 15
- 4
code/espurna/crash.cpp View File

@ -222,6 +222,20 @@ void dump(Print& print) {
dump(print, true);
}
#if TERMINAL_SUPPORT
alignas(4) static constexpr char Name[] PROGMEM = "CRASH";
void command(::terminal::CommandContext&& ctx) {
debug::crash::forceDump(ctx.output);
terminalOK(ctx);
}
static constexpr ::terminal::Command Commands[] PROGMEM {
{Name, command},
};
#endif
} // namespace crash
} // namespace
} // namespace debug
@ -331,10 +345,7 @@ void crashSetup() {
}
#if TERMINAL_SUPPORT
terminalRegisterCommand(F("CRASH"), [](::terminal::CommandContext&& ctx) {
debug::crash::forceDump(ctx.output);
terminalOK(ctx);
});
espurna::terminal::add(debug::crash::Commands);
#endif
debug::crash::enableFromSettings();


+ 30
- 12
code/espurna/debug.cpp View File

@ -613,6 +613,35 @@ void onBoot() {
configure();
}
#if TERMINAL_SUPPORT
namespace terminal {
alignas(4) static constexpr char DebugBuffer[] PROGMEM = "DEBUG.BUFFER";
void debug_buffer(::terminal::CommandContext&& ctx) {
debug::buffer::disable();
if (!debug::buffer::size()) {
terminalError(ctx, F("buffer is empty\n"));
return;
}
ctx.output.printf_P(PSTR("buffer size: %u / %u bytes\n"),
debug::buffer::size(), debug::buffer::capacity());
debug::buffer::dump(ctx.output);
terminalOK(ctx);
}
static constexpr ::terminal::Command commands[] PROGMEM {
{DebugBuffer, debug_buffer},
};
void setup() {
espurna::terminal::add(commands);
}
} // namespace terminal
#endif
} // namespace
} // namespace debug
} // namespace espurna
@ -684,18 +713,7 @@ void debugSetup() {
#if DEBUG_LOG_BUFFER_SUPPORT
#if TERMINAL_SUPPORT
terminalRegisterCommand(F("DEBUG.BUFFER"), [](::terminal::CommandContext&& ctx) {
espurna::debug::buffer::disable();
if (!espurna::debug::buffer::size()) {
terminalError(ctx, F("buffer is empty\n"));
return;
}
ctx.output.printf_P(PSTR("buffer size: %u / %u bytes\n"),
espurna::debug::buffer::size(), espurna::debug::buffer::capacity());
espurna::debug::buffer::dump(ctx.output);
terminalOK(ctx);
});
espurna::debug::terminal::setup();
#endif
#endif
}


+ 16
- 4
code/espurna/gpio.cpp View File

@ -607,6 +607,8 @@ void add(Origin origin) {
#if TERMINAL_SUPPORT
namespace terminal {
alignas(4) static constexpr char GpioLocks[] PROGMEM = "GPIO.LOCKS";
void gpio_list_origins(::terminal::CommandContext&& ctx) {
for (const auto& origin : origin::internal::origins) {
ctx.output.printf_P(PSTR("%c %s GPIO%hhu\t%d:%s:%s\n"),
@ -618,6 +620,8 @@ void gpio_list_origins(::terminal::CommandContext&& ctx) {
}
}
alignas(4) static constexpr char Gpio[] PROGMEM = "GPIO";
void gpio_read_write(::terminal::CommandContext&& ctx) {
const int pin = (ctx.argv.size() >= 2)
? espurna::settings::internal::convert<int>(ctx.argv[1])
@ -692,6 +696,8 @@ void gpio_read_write(::terminal::CommandContext&& ctx) {
terminalOK(ctx);
}
alignas(4) static constexpr char RegRead[] PROGMEM = "REG.READ";
void reg_read(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 2) {
const auto convert = espurna::settings::internal::convert<uint32_t>;
@ -706,6 +712,8 @@ void reg_read(::terminal::CommandContext&& ctx) {
terminalError(ctx, F("REG.READ <ADDRESS>"));
}
alignas(4) static constexpr char RegWrite[] PROGMEM = "REG.WRITE";
void reg_write(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 3) {
const auto convert = espurna::settings::internal::convert<uint32_t>;
@ -723,11 +731,15 @@ void reg_write(::terminal::CommandContext&& ctx) {
terminalError(ctx, F("REG.WRITE <ADDRESS> <VALUE>"));
}
static constexpr espurna::terminal::Command Commands[] PROGMEM {
{GpioLocks, gpio_list_origins},
{Gpio, gpio_read_write},
{RegRead, reg_read},
{RegWrite, reg_write},
};
void setup() {
terminalRegisterCommand(F("GPIO.LOCKS"), gpio_list_origins);
terminalRegisterCommand(F("GPIO"), gpio_read_write);
terminalRegisterCommand(F("REG.READ"), reg_read);
terminalRegisterCommand(F("REG.WRITE"), reg_write);
espurna::terminal::add(Commands);
}
} // namespace terminal


+ 23
- 5
code/espurna/homeassistant.cpp View File

@ -1041,6 +1041,28 @@ bool onKeyCheck(espurna::StringView key, const JsonVariant& value) {
} // namespace web
#if TERMINAL_SUPPORT
namespace terminal {
alignas(4) static constexpr char Send[] PROGMEM = "HA.SEND";
void send(::terminal::CommandContext&& ctx) {
internal::state = internal::State::Pending;
publishDiscovery();
terminalOK(ctx);
}
static constexpr ::terminal::Command Commands[] PROGMEM {
{Send, send},
};
void setup() {
espurna::terminal::add(Commands);
}
} // namespace terminal
#endif
void setup() {
#if WEB_SUPPORT
wsRegister()
@ -1056,11 +1078,7 @@ void setup() {
mqttRegister(mqttCallback);
#if TERMINAL_SUPPORT
terminalRegisterCommand(F("HA.SEND"), [](::terminal::CommandContext&& ctx) {
internal::state = internal::State::Pending;
publishDiscovery();
terminalOK(ctx);
});
terminal::setup();
#endif
espurnaRegisterReload(configure);


+ 41
- 25
code/espurna/i2c.cpp View File

@ -312,40 +312,56 @@ void init() {
}
#if TERMINAL_SUPPORT
namespace terminal {
void initTerminalCommands() {
terminalRegisterCommand(F("I2C.LOCKED"), [](::terminal::CommandContext&& ctx) {
for (size_t address = 0; address < lock::storage.size(); ++address) {
if (lock::storage.test(address)) {
ctx.output.printf_P(PSTR("0x%02X\n"), address);
}
}
terminalOK(ctx);
});
alignas(4) static constexpr char Locked[] PROGMEM = "I2C.LOCKED";
terminalRegisterCommand(F("I2C.SCAN"), [](::terminal::CommandContext&& ctx) {
size_t devices { 0 };
i2c::scan([&](uint8_t address) {
++devices;
void locked(::terminal::CommandContext&& ctx) {
for (size_t address = 0; address < lock::storage.size(); ++address) {
if (lock::storage.test(address)) {
ctx.output.printf_P(PSTR("0x%02X\n"), address);
});
if (devices) {
ctx.output.printf_P(PSTR("found %zu device(s)\n"), devices);
terminalOK(ctx);
return;
}
}
terminalOK(ctx);
}
alignas(4) static constexpr char Scan[] PROGMEM = "I2C.SCAN";
terminalError(ctx, F("no devices found"));
void scan(::terminal::CommandContext&& ctx) {
size_t devices { 0 };
i2c::scan([&](uint8_t address) {
++devices;
ctx.output.printf_P(PSTR("0x%02X\n"), address);
});
terminalRegisterCommand(F("I2C.CLEAR"), [](::terminal::CommandContext&& ctx) {
ctx.output.printf_P(PSTR("result %d\n"), i2c::clear());
if (devices) {
ctx.output.printf_P(PSTR("found %zu device(s)\n"), devices);
terminalOK(ctx);
});
return;
}
terminalError(ctx, F("no devices found"));
}
alignas(4) static constexpr char Clear[] PROGMEM = "I2C.CLEAR";
void clear(::terminal::CommandContext&& ctx) {
ctx.output.printf_P(PSTR("result %d\n"), i2c::clear());
terminalOK(ctx);
}
static constexpr ::terminal::Command Commands[] PROGMEM {
{Locked, locked},
{Scan, scan},
{Clear, clear},
};
void setup() {
espurna::terminal::add(Commands);
}
} // namespace terminal
#endif // TERMINAL_SUPPORT
} // namespace
@ -578,7 +594,7 @@ void i2cSetup() {
espurna::i2c::init();
#if TERMINAL_SUPPORT
espurna::i2c::initTerminalCommands();
espurna::i2c::terminal::setup();
#endif
if (espurna::i2c::build::performScanOnBoot()) {


+ 30
- 10
code/espurna/ifan.cpp View File

@ -284,6 +284,35 @@ private:
const Config& _config;
};
#if TERMINAL_SUPPORT
namespace terminal {
alignas(4) static constexpr char Speed[] PROGMEM = "SPEED";
void speed(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 2) {
updateSpeedFromPayload(ctx.argv[1]);
}
ctx.output.printf_P(PSTR("%s %s\n"),
(config.speed != FanSpeed::Off)
? PSTR("speed")
: PSTR("fan is"),
speedToPayload(config.speed).c_str());
terminalOK(ctx);
}
static constexpr ::terminal::Command Commands[] PROGMEM {
{Speed, speed},
};
void setup() {
espurna::terminal::add(Commands);
}
} // namespace terminal
#endif
void setup() {
config.state_pins = setupStatePins();
@ -322,16 +351,7 @@ void setup() {
#endif
#if TERMINAL_SUPPORT
terminalRegisterCommand(F("SPEED"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 2) {
updateSpeedFromPayload(ctx.argv[1]);
}
ctx.output.printf_P(PSTR("%s %s\n"),
(config.speed != FanSpeed::Off) ? "speed" : "fan is",
speedToPayload(config.speed).c_str());
terminalOK(ctx);
});
terminal::setup();
#endif
}


+ 22
- 8
code/espurna/influxdb.cpp View File

@ -278,6 +278,27 @@ bool _idbHeartbeat(espurna::heartbeat::Mask mask) {
return true;
}
#if TERMINAL_SUPPORT
alignas(4) static constexpr char IdbSend[] PROGMEM = "IDB.SEND";
static void idbTerminalSend(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 4) {
terminalError(ctx, F("idb.send <topic> <id> <value>"));
return;
}
idbSend(ctx.argv[1].c_str(), ctx.argv[2].toInt(), ctx.argv[3].c_str());
}
static constexpr ::terminal::Command IdbCommands[] {
{IdbSend, idbTerminalSend},
};
static void idbTerminalSetup() {
espurna::terminal::add(IdbCommands);
}
#endif
void idbSetup() {
systemHeartbeat(_idbHeartbeat);
systemHeartbeat(_idbHeartbeat,
@ -305,14 +326,7 @@ void idbSetup() {
espurnaRegisterLoop(_idbFlush);
#if TERMINAL_SUPPORT
terminalRegisterCommand(F("IDB.SEND"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 4) {
terminalError(ctx, F("idb.send <topic> <id> <value>"));
return;
}
idbSend(ctx.argv[1].c_str(), ctx.argv[2].toInt(), ctx.argv[3].c_str());
});
idbTerminalSetup();
#endif
}


+ 30
- 22
code/espurna/ir.cpp View File

@ -1580,35 +1580,43 @@ void process(rx::DecodeResult& result) {
}
}
void setup() {
terminalRegisterCommand(F("IR.SEND"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 2) {
auto view = StringView{ctx.argv[1]};
alignas(4) static constexpr char IrSend[] PROGMEM = "IR.SEND";
auto simple = ir::simple::parse(view);
if (ir::tx::enqueue(std::move(simple))) {
terminalOK(ctx);
return;
}
void send(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 2) {
auto view = StringView{ctx.argv[1]};
auto state = ir::state::parse(view);
if (ir::tx::enqueue(std::move(state))) {
terminalOK(ctx);
return;
}
auto simple = ir::simple::parse(view);
if (ir::tx::enqueue(std::move(simple))) {
terminalOK(ctx);
return;
}
auto raw = ir::raw::parse(view);
if (ir::tx::enqueue(std::move(raw))) {
terminalOK(ctx);
return;
}
auto state = ir::state::parse(view);
if (ir::tx::enqueue(std::move(state))) {
terminalOK(ctx);
return;
}
terminalError(ctx, F("Invalid payload"));
auto raw = ir::raw::parse(view);
if (ir::tx::enqueue(std::move(raw))) {
terminalOK(ctx);
return;
}
terminalError(ctx, F("IR.SEND <PAYLOAD>"));
});
terminalError(ctx, F("Invalid payload"));
return;
}
terminalError(ctx, F("IR.SEND <PAYLOAD>"));
}
static constexpr ::terminal::Command Commands[] PROGMEM {
{IrSend, send},
};
void setup() {
espurna::terminal::add(Commands);
}
} // namespace terminal


+ 32
- 24
code/espurna/led.cpp View File

@ -1020,36 +1020,44 @@ void onConnected(JsonObject& root) {
namespace terminal {
namespace {
void setup() {
terminalRegisterCommand(F("LED"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() > 1) {
size_t id;
if (!tryParseId(ctx.argv[1].c_str(), ledCount, id)) {
terminalError(ctx, F("Invalid ledID"));
return;
}
auto& led = internal::leds[id];
if (ctx.argv.size() == 2) {
settingsDump(ctx, settings::query::IndexedSettings, id);
} else if (ctx.argv.size() > 2) {
led.mode(LedMode::Manual);
pattern(led, Pattern(ctx.argv[2]));
}
schedule();
terminalOK(ctx);
alignas(4) static constexpr char Led[] PROGMEM = "LED";
void led(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() > 1) {
size_t id;
if (!tryParseId(ctx.argv[1].c_str(), ledCount, id)) {
terminalError(ctx, F("Invalid ledID"));
return;
}
size_t id { 0 };
for (const auto& led : internal::leds) {
ctx.output.printf_P(
auto& led = internal::leds[id];
if (ctx.argv.size() == 2) {
settingsDump(ctx, settings::query::IndexedSettings, id);
} else if (ctx.argv.size() > 2) {
led.mode(LedMode::Manual);
pattern(led, Pattern(ctx.argv[2]));
}
schedule();
terminalOK(ctx);
return;
}
size_t id { 0 };
for (const auto& led : internal::leds) {
ctx.output.printf_P(
PSTR("led%u {Gpio=%hhu Mode=%s}\n"), id++, led.pin(),
espurna::settings::internal::serialize(led.mode()).c_str());
}
});
}
}
static constexpr ::terminal::Command Commands[] PROGMEM {
{Led, led},
};
void setup() {
espurna::terminal::add(Commands);
}
} // namespace


+ 167
- 134
code/espurna/light.cpp View File

@ -2443,7 +2443,6 @@ void _lightWebSocketOnAction(uint32_t client_id, const char* action, JsonObject&
#endif
#if TERMINAL_SUPPORT
namespace {
// TODO: at this point we have 3 different state save / restoration
@ -2494,171 +2493,205 @@ void _lightNotificationInit(size_t channel) {
lightState(true);
}
void _lightInitCommands() {
alignas(4) static constexpr char LightCommandNotify[] PROGMEM = "NOTIFY";
terminalRegisterCommand(F("NOTIFY"), [](::terminal::CommandContext&& ctx) {
static constexpr auto NotifyTransition = LightTransition{
.time = espurna::duration::Seconds(1),
.step = espurna::duration::Milliseconds(50),
};
static void _lightCommandNotify(::terminal::CommandContext&& ctx) {
static constexpr auto NotifyTransition = LightTransition{
.time = espurna::duration::Seconds(1),
.step = espurna::duration::Milliseconds(50),
};
if ((ctx.argv.size() < 2) || (ctx.argv.size() > 5)) {
terminalError(ctx, F("NOTIFY <CHANNEL> [<REPEATS>] [<TIME>] [<STEP>]"));
return;
}
if ((ctx.argv.size() < 2) || (ctx.argv.size() > 5)) {
terminalError(ctx, F("NOTIFY <CHANNEL> [<REPEATS>] [<TIME>] [<STEP>]"));
return;
}
size_t channel;
if (!_lightTryParseChannel(ctx.argv[1].c_str(), channel)) {
terminalError(ctx, F("Invalid channel ID"));
return;
}
size_t channel;
if (!_lightTryParseChannel(ctx.argv[1].c_str(), channel)) {
terminalError(ctx, F("Invalid channel ID"));
return;
}
using Duration = espurna::duration::Milliseconds;
const auto time_convert = espurna::settings::internal::convert<Duration>;
using Duration = espurna::duration::Milliseconds;
const auto time_convert = espurna::settings::internal::convert<Duration>;
constexpr auto DefaultNotification = LightTransition {
.time = Duration(500),
.step = Duration(25),
};
constexpr auto DefaultNotification = LightTransition {
.time = Duration(500),
.step = Duration(25),
};
const auto notification = (ctx.argv.size() >= 4)
? LightTransition{
.time = time_convert(ctx.argv[2]),
.step = time_convert(ctx.argv[3])}
: DefaultNotification;
const auto notification = (ctx.argv.size() >= 4)
? LightTransition{
.time = time_convert(ctx.argv[2]),
.step = time_convert(ctx.argv[3])}
: DefaultNotification;
constexpr size_t DefaultRepeats { 3 };
constexpr size_t DefaultRepeats { 3 };
const auto repeats_convert = espurna::settings::internal::convert<size_t>;
const auto repeats = (ctx.argv.size() >= 5)
? repeats_convert(ctx.argv[4])
: DefaultRepeats;
const auto repeats_convert = espurna::settings::internal::convert<size_t>;
const auto repeats = (ctx.argv.size() >= 5)
? repeats_convert(ctx.argv[4])
: DefaultRepeats;
auto state = std::make_shared<LightValuesState>(_lightValuesState());
auto restore = [state]() {
_lightNotificationRestore(*state);
lightUpdate(NotifyTransition, 0, false);
};
auto state = std::make_shared<LightValuesState>(_lightValuesState());
auto restore = [state]() {
_lightNotificationRestore(*state);
lightUpdate(NotifyTransition, 0, false);
};
auto on = [channel, notification]() {
lightChannel(channel, espurna::light::ValueMax);
lightUpdateSequence(notification);
};
auto on = [channel, notification]() {
lightChannel(channel, espurna::light::ValueMax);
lightUpdateSequence(notification);
};
auto off = [channel, notification]() {
lightChannel(channel, espurna::light::ValueMin);
lightUpdateSequence(notification);
};
auto off = [channel, notification]() {
lightChannel(channel, espurna::light::ValueMin);
lightUpdateSequence(notification);
};
_lightNotificationInit(channel);
lightUpdate(NotifyTransition);
_lightNotificationInit(channel);
lightUpdate(NotifyTransition);
LightSequenceCallbacks callbacks;
callbacks.push_front(restore);
for (size_t n = 0; n < repeats; ++n) {
callbacks.push_front(off);
callbacks.push_front(on);
}
LightSequenceCallbacks callbacks;
callbacks.push_front(restore);
for (size_t n = 0; n < repeats; ++n) {
callbacks.push_front(off);
callbacks.push_front(on);
}
lightSequence(std::move(callbacks));
});
lightSequence(std::move(callbacks));
}
terminalRegisterCommand(F("LIGHT"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() > 1) {
if (!_lightParsePayload(ctx.argv[1].c_str())) {
terminalError(ctx, F("Invalid payload"));
return;
}
lightUpdate();
alignas(4) static constexpr char LightCommand[] PROGMEM = "LIGHT";
static void _lightCommand(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() > 1) {
if (!_lightParsePayload(ctx.argv[1].c_str())) {
terminalError(ctx, F("Invalid payload"));
return;
}
lightUpdate();
}
ctx.output.printf("%s\n", _light_state ? "ON" : "OFF");
terminalOK(ctx);
});
ctx.output.printf("%s\n", _light_state ? "ON" : "OFF");
terminalOK(ctx);
}
terminalRegisterCommand(F("BRIGHTNESS"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() > 1) {
_lightAdjustBrightness(ctx.argv[1]);
lightUpdate();
}
ctx.output.printf("%ld\n", _light_brightness);
terminalOK(ctx);
});
alignas(4) static constexpr char LightCommandBrightness[] PROGMEM = "BRIGHTNESS";
terminalRegisterCommand(F("CHANNEL"), [](::terminal::CommandContext&& ctx) {
const size_t Channels { _light_channels.size() };
if (!Channels) {
terminalError(ctx, F("No channels configured"));
static void _lightCommandBrightness(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() > 1) {
_lightAdjustBrightness(ctx.argv[1]);
lightUpdate();
}
ctx.output.printf("%ld\n", _light_brightness);
terminalOK(ctx);
}
alignas(4) static constexpr char LightCommandChannel[] PROGMEM = "CHANNEL";
static void _lightCommandChannel(::terminal::CommandContext&& ctx) {
const size_t Channels { _light_channels.size() };
if (!Channels) {
terminalError(ctx, F("No channels configured"));
return;
}
auto description = [&](size_t channel) {
ctx.output.printf_P(PSTR("#%zu (%s) input:%ld value:%ld target:%ld current:%s\n"),
channel, _lightDesc(Channels, channel),
_light_channels[channel].inputValue,
_light_channels[channel].value,
_light_channels[channel].target,
String(_light_channels[channel].current, 2).c_str());
};
if (ctx.argv.size() > 2) {
size_t id;
if (!_lightTryParseChannel(ctx.argv[1].c_str(), id)) {
terminalError(ctx, F("Invalid channel ID"));
return;
}
auto description = [&](size_t channel) {
ctx.output.printf_P(PSTR("#%zu (%s) input:%ld value:%ld target:%ld current:%s\n"),
channel, _lightDesc(Channels, channel),
_light_channels[channel].inputValue,
_light_channels[channel].value,
_light_channels[channel].target,
String(_light_channels[channel].current, 2).c_str());
};
_lightAdjustChannel(id, ctx.argv[2]);
lightUpdate();
description(id);
} else {
for (size_t index = 0; index < Channels; ++index) {
description(index);
}
}
if (ctx.argv.size() > 2) {
size_t id;
if (!_lightTryParseChannel(ctx.argv[1].c_str(), id)) {
terminalError(ctx, F("Invalid channel ID"));
return;
}
terminalOK(ctx);
}
_lightAdjustChannel(id, ctx.argv[2]);
lightUpdate();
description(id);
} else {
for (size_t index = 0; index < Channels; ++index) {
description(index);
}
}
alignas(4) static constexpr char LightCommandRgb[] PROGMEM = "RGB";
terminalOK(ctx);
});
static void _lightCommandRgb(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() > 1) {
_lightFromRgbPayload(ctx.argv[1].c_str());
lightUpdate();
}
terminalRegisterCommand(F("RGB"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() > 1) {
_lightFromRgbPayload(ctx.argv[1].c_str());
lightUpdate();
}
ctx.output.printf_P(PSTR("rgb %s\n"), _lightRgbPayload(_lightToTargetRgb()).c_str());
terminalOK(ctx);
});
ctx.output.printf_P(PSTR("rgb %s\n"),
_lightRgbPayload(_lightToTargetRgb()).c_str());
terminalOK(ctx);
}
terminalRegisterCommand(F("HSV"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() > 1) {
_lightFromHsvPayload(ctx.argv[1].c_str());
lightUpdate();
}
ctx.output.printf_P(PSTR("hsv %s\n"), _lightHsvPayload().c_str());
terminalOK(ctx);
});
alignas(4) static constexpr char LightCommandHsv[] PROGMEM = "HSV";
terminalRegisterCommand(F("KELVIN"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() > 1) {
_lightAdjustKelvin(ctx.argv[1]);
lightUpdate();
}
ctx.output.printf_P(PSTR("kelvin %ld\n"), _toKelvin(_light_mireds));
terminalOK(ctx);
});
static void _lightCommandHsv(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() > 1) {
_lightFromHsvPayload(ctx.argv[1].c_str());
lightUpdate();
}
terminalRegisterCommand(F("MIRED"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() > 1) {
_lightAdjustMireds(ctx.argv[1]);
lightUpdate();
}
ctx.output.printf_P(PSTR("mireds %ld\n"), _light_mireds);
terminalOK(ctx);
});
ctx.output.printf_P(PSTR("hsv %s\n"),
_lightHsvPayload().c_str());
terminalOK(ctx);
}
} // namespace
alignas(4) static constexpr char LightCommandKelvin[] PROGMEM = "KELVIN";
static void _lightCommandKelvin(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() > 1) {
_lightAdjustKelvin(ctx.argv[1]);
lightUpdate();
}
ctx.output.printf_P(PSTR("kelvin %ld\n"),
_toKelvin(_light_mireds));
terminalOK(ctx);
}
alignas(4) static constexpr char LightCommandMired[] PROGMEM = "MIRED";
static void _lightCommandMired(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() > 1) {
_lightAdjustMireds(ctx.argv[1]);
lightUpdate();
}
ctx.output.printf_P(PSTR("mireds %ld\n"), _light_mireds);
terminalOK(ctx);
}
static constexpr ::terminal::Command Commands[] PROGMEM {
{LightCommandNotify, _lightCommandNotify},
{LightCommand, _lightCommand},
{LightCommandBrightness, _lightCommandBrightness},
{LightCommandChannel, _lightCommandChannel},
{LightCommandRgb, _lightCommandRgb},
{LightCommandHsv, _lightCommandHsv},
{LightCommandKelvin, _lightCommandKelvin},
{LightCommandMired, _lightCommandMired},
};
void _lightInitCommands() {
espurna::terminal::add(Commands);
}
} // namespace
#endif // TERMINAL_SUPPORT
size_t lightChannels() {


+ 26
- 10
code/espurna/lightfox.cpp View File

@ -22,10 +22,18 @@ Copyright (C) 2019 by Andrey F. Kupreychik <foxle@quickfox.ru>
static_assert(1 == (RELAY_SUPPORT), "");
static_assert(1 == (BUTTON_SUPPORT), "");
#ifndef LIGHTFOX_BUTTONS
#define LIGHTFOX_BUTTONS 4
#endif
constexpr size_t _lightfoxBuildButtons() {
return LIGHTFOX_BUTTONS;
}
#ifndef LIGHTFOX_RELAYS
#define LIGHTFOX_RELAYS 2
#endif
constexpr size_t _lightfoxBuildRelays() {
return LIGHTFOX_RELAYS;
}
@ -156,20 +164,28 @@ void _lightfoxWebSocketOnAction(uint32_t client_id, const char * action, JsonObj
// -----------------------------------------------------------------------------
#if TERMINAL_SUPPORT
alignas(4) static constexpr char LightfoxCommandLearn[] PROGMEM = "LIGHTFOX.LEARN";
void _lightfoxInitCommands() {
static void _lightfoxCommandLearn(::terminal::CommandContext&& ctx) {
lightfoxLearn();
terminalOK(ctx);
}
terminalRegisterCommand(F("LIGHTFOX.LEARN"), [](::terminal::CommandContext&& ctx) {
lightfoxLearn();
terminalOK(ctx);
});
alignas(4) static constexpr char LightfoxCommandClear[] PROGMEM = "LIGHTFOX.LEARN";
terminalRegisterCommand(F("LIGHTFOX.CLEAR"), [](::terminal::CommandContext&& ctx) {
lightfoxClear();
terminalOK(ctx);
});
static void _lightfoxCommandClear(::terminal::CommandContext&& ctx) {
lightfoxClear();
terminalOK(ctx);
}
static constexpr ::terminal::Command LightfoxCommands[] PROGMEM {
{LightfoxCommandLearn, _lightfoxCommandLearn},
{LightfoxCommandClear, _lightfoxCommandClear},
};
void _lightfoxCommandsSetup() {
espurna::terminal::add(LightfoxCommands);
}
#endif
// -----------------------------------------------------------------------------
@ -209,7 +225,7 @@ void lightfoxSetup() {
#endif
#if TERMINAL_SUPPORT
_lightfoxInitCommands();
_lightfoxCommandsSetup();
#endif
for (size_t relay = 0; relay < _lightfoxBuildRelays(); ++relay) {


+ 37
- 25
code/espurna/mqtt.cpp View File

@ -953,41 +953,53 @@ void _mqttWebSocketOnConnected(JsonObject& root) {
// SETTINGS
// -----------------------------------------------------------------------------
#if TERMINAL_SUPPORT
namespace {
#if TERMINAL_SUPPORT
alignas(4) static constexpr char MqttCommand[] PROGMEM = "MQTT";
void _mqttInitCommands() {
terminalRegisterCommand(F("MQTT"), [](::terminal::CommandContext&& ctx) {
ctx.output.printf_P(PSTR("%s\n"), _mqttBuildInfo());
ctx.output.printf_P(PSTR("client %s\n"), _mqttClientState().c_str());
settingsDump(ctx, mqtt::settings::query::Settings);
terminalOK(ctx);
});
static void _mqttCommand(::terminal::CommandContext&& ctx) {
ctx.output.printf_P(PSTR("%s\n"), _mqttBuildInfo());
ctx.output.printf_P(PSTR("client %s\n"), _mqttClientState().c_str());
settingsDump(ctx, mqtt::settings::query::Settings);
terminalOK(ctx);
}
terminalRegisterCommand(F("MQTT.RESET"), [](::terminal::CommandContext&& ctx) {
_mqttConfigure();
mqttDisconnect();
terminalOK(ctx);
});
alignas(4) static constexpr char MqttCommandReset[] PROGMEM = "MQTT.RESET";
terminalRegisterCommand(F("MQTT.SEND"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 3) {
if (mqttSend(ctx.argv[1].c_str(), ctx.argv[2].c_str(), false, false)) {
terminalOK(ctx);
} else {
terminalError(ctx, F("Cannot queue the message"));
}
return;
static void _mqttCommandReset(::terminal::CommandContext&& ctx) {
_mqttConfigure();
mqttDisconnect();
terminalOK(ctx);
}
alignas(4) static constexpr char MqttCommandSend[] PROGMEM = "MQTT.SEND";
static void _mqttCommandSend(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 3) {
if (mqttSend(ctx.argv[1].c_str(), ctx.argv[2].c_str(), false, false)) {
terminalOK(ctx);
} else {
terminalError(ctx, F("Cannot queue the message"));
}
return;
}
terminalError(ctx, F("MQTT.SEND <topic> <payload>"));
});
terminalError(ctx, F("MQTT.SEND <topic> <payload>"));
}
#endif // TERMINAL_SUPPORT
static constexpr ::terminal::Command MqttCommands[] PROGMEM {
{MqttCommand, _mqttCommand},
{MqttCommandReset, _mqttCommandReset},
{MqttCommandSend, _mqttCommandSend},
};
void _mqttCommandsSetup() {
espurna::terminal::add(MqttCommands);
}
} // namespace
#endif // TERMINAL_SUPPORT
// -----------------------------------------------------------------------------
// MQTT Callbacks
@ -1763,7 +1775,7 @@ void mqttSetup() {
#endif
#if TERMINAL_SUPPORT
_mqttInitCommands();
_mqttCommandsSetup();
#endif
// Main callbacks


+ 15
- 5
code/espurna/network.cpp View File

@ -103,6 +103,8 @@ void start(String hostname, IpFoundCallback callback) {
namespace terminal {
namespace commands {
alignas(4) static constexpr char Host[] PROGMEM = "HOST";
void host(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 2) {
terminalError(ctx, F("HOST <hostname>"));
@ -125,6 +127,8 @@ void host(::terminal::CommandContext&& ctx) {
}
}
alignas(4) static constexpr char Netstat[] PROGMEM = "NETSTAT";
void netstat(::terminal::CommandContext&& ctx) {
const struct tcp_pcb* pcbs[] {
tcp_active_pcbs,
@ -145,6 +149,8 @@ void netstat(::terminal::CommandContext&& ctx) {
}
#if SECURE_CLIENT == SECURE_CLIENT_BEARSSL
alignas(4) static constexpr char MflnProbe[] PROGMEM = "MFLN.PROBE";
void mfln_probe(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 3) {
terminalError(ctx, F("MFLN.PROBE <URL> <SIZE>"));
@ -168,14 +174,18 @@ void mfln_probe(::terminal::CommandContext&& ctx) {
}
#endif
static constexpr ::terminal::Command List[] PROGMEM {
{Host, host},
{Netstat, netstat},
#if SECURE_CLIENT == SECURE_CLIENT_BEARSSL
{MflnProbe, mfln_probe},
#endif
};
} // namespace commands
void setup() {
terminalRegisterCommand(F("NETSTAT"), commands::netstat);
terminalRegisterCommand(F("HOST"), commands::host);
#if SECURE_CLIENT == SECURE_CLIENT_BEARSSL
terminalRegisterCommand(F("MFLN.PROBE"), commands::mfln_probe);
#endif
espurna::terminal::add(commands::List);
}
} // namespace terminal


+ 12
- 6
code/espurna/nofuss.cpp View File

@ -98,14 +98,20 @@ void _nofussLoop() {
}
#if TERMINAL_SUPPORT
alignas(4) static constexpr char NofussCommand[] PROGMEM = "NOFUSS";
void _nofussInitCommands() {
terminalRegisterCommand(F("NOFUSS"), [](::terminal::CommandContext&& ctx) {
terminalOK(ctx);
nofussRun();
});
static void _nofussCommand(::terminal::CommandContext&& ctx) {
terminalOK(ctx);
nofussRun();
}
static constexpr ::terminal::Command NofussCommands[] PROGMEM {
{NofussCommand, _nofussCommand},
};
void _nofussCommandsSetup() {
espurna::terminal::add(NofussCommands);
}
#endif // TERMINAL_SUPPORT
void nofussSetup() {
@ -180,7 +186,7 @@ void nofussSetup() {
#endif
#if TERMINAL_SUPPORT
_nofussInitCommands();
_nofussCommandsSetup();
#endif
// Main callbacks


+ 13
- 3
code/espurna/ntp.cpp View File

@ -488,6 +488,8 @@ void report(Print& out) {
namespace commands {
alignas(4) static constexpr char Ntp[] PROGMEM = "NTP";
void ntp(::terminal::CommandContext&& ctx) {
if (synced()) {
report(ctx.output);
@ -498,6 +500,8 @@ void ntp(::terminal::CommandContext&& ctx) {
terminalError(ctx, F("NTP not synced"));
}
alignas(4) static constexpr char Sync[] PROGMEM = "NTP.SYNC";
void sync(::terminal::CommandContext&& ctx) {
if (synced()) {
sntp_stop();
@ -509,6 +513,8 @@ void sync(::terminal::CommandContext&& ctx) {
terminalError(ctx, F("NTP waiting for initial sync"));
}
alignas(4) static constexpr char Set[] PROGMEM = "NTP.SET";
[[gnu::unused]]
void set_simple(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 2) {
@ -561,12 +567,16 @@ void set_time(::terminal::CommandContext&& ctx) {
#endif
}
static constexpr ::terminal::Command List[] PROGMEM {
{Ntp, ntp},
{Sync, sync},
{Set, set_time},
};
} // namespace commands
void setup() {
terminalRegisterCommand(F("NTP"), commands::ntp);
terminalRegisterCommand(F("NTP.SYNC"), commands::sync);
terminalRegisterCommand(F("NTP.SET"), commands::set_time);
espurna::terminal::add(commands::List);
}
} // namespace terminal


+ 16
- 10
code/espurna/ota_asynctcp.cpp View File

@ -240,19 +240,25 @@ void clientFromUrl(const String& string) {
}
#if TERMINAL_SUPPORT
alignas(4) static constexpr char OtaCommand[] PROGMEM = "OTA";
void terminalCommands() {
terminalRegisterCommand(F("OTA"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 2) {
clientFromUrl(ctx.argv[1]);
terminalOK(ctx);
return;
}
static void otaCommand(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 2) {
terminalError(ctx, F("OTA <URL>"));
return;
}
terminalError(ctx, F("OTA <url>"));
});
clientFromUrl(ctx.argv[1]);
terminalOK(ctx);
}
static constexpr ::terminal::Command OtaCommands[] PROGMEM {
{OtaCommand, otaCommand},
};
void terminalSetup() {
espurna::terminal::add(OtaCommands);
}
#endif // TERMINAL_SUPPORT
#if OTA_MQTT_SUPPORT
@ -287,7 +293,7 @@ void otaClientSetup() {
moveSetting("otafp", "otaFP");
#if TERMINAL_SUPPORT
ota::asynctcp::terminalCommands();
ota::asynctcp::terminalSetup();
#endif
#if (MQTT_SUPPORT && OTA_MQTT_SUPPORT)


+ 16
- 10
code/espurna/ota_httpupdate.cpp View File

@ -142,19 +142,25 @@ void clientFromUrl(const String& url) {
}
#if TERMINAL_SUPPORT
alignas(4) static constexpr char OtaCommand[] PROGMEM = "OTA";
void terminalCommands() {
terminalRegisterCommand(F("OTA"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 2) {
clientFromUrl(ctx.argv[1]);
terminalOK(ctx);
return;
}
static void otaCommand(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 2) {
terminalError(ctx, F("OTA <URL>"));
return;
}
terminalError(ctx, F("OTA <url>"));
});
clientFromUrl(ctx.argv[1]);
terminalOK(ctx);
}
static constexpr ::terminal::Command OtaCommands[] PROGMEM {
{OtaCommand, otaCommand},
};
void terminalSetup() {
espurna::terminal::add(OtaCommands);
}
#endif // TERMINAL_SUPPORT
#if (MQTT_SUPPORT && OTA_MQTT_SUPPORT)
@ -195,7 +201,7 @@ void otaClientSetup() {
moveSetting("otafp", "otaFP");
#if TERMINAL_SUPPORT
ota::httpupdate::terminalCommands();
ota::httpupdate::terminalSetup();
#endif
#if (MQTT_SUPPORT && OTA_MQTT_SUPPORT)


+ 27
- 19
code/espurna/pwm.cpp View File

@ -437,30 +437,38 @@ using namespace generic;
#if TERMINAL_SUPPORT
namespace terminal {
void setup() {
terminalRegisterCommand(F("PWM.WRITE"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 3) {
const auto convert_channel = espurna::settings::internal::convert<uint32_t>;
const auto channel = convert_channel(ctx.argv[1]);
if (channel >= channels()) {
terminalError(ctx, F("Invalid channel ID"));
return;
}
alignas(4) static constexpr char PwmWrite[] PROGMEM = "PWM.WRITE";
void pwm_write(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 3) {
const auto convert_channel = espurna::settings::internal::convert<uint32_t>;
const auto channel = convert_channel(ctx.argv[1]);
if (channel >= channels()) {
terminalError(ctx, F("Invalid channel ID"));
return;
}
const auto convert_duty = espurna::settings::internal::convert<float>;
const auto value = std::clamp(convert_duty(ctx.argv[2]), 0.f, 100.f);
ctx.output.printf("PWM channel %u duty %s\n",
const auto convert_duty = espurna::settings::internal::convert<float>;
const auto value = std::clamp(convert_duty(ctx.argv[2]), 0.f, 100.f);
ctx.output.printf("PWM channel %u duty %s\n",
channel, String(value, 3).c_str());
duty(channel, value);
update();
duty(channel, value);
update();
terminalOK(ctx);
return;
}
terminalOK(ctx);
return;
}
terminalError(ctx, F("PWM.WRITE <CHANNEL> <DUTY>"));
}
terminalError(ctx, F("PWM.WRITE <CHANNEL> <DUTY>"));
});
static constexpr ::terminal::Command Commands[] PROGMEM {
{PwmWrite, pwm_write},
};
void setup() {
espurna::terminal::add(Commands);
}
} // namespace terminal


+ 55
- 44
code/espurna/relay.cpp View File

@ -2637,61 +2637,72 @@ void _relayPrint(Print& out, size_t start, size_t stop) {
}
}
void _relayInitCommands() {
terminalRegisterCommand(F("RELAY"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 1) {
_relayPrint(ctx.output, 0, _relays.size());
terminalOK(ctx);
return;
}
alignas(4) static constexpr char RelayCommand[] PROGMEM = "RELAY";
size_t id;
if (!_relayTryParseId(ctx.argv[1].c_str(), id)) {
terminalError(ctx, F("Invalid relayID"));
return;
}
static void _relayCommand(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 1) {
_relayPrint(ctx.output, 0, _relays.size());
terminalOK(ctx);
return;
}
if (ctx.argv.size() > 2) {
auto status = relayParsePayload(ctx.argv[2].c_str());
if (PayloadStatus::Unknown == status) {
terminalError(ctx, F("Invalid status"));
return;
}
size_t id;
if (!_relayTryParseId(ctx.argv[1].c_str(), id)) {
terminalError(ctx, F("Invalid relayID"));
return;
}
_relayHandleStatus(id, status);
_relayPrint(ctx.output, _relays[id], id);
terminalOK(ctx);
if (ctx.argv.size() > 2) {
auto status = relayParsePayload(ctx.argv[2].c_str());
if (PayloadStatus::Unknown == status) {
terminalError(ctx, F("Invalid status"));
return;
}
settingsDump(ctx, espurna::relay::settings::query::IndexedSettings, id);
_relayHandleStatus(id, status);
_relayPrint(ctx.output, _relays[id], id);
terminalOK(ctx);
});
return;
}
terminalRegisterCommand(F("PULSE"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() < 3) {
terminalError(ctx, F("PULSE <ID> <TIME> [<NORMAL STATUS>]"));
return;
}
settingsDump(ctx, espurna::relay::settings::query::IndexedSettings, id);
terminalOK(ctx);
}
size_t id;
if (!_relayTryParseId(ctx.argv[1].c_str(), id)) {
terminalError(ctx, F("Invalid relayID"));
return;
}
alignas(4) static constexpr char PulseCommand[] PROGMEM = "PULSE";
if ((ctx.argv.size() == 4) && !_relayHandlePayload(id, ctx.argv[3])) {
terminalError(ctx, F("Invalid relay status"));
return;
}
static void _relayCommandPulse(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() < 3) {
terminalError(ctx, F("PULSE <ID> <TIME> [<NORMAL STATUS>]"));
return;
}
if (!_relayHandlePulsePayload(id, ctx.argv[2])) {
terminalError(ctx, F("Normal state conflict"));
return;
}
size_t id;
if (!_relayTryParseId(ctx.argv[1].c_str(), id)) {
terminalError(ctx, F("Invalid relayID"));
return;
}
terminalOK(ctx);
});
if ((ctx.argv.size() == 4) && !_relayHandlePayload(id, ctx.argv[3])) {
terminalError(ctx, F("Invalid relay status"));
return;
}
if (!_relayHandlePulsePayload(id, ctx.argv[2])) {
terminalError(ctx, F("Normal state conflict"));
return;
}
terminalOK(ctx);
}
static constexpr ::terminal::Command RelayCommands[] PROGMEM {
{RelayCommand, _relayCommand},
{PulseCommand, _relayCommandPulse},
};
void _relayCommandsSetup() {
espurna::terminal::add(RelayCommands);
}
} // namespace
@ -2983,7 +2994,7 @@ void relaySetup() {
relaySetupMQTT();
#endif
#if TERMINAL_SUPPORT
_relayInitCommands();
_relayCommandsSetup();
#endif
// Main callbacks


+ 66
- 47
code/espurna/rfbridge.cpp View File

@ -1163,68 +1163,87 @@ void _rfbCommandStatusDispatch(::terminal::CommandContext&& ctx, size_t id, Rela
}
}
void _rfbInitCommands() {
alignas(4) static constexpr char RfbCommandSend[] PROGMEM = "RFB.SEND";
terminalRegisterCommand(F("RFB.SEND"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 2) {
rfbSend(ctx.argv[1]);
return;
}
static void _rfbCommandSend(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 2) {
rfbSend(ctx.argv[1]);
return;
}
terminalError(ctx, F("RFB.SEND <CODE>"));
});
terminalError(ctx, F("RFB.SEND <CODE>"));
}
#if RELAY_SUPPORT
terminalRegisterCommand(F("RFB.LEARN"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 3) {
terminalError(ctx, F("RFB.LEARN <ID> <STATUS>"));
return;
}
alignas(4) static constexpr char RfbCommandLearn[] PROGMEM = "RFB.LEARN";
size_t id;
if (!tryParseId(ctx.argv[1].c_str(), relayCount, id)) {
terminalError(ctx, F("Invalid relay ID"));
return;
}
static void _rfbCommandLearn(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 3) {
terminalError(ctx, F("RFB.LEARN <ID> <STATUS>"));
return;
}
_rfbCommandStatusDispatch(std::move(ctx), id, rfbLearn);
});
size_t id;
if (!tryParseId(ctx.argv[1].c_str(), relayCount, id)) {
terminalError(ctx, F("Invalid relay ID"));
return;
}
terminalRegisterCommand(F("RFB.FORGET"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() < 2) {
terminalError(ctx, F("RFB.FORGET <ID> [<STATUS>]"));
return;
}
_rfbCommandStatusDispatch(std::move(ctx), id, rfbLearn);
}
size_t id;
if (!tryParseId(ctx.argv[1].c_str(), relayCount, id)) {
terminalError(ctx, F("Invalid relay ID"));
return;
}
alignas(4) static constexpr char RfbCommandForget[] PROGMEM = "RFB.FORGET";
if (ctx.argv.size() == 3) {
_rfbCommandStatusDispatch(std::move(ctx), id, rfbForget);
return;
}
static void _rfbCommandForget(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() < 2) {
terminalError(ctx, F("RFB.FORGET <ID> [<STATUS>]"));
return;
}
rfbForget(id, true);
rfbForget(id, false);
size_t id;
if (!tryParseId(ctx.argv[1].c_str(), relayCount, id)) {
terminalError(ctx, F("Invalid relay ID"));
return;
}
terminalOK(ctx);
});
if (ctx.argv.size() == 3) {
_rfbCommandStatusDispatch(std::move(ctx), id, rfbForget);
return;
}
rfbForget(id, true);
rfbForget(id, false);
terminalOK(ctx);
}
#endif // if RELAY_SUPPORT
#if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
terminalRegisterCommand(F("RFB.WRITE"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 2) {
terminalError(ctx, F("RFB.WRITE <PAYLOAD>"));
return;
}
_rfbSendRawFromPayload(ctx.argv[1].c_str());
terminalOK(ctx);
});
alignas(4) static constexpr char RfbCommandWrite[] PROGMEM = "RFB.WRITE";
static void _rfbCommandWrite(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 2) {
terminalError(ctx, F("RFB.WRITE <PAYLOAD>"));
return;
}
_rfbSendRawFromPayload(ctx.argv[1].c_str());
terminalOK(ctx);
});
#endif
static constexpr ::terminal::Command RfbCommands[] PROGMEM {
{RfbCommandSend, _rfbCommandSend},
#if RELAY_SUPPORT
{RfbCommandLearn, _rfbCommandLearn},
{RfbCommandForget, _rfbCommandForget},
#endif
#if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
{RfbCommandWrite, _rfbCommandWrite},
#endif
};
void _rfbCommandsSetup() {
espurna::terminal::add(RfbCommands);
}
#endif // TERMINAL_SUPPORT
@ -1390,7 +1409,7 @@ void rfbSetup() {
#endif
#if TERMINAL_SUPPORT
_rfbInitCommands();
_rfbCommandsSetup();
#endif
_rfb_repeats = getSetting("rfbRepeat", RFB_SEND_REPEATS);


+ 92
- 54
code/espurna/rpnrules.cpp View File

@ -287,65 +287,95 @@ void showStack(Print& output) {
output.print(F(" (empty)\n"));
}
void setup() {
terminalRegisterCommand(F("RPN.RUNNERS"), [](::terminal::CommandContext&& ctx) {
if (internal::runners.empty()) {
terminalError(ctx, F("No active runners"));
return;
}
alignas(4) static constexpr char Runners[] PROGMEM = "RPN.RUNNERS";
for (auto& runner : internal::runners) {
char buffer[128] = {0};
snprintf_P(buffer, sizeof(buffer), PSTR("%p %s %u ms, last %u ms\n"),
&runner, (Runner::Policy::Periodic == runner.policy()) ? "every" : "one-shot",
runner.period(), runner.last());
ctx.output.print(buffer);
}
void runners(::terminal::CommandContext&& ctx) {
if (internal::runners.empty()) {
terminalError(ctx, F("No active runners"));
return;
}
terminalOK(ctx);
});
for (auto& runner : internal::runners) {
char buffer[128] = {0};
snprintf_P(buffer, sizeof(buffer),
PSTR("%p %s %u ms, last %u ms\n"),
&runner,
(Runner::Policy::Periodic == runner.policy())
? "every"
: "one-shot",
runner.period(), runner.last());
ctx.output.print(buffer);
}
terminalOK(ctx);
}
alignas(4) static constexpr char Variables[] PROGMEM = "RPN.VARS";
terminalRegisterCommand(F("RPN.VARS"), [](::terminal::CommandContext&& ctx) {
rpn_variables_foreach(internal::context, [&ctx](const String& name, const rpn_value& value) {
void variables(::terminal::CommandContext&& ctx) {
rpn_variables_foreach(internal::context,
[&ctx](const String& name, const rpn_value& value) {
char buffer[256] = {0};
snprintf_P(buffer, sizeof(buffer), PSTR(" %s: %s\n"), name.c_str(), valueToString(value).c_str());
snprintf_P(buffer, sizeof(buffer),
PSTR(" %s: %s\n"),
name.c_str(), valueToString(value).c_str());
ctx.output.print(buffer);
});
terminalOK(ctx);
});
terminalOK(ctx);
}
terminalRegisterCommand(F("RPN.OPS"), [](::terminal::CommandContext&& ctx) {
rpn_operators_foreach(internal::context, [&ctx](const String& name, size_t argc, rpn_operator::callback_type) {
alignas(4) static constexpr char Operators[] PROGMEM = "RPN.OPS";
void operators(::terminal::CommandContext&& ctx) {
rpn_operators_foreach(internal::context,
[&ctx](const String& name, size_t argc, rpn_operator::callback_type) {
char buffer[128] = {0};
snprintf_P(buffer, sizeof(buffer), PSTR(" %s (%d)\n"), name.c_str(), argc);
snprintf_P(buffer, sizeof(buffer),
PSTR(" %s (%d)\n"),
name.c_str(), argc);
ctx.output.print(buffer);
});
terminalOK(ctx);
});
terminalOK(ctx);
}
terminalRegisterCommand(F("RPN.TEST"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 2) {
terminalError(ctx, F("Wrong arguments"));
return;
}
alignas(4) static constexpr char Test[] PROGMEM = "RPN.TEST";
const char* ptr = ctx.argv[1].c_str();
ctx.output.printf_P(PSTR("Expression: \"%s\"\n"), ctx.argv[1].c_str());
void test(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 2) {
terminalError(ctx, F("Wrong arguments"));
return;
}
if (!rpn_process(internal::context, ptr)) {
rpn_stack_clear(internal::context);
char buffer[64] = {0};
snprintf_P(buffer, sizeof(buffer), PSTR("at %u (category %d code %d)"),
internal::context.error.position, static_cast<int>(internal::context.error.category), internal::context.error.code);
terminalError(ctx, buffer);
return;
}
const char* ptr = ctx.argv[1].c_str();
ctx.output.printf_P(PSTR("Expression: \"%s\"\n"), ctx.argv[1].c_str());
showStack(ctx.output);
if (!rpn_process(internal::context, ptr)) {
rpn_stack_clear(internal::context);
char buffer[64] = {0};
snprintf_P(buffer, sizeof(buffer),
PSTR("at %u (category %d code %d)"),
internal::context.error.position,
static_cast<int>(internal::context.error.category),
internal::context.error.code);
terminalError(ctx, buffer);
return;
}
terminalOK(ctx);
});
showStack(ctx.output);
rpn_stack_clear(internal::context);
terminalOK(ctx);
}
static constexpr ::terminal::Command Commands[] PROGMEM {
{Runners, runners},
{Variables, variables},
{Operators, operators},
{Test, test},
};
void setup() {
espurna::terminal::add(Commands);
}
} // namespace terminal
@ -975,6 +1005,24 @@ void codeHandler(unsigned char protocol, const char* raw_code) {
schedule();
}
alignas(4) static constexpr char RfbCodes[] PROGMEM = "RFB.CODES";
void rfb_codes(::terminal::CommandContext&& ctx) {
for (auto& code : internal::codes) {
char buffer[128] = {0};
snprintf_P(buffer, sizeof(buffer),
PSTR("proto=%u raw=\"%s\" count=%u last=%u\n"),
code.protocol, code.raw.c_str(), code.count, code.last);
ctx.output.print(buffer);
}
terminalOK(ctx);
}
static ::terminal::Command RfbCommands[] PROGMEM {
{RfbCodes, rfb_codes},
};
void init(rpn_context& context) {
// - Repeat window is an arbitrary time, just about 3-4 more times it takes for
// a code to be sent again when holding a generic remote button
@ -986,17 +1034,7 @@ void init(rpn_context& context) {
internal::stale_delay = settings::staleDelay();
#if TERMINAL_SUPPORT
terminalRegisterCommand(F("RFB.CODES"), [](::terminal::CommandContext&& ctx) {
for (auto& code : internal::codes) {
char buffer[128] = {0};
snprintf_P(buffer, sizeof(buffer),
PSTR("proto=%u raw=\"%s\" count=%u last=%u\n"),
code.protocol, code.raw.c_str(), code.count, code.last);
ctx.output.print(buffer);
}
terminalOK(ctx);
});
espurna::terminal::add(RfbCommands);
#endif
// Main bulk of the processing goes on in here


+ 88
- 51
code/espurna/rtcmem.cpp View File

@ -9,28 +9,43 @@ Copyright (C) 2019 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
#include "espurna.h"
#include "rtcmem.h"
volatile RtcmemData* Rtcmem = reinterpret_cast<volatile RtcmemData*>(RTCMEM_ADDR);
static constexpr uint32_t RtcmemMagic { RTCMEM_MAGIC };
static constexpr uintptr_t RtcmemBlocks { RTCMEM_BLOCKS };
static constexpr uintptr_t RtcmemBegin { RTCMEM_ADDR };
static constexpr uintptr_t RtcmemEnd { RtcmemBegin + RtcmemBlocks };
volatile RtcmemData* Rtcmem = reinterpret_cast<volatile RtcmemData*>(RtcmemBegin);
namespace espurna {
namespace peripherals {
namespace rtc {
namespace {
bool _rtcmem_status = false;
namespace internal {
bool status = false;
} // namespace internal
void erase() {
DEBUG_MSG_P(PSTR("[RTCMEM] Erasing start=0x%08x end=0x%08x\n"),
RtcmemBegin, RtcmemEnd);
void _rtcmemErase() {
auto ptr = reinterpret_cast<volatile uint32_t*>(RTCMEM_ADDR);
const auto end = ptr + RTCMEM_BLOCKS;
DEBUG_MSG_P(PSTR("[RTCMEM] Erasing start=%p end=%p\n"), ptr, end);
do {
*ptr = 0;
} while (++ptr != end);
auto begin = reinterpret_cast<volatile uint32_t*>(RtcmemBegin);
auto end = reinterpret_cast<volatile uint32_t*>(RtcmemEnd);
for (auto it = begin; it != end; ++it) {
*it = 0;
}
}
void _rtcmemInit() {
_rtcmemErase();
Rtcmem->magic = RTCMEM_MAGIC;
void init() {
erase();
Rtcmem->magic = RtcmemMagic;
}
// Treat memory as dirty on cold boot, hardware wdt reset and rst pin
bool _rtcmemStatus() {
bool status() {
bool readable;
switch (systemResetReason()) {
@ -42,70 +57,92 @@ bool _rtcmemStatus() {
readable = true;
}
readable = readable and (RTCMEM_MAGIC == Rtcmem->magic);
readable = readable
&& (RtcmemMagic == Rtcmem->magic);
return readable;
}
#if TERMINAL_SUPPORT
namespace terminal {
void _rtcmemInitCommands() {
terminalRegisterCommand(F("RTCMEM.REINIT"), [](::terminal::CommandContext&&) {
_rtcmemInit();
});
alignas(4) static constexpr char Init[] PROGMEM = "RTCMEM.INIT";
#if DEBUG_SUPPORT
terminalRegisterCommand(F("RTCMEM.DUMP"), [](::terminal::CommandContext&&) {
void init(::terminal::CommandContext&& ctx) {
rtc::init();
terminalOK(ctx);
}
DEBUG_MSG_P(PSTR("[RTCMEM] boot_status=%u status=%u blocks_used=%u\n"),
_rtcmem_status, _rtcmemStatus(), RtcmemSize);
alignas(4) static constexpr char Dump[] PROGMEM = "RTCMEM.DUMP";
String line;
line.reserve(96);
char buffer[16] = {0};
void dump(::terminal::CommandContext&& ctx) {
ctx.output.printf_P(PSTR("boot_status=%s status=%s capacity=%u\n"),
internal::status ? "OK" : "INIT",
status() ? "OK" : "INIT",
RtcmemSize);
auto addr = reinterpret_cast<volatile uint32_t*>(RTCMEM_ADDR);
constexpr size_t Blocks = 8;
constexpr auto Increment = alignof(uint32_t) * Blocks;
uint8_t block = 1;
uint8_t offset = 0;
uint8_t start = 0;
alignas(4) uint8_t buffer[Blocks];
String line;
for (auto addr = RtcmemBegin; addr < RtcmemEnd; addr += Increment) {
std::memcpy(buffer, reinterpret_cast<uint32_t*>(addr), 1);
do {
line += PSTR("0x");
line += String(addr, 16);
line += ':';
offset = block - 1;
for (auto offset = &buffer[0]; offset < &buffer[Blocks]; offset += 4) {
line += PSTR(" ");
line += hexEncode(offset, offset + 4);
}
snprintf(buffer, sizeof(buffer), "%08x ", *(addr + offset));
line += buffer;
line += '\n';
if ((block % 8) == 0) {
DEBUG_MSG_P(PSTR("%02u %p: %s\n"), start, addr+start, line.c_str());
start = block;
line = "";
}
ctx.output.print(line);
line = "";
}
}
++block;
static constexpr ::terminal::Command Commands[] PROGMEM {
{Init, init},
{Dump, dump},
};
} while (block<(RTCMEM_BLOCKS+1));
void setup() {
espurna::terminal::add(Commands);
}
});
#endif
} // namespace terminal
#endif
bool current_status() {
return internal::status;
}
void setup() {
#if TERMINAL_SUPPORT
terminal::setup();
#endif
internal::status = status();
if (!internal::status) {
init();
}
}
} // namespace
} // namespace rtc
} // namespace peripherals
} // namespace espurna
bool rtcmemStatus() {
return _rtcmem_status;
return espurna::peripherals::rtc::current_status();
}
void rtcmemSetup() {
_rtcmem_status = _rtcmemStatus();
if (!_rtcmem_status) {
_rtcmemInit();
}
#if TERMINAL_SUPPORT
_rtcmemInitCommands();
#endif
espurna::peripherals::rtc::setup();
}

+ 85
- 64
code/espurna/sensor.cpp View File

@ -3649,88 +3649,109 @@ void setup() {
namespace terminal {
namespace {
void setup() {
terminalRegisterCommand(F("MAGNITUDES"), [](::terminal::CommandContext&& ctx) {
if (!magnitude::count()) {
terminalError(ctx, F("No magnitudes"));
return;
}
namespace commands {
size_t index = 0;
for (const auto& magnitude : magnitude::internal::magnitudes) {
ctx.output.printf_P(PSTR("%2zu * %s @ %s (read:%s reported:%s units:%s)\n"),
index++, magnitude::topicWithIndex(magnitude).c_str(),
magnitude::description(magnitude).c_str(),
magnitude::format(magnitude, magnitude.last).c_str(),
magnitude::format(magnitude, magnitude.reported).c_str(),
magnitude::units(magnitude).c_str());
}
alignas(4) static constexpr char Magnitudes[] PROGMEM = "MAGNITUDES";
terminalOK(ctx);
});
void magnitudes(::terminal::CommandContext&& ctx) {
if (!magnitude::count()) {
terminalError(ctx, F("No magnitudes"));
return;
}
terminalRegisterCommand(F("EXPECTED"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 3) {
const auto id = espurna::settings::internal::convert<size_t>(ctx.argv[1]);
if (id < magnitude::count()) {
const auto& magnitude = magnitude::get(id);
size_t index = 0;
for (const auto& magnitude : magnitude::internal::magnitudes) {
ctx.output.printf_P(PSTR("%2zu * %s @ %s (read:%s reported:%s units:%s)\n"),
index++, magnitude::topicWithIndex(magnitude).c_str(),
magnitude::description(magnitude).c_str(),
magnitude::format(magnitude, magnitude.last).c_str(),
magnitude::format(magnitude, magnitude.reported).c_str(),
magnitude::units(magnitude).c_str());
}
const auto result = energy::ratioFromValue(
magnitude, espurna::settings::internal::convert<double>(ctx.argv[2]));
const auto key = settings::keys::get(
magnitude, settings::suffix::Ratio);
ctx.output.printf("%s => %s\n", key.c_str(), String(result).c_str());
terminalOK(ctx);
return;
}
terminalOK(ctx);
}
terminalError(ctx, F("Invalid magnitude ID"));
alignas(4) static constexpr char Expected[] PROGMEM = "EXPECTED";
void expected(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 3) {
const auto id = espurna::settings::internal::convert<size_t>(ctx.argv[1]);
if (id < magnitude::count()) {
const auto& magnitude = magnitude::get(id);
const auto result = energy::ratioFromValue(
magnitude, espurna::settings::internal::convert<double>(ctx.argv[2]));
const auto key = settings::keys::get(
magnitude, settings::suffix::Ratio);
ctx.output.printf("%s => %s\n", key.c_str(), String(result).c_str());
terminalOK(ctx);
return;
}
terminalError(ctx, F("EXPECTED <ID> <VALUE>"));
});
terminalError(ctx, F("Invalid magnitude ID"));
return;
}
terminalRegisterCommand(F("RESET.RATIOS"), [](::terminal::CommandContext&& ctx) {
energy::reset();
terminalOK(ctx);
});
terminalError(ctx, F("EXPECTED <ID> <VALUE>"));
}
terminalRegisterCommand(F("ENERGY"), [](::terminal::CommandContext&& ctx) {
using IndexType = decltype(Magnitude::index_global);
alignas(4) static constexpr char ResetRatios[] PROGMEM = "RESET.RATIOS";
if (ctx.argv.size() < 2) {
terminalError(ctx, F("ENERGY <ID> [<VALUE>]"));
return;
}
void reset_ratios(::terminal::CommandContext&& ctx) {
energy::reset();
terminalOK(ctx);
}
const auto index = espurna::settings::internal::convert<IndexType>(ctx.argv[1]);
alignas(4) static constexpr char Energy[] PROGMEM = "ENERGY";
const auto* magnitude = magnitude::find(MAGNITUDE_ENERGY, index);
if (!magnitude) {
terminalError(ctx, F("Invalid magnitude ID"));
return;
}
void energy(::terminal::CommandContext&& ctx) {
using IndexType = decltype(Magnitude::index_global);
if (ctx.argv.size() == 2) {
ctx.output.printf_P(PSTR("%s => %s (%s)\n"),
magnitude::topicWithIndex(*magnitude).c_str(),
magnitude::format(*magnitude, magnitude->reported).c_str(),
magnitude::units(*magnitude).c_str());
terminalOK(ctx);
if (ctx.argv.size() < 2) {
terminalError(ctx, F("ENERGY <ID> [<VALUE>]"));
return;
}
const auto index = espurna::settings::internal::convert<IndexType>(ctx.argv[1]);
const auto* magnitude = magnitude::find(MAGNITUDE_ENERGY, index);
if (!magnitude) {
terminalError(ctx, F("Invalid magnitude ID"));
return;
}
if (ctx.argv.size() == 2) {
ctx.output.printf_P(PSTR("%s => %s (%s)\n"),
magnitude::topicWithIndex(*magnitude).c_str(),
magnitude::format(*magnitude, magnitude->reported).c_str(),
magnitude::units(*magnitude).c_str());
terminalOK(ctx);
return;
}
if (ctx.argv.size() == 3) {
const auto energy = energy::convert(ctx.argv[2]);
if (!energy) {
terminalError(ctx, F("Invalid energy string"));
return;
}
if (ctx.argv.size() == 3) {
const auto energy = energy::convert(ctx.argv[2]);
if (!energy) {
terminalError(ctx, F("Invalid energy string"));
return;
}
energy::set(*magnitude, energy.value());
}
}
energy::set(*magnitude, energy.value());
}
});
static constexpr ::terminal::Command List[] PROGMEM {
{Magnitudes, commands::magnitudes},
{Expected, commands::expected},
{ResetRatios, commands::reset_ratios},
{Energy, commands::energy},
};
} // namespace commands
void setup() {
espurna::terminal::add(commands::List);
}
} // namespace


+ 86
- 69
code/espurna/sensors/PZEM004TSensor.h View File

@ -475,94 +475,111 @@ private:
constexpr BaseEmonSensor::Magnitude PZEM004TSensor::Magnitudes[];
#endif
void PZEM004TSensor::registerTerminalCommands() {
#if TERMINAL_SUPPORT
terminalRegisterCommand(F("PZ.DEVICES"), [](::terminal::CommandContext&& ctx) {
foreach([&](const PZEM004TSensor& device) {
ctx.output.printf("%s\n", device._address.toString().c_str());
});
terminalOK(ctx);
});
terminalRegisterCommand(F("PZ.PORTS"), [](::terminal::CommandContext&& ctx) {
auto it = _ports.begin();
auto end = _ports.end();
alignas(4) static constexpr char PzemDevices[] PROGMEM = "PZ.DEVICES";
if (ctx.argv.size() == 2) {
auto offset = espurna::settings::internal::convert<size_t>(ctx.argv[1]);
if (offset >= _ports.size()) {
terminalError(ctx, F("Invalid port ID"));
return;
}
void pzem_devices(::terminal::CommandContext&& ctx) {
foreach([&](const PZEM004TSensor& device) {
ctx.output.printf("%s\n", device._address.toString().c_str());
});
terminalOK(ctx);
}
while ((it != end) && offset) {
++it;
--offset;
}
alignas(4) static constexpr char PzemPorts[] PROGMEM = "PZ.PORTS";
if (it == end) {
terminalError(ctx, F("Invalid port ID"));
return;
}
void pzem_ports(::terminal::CommandContext&& ctx) {
auto it = _ports.begin();
auto end = _ports.end();
end = it + 1;
if (ctx.argv.size() == 2) {
auto offset = espurna::settings::internal::convert<size_t>(ctx.argv[1]);
if (offset >= _ports.size()) {
terminalError(ctx, F("Invalid port ID"));
return;
}
auto print = [&](const size_t index, const PortWeakPtr& ptr) {
auto port = ptr.lock();
if (port) {
ctx.output.printf_P(PSTR("%u -> %sSerial (%hhu,%hhu)\n"),
index, port->tag(), port->rx(), port->tx());
} else {
ctx.output.print(F("%u -> (not configured)\n"));
}
};
size_t index { 0 };
while ((it != end) && (*it).use_count()) {
print(index, *it);
while ((it != end) && offset) {
++it;
++index;
}
terminalOK(ctx);
});
// Set the *currently connected* device address
// (ref. comment at the top, shouldn't do this when multiple devices are connected)
terminalRegisterCommand(F("PZ.ADDRESS"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 3) {
terminalError(ctx, F("PZ.ADDRESS <PORT> <ADDRESS>"));
return;
--offset;
}
auto id = espurna::settings::internal::convert<size_t>(ctx.argv[1]);
if (id >= _ports.size()) {
if (it == end) {
terminalError(ctx, F("Invalid port ID"));
return;
}
auto port = _ports[id].lock();
if (!port) {
terminalError(ctx, F("Port not configured"));
return;
end = it + 1;
}
auto print = [&](const size_t index, const PortWeakPtr& ptr) {
auto port = ptr.lock();
if (port) {
ctx.output.printf_P(PSTR("%u -> %sSerial (%hhu,%hhu)\n"),
index, port->tag(), port->rx(), port->tx());
} else {
ctx.output.print(F("%u -> (not configured)\n"));
}
};
IPAddress addr;
addr.fromString(ctx.argv[2]);
size_t index { 0 };
while ((it != end) && (*it).use_count()) {
print(index, *it);
++it;
++index;
}
if (!addr.isSet()) {
terminalError(ctx, F("Invalid address"));
return;
}
terminalOK(ctx);
}
if (!port->address(addr)) {
terminalError(ctx, F("Failed to set the address"));
return;
}
alignas(4) static constexpr char PzemAddress[] PROGMEM = "PZ.ADDRESS";
terminalOK(ctx);
});
// Set the *currently connected* device address
// (ref. comment at the top, shouldn't do this when multiple devices are connected)
static void pzem_address(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 3) {
terminalError(ctx, F("PZ.ADDRESS <PORT> <ADDRESS>"));
return;
}
auto id = espurna::settings::internal::convert<size_t>(ctx.argv[1]);
if (id >= _ports.size()) {
terminalError(ctx, F("Invalid port ID"));
return;
}
auto port = _ports[id].lock();
if (!port) {
terminalError(ctx, F("Port not configured"));
return;
}
IPAddress addr;
addr.fromString(ctx.argv[2]);
if (!addr.isSet()) {
terminalError(ctx, F("Invalid address"));
return;
}
if (!port->address(addr)) {
terminalError(ctx, F("Failed to set the address"));
return;
}
terminalOK(ctx);
}
static constexpr ::terminal::Command PzemCommands[] PROGMEM {
{PzemDevices, pzem_devices},
{PzemPorts, pzem_ports},
{PzemAddress, pzem_address},
};
#endif
void PZEM004TSensor::registerTerminalCommands() {
#if TERMINAL_SUPPORT
espurna::terminal::add(PzemCommands);
#endif
}


+ 25
- 17
code/espurna/sensors/PZEM004TV30Sensor.h View File

@ -709,26 +709,34 @@ constexpr espurna::duration::Milliseconds PZEM004TV30Sensor::DefaultUpdateInterv
PZEM004TV30Sensor::Instance PZEM004TV30Sensor::_instance{};
void PZEM004TV30Sensor::registerTerminalCommands() {
#if TERMINAL_SUPPORT
terminalRegisterCommand(F("PZ.ADDRESS"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 2) {
terminalError(ctx, F("PZ.ADDRESS <ADDRESS>"));
return;
}
alignas(4) static constexpr char PzemV3Address[] PROGMEM = "PZ.ADDRESS";
uint8_t updated = espurna::settings::internal::convert<uint8_t>(ctx.argv[1]);
static void pzemv3_address(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 2) {
terminalError(ctx, F("PZ.ADDRESS <ADDRESS>"));
return;
}
_instance->flush();
if (_instance->modbusChangeAddress(updated)) {
_instance->_address = updated;
setSetting("pzemv30Addr", updated);
terminalOK(ctx);
return;
}
uint8_t updated = espurna::settings::internal::convert<uint8_t>(ctx.argv[1]);
terminalError(ctx, F("Could not change the address"));
});
_instance->flush();
if (_instance->modbusChangeAddress(updated)) {
_instance->_address = updated;
setSetting("pzemv30Addr", updated);
terminalOK(ctx);
return;
}
terminalError(ctx, F("Could not change the address"));
}
static constexpr ::terminal::Command PzemV3Commands[] PROGMEM {
{PzemV3Address, pzemv3_address},
};
void PZEM004TV30Sensor::registerTerminalCommands() {
#if TERMINAL_SUPPORT
espurna::terminal::add(PzemV3Commands);
#endif
}


+ 35
- 12
code/espurna/settings.cpp View File

@ -300,6 +300,8 @@ void dump(const ::terminal::CommandContext& ctx, const query::IndexedSetting* be
namespace commands {
alignas(4) static constexpr char Config[] PROGMEM = "CONFIG";
void config(::terminal::CommandContext&& ctx) {
DynamicJsonBuffer jsonBuffer(1024);
JsonObject& root = jsonBuffer.createObject();
@ -308,6 +310,8 @@ void config(::terminal::CommandContext&& ctx) {
terminalOK(ctx);
}
alignas(4) static constexpr char Keys[] PROGMEM = "KEYS";
void keys(::terminal::CommandContext&& ctx) {
const auto keys = settings::sorted_keys();
@ -329,6 +333,8 @@ void keys(::terminal::CommandContext&& ctx) {
terminalOK(ctx);
}
alignas(4) static constexpr char Gc[] PROGMEM = "GC";
void gc(::terminal::CommandContext&& ctx) {
const auto keys = settings::sorted_keys();
@ -354,6 +360,8 @@ void gc(::terminal::CommandContext&& ctx) {
terminalOK(ctx);
}
alignas(4) static constexpr char Del[] PROGMEM = "DEL";
void del(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() < 2) {
terminalError(ctx, F("del <key> [<key>...]"));
@ -372,6 +380,8 @@ void del(::terminal::CommandContext&& ctx) {
}
}
alignas(4) static constexpr char Set[] PROGMEM = "SET";
void set(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 3) {
terminalError(ctx, F("set <key> <value>"));
@ -386,6 +396,8 @@ void set(::terminal::CommandContext&& ctx) {
terminalError(ctx, F("could not set the key"));
}
alignas(4) static constexpr char Get[] PROGMEM = "GET";
void get(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() < 2) {
terminalError(ctx, F("get <key> [<key>...]"));
@ -411,39 +423,50 @@ void get(::terminal::CommandContext&& ctx) {
terminalOK(ctx);
}
alignas(4) static constexpr char Reload[] PROGMEM = "RELOAD";
void reload(::terminal::CommandContext&& ctx) {
espurnaReload();
terminalOK(ctx);
}
alignas(4) static constexpr char FactoryReset[] PROGMEM = "FACTORY.RESET";
void factory_reset(::terminal::CommandContext&& ctx) {
factoryReset();
terminalOK(ctx);
}
[[gnu::unused]]
alignas(4) static constexpr char Save[] PROGMEM = "SAVE";
[[gnu::unused]]
void save(::terminal::CommandContext&& ctx) {
eepromCommit();
terminalOK(ctx);
}
} // namespace commands
static constexpr ::terminal::Command List[] PROGMEM {
{Config, commands::config},
{Keys, commands::keys},
{Gc, commands::gc},
void setup() {
terminalRegisterCommand(F("CONFIG"), commands::config);
terminalRegisterCommand(F("KEYS"), commands::keys);
terminalRegisterCommand(F("GC"), commands::gc);
{Del, commands::del},
{Set, commands::set},
{Get, commands::get},
terminalRegisterCommand(F("DEL"), commands::del);
terminalRegisterCommand(F("SET"), commands::set);
terminalRegisterCommand(F("GET"), commands::get);
terminalRegisterCommand(F("RELOAD"), commands::reload);
terminalRegisterCommand(F("FACTORY.RESET"), commands::factory_reset);
{Reload, commands::reload},
{FactoryReset, commands::factory_reset},
#if not SETTINGS_AUTOSAVE
terminalRegisterCommand(F("SAVE"), commands::save);
{Save, commands::save},
#endif
};
} // namespace commands
void setup() {
espurna::terminal::add(commands::List);
}
} // namespace


+ 48
- 35
code/espurna/storage_eeprom.cpp View File

@ -71,46 +71,59 @@ void eepromBackup(uint32_t index){
}
#if TERMINAL_SUPPORT
alignas(4) static constexpr char EepromCommand[] PROGMEM = "EEPROM";
static void _eepromCommand(::terminal::CommandContext&& ctx) {
ctx.output.printf_P(PSTR("Sectors: %s, current: %lu\n"),
eepromSectors().c_str(), eepromCurrent());
if (_eeprom_commit_count > 0) {
ctx.output.printf_P(PSTR("Commits done: %lu, last: %s\n"),
_eeprom_commit_count, _eeprom_last_commit_result ? "OK" : "ERROR");
}
terminalOK(ctx);
}
void _eepromInitCommands() {
alignas(4) static constexpr char EepromCommit[] PROGMEM = "EEPROM.COMMIT";
terminalRegisterCommand(F("EEPROM"), [](::terminal::CommandContext&& ctx) {
ctx.output.printf_P(PSTR("Sectors: %s, current: %lu\n"),
eepromSectors().c_str(), eepromCurrent());
if (_eeprom_commit_count > 0) {
ctx.output.printf_P(PSTR("Commits done: %lu, last: %s\n"),
_eeprom_commit_count, _eeprom_last_commit_result ? "OK" : "ERROR");
}
terminalOK(ctx);
});
static void _eepromCommandCommit(::terminal::CommandContext&& ctx) {
_eepromCommit();
terminalOK(ctx);
}
terminalRegisterCommand(F("EEPROM.COMMIT"), [](::terminal::CommandContext&& ctx) {
_eepromCommit();
terminalOK(ctx);
});
terminalRegisterCommand(F("EEPROM.DUMP"), [](::terminal::CommandContext&& ctx) {
EEPROMr.dump(static_cast<Stream&>(ctx.output)); // XXX: only Print interface is used
terminalOK(ctx);
});
terminalRegisterCommand(F("FLASH.DUMP"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() < 2) {
terminalError(ctx, F("Wrong arguments"));
return;
}
uint32_t sector = ctx.argv[1].toInt();
uint32_t max = ESP.getFlashChipSize() / SPI_FLASH_SEC_SIZE;
if (sector >= max) {
terminalError(ctx, F("Sector out of range"));
return;
}
EEPROMr.dump(static_cast<Stream&>(ctx.output), sector); // XXX: only Print interface is used
terminalOK(ctx);
});
alignas(4) static constexpr char EepromDump[] PROGMEM = "EEPROM.DUMP";
static void _eepromCommandDump(::terminal::CommandContext&& ctx) {
EEPROMr.dump(static_cast<Stream&>(ctx.output)); // XXX: only Print interface is used
terminalOK(ctx);
}
alignas(4) static constexpr char FlashDump[] PROGMEM = "FLASH.DUMP";
static void _flashCommandDump(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() < 2) {
terminalError(ctx, F("Wrong arguments"));
return;
}
uint32_t sector = ctx.argv[1].toInt();
uint32_t max = ESP.getFlashChipSize() / SPI_FLASH_SEC_SIZE;
if (sector >= max) {
terminalError(ctx, F("Sector out of range"));
return;
}
EEPROMr.dump(static_cast<Stream&>(ctx.output), sector); // XXX: only Print interface is used
terminalOK(ctx);
}
static constexpr ::terminal::Command EepromCommands[] PROGMEM {
{EepromCommand, _eepromCommand},
{EepromCommit, _eepromCommandCommit},
{EepromDump, _eepromCommandDump},
{FlashDump, _flashCommandDump},
};
void _eepromCommandsSetup() {
espurna::terminal::add(EepromCommands);
}
#endif
// -----------------------------------------------------------------------------
@ -141,7 +154,7 @@ void eepromSetup() {
EEPROMr.begin(EepromSize);
#if TERMINAL_SUPPORT
_eepromInitCommands();
_eepromCommandsSetup();
#endif
espurnaRegisterLoop(eepromLoop);


+ 38
- 27
code/espurna/telnet.cpp View File

@ -852,39 +852,50 @@ bool connect(Address address) {
#if TERMINAL_SUPPORT
namespace terminal {
namespace commands {
void setup() {
terminalRegisterCommand(F("TELNET.REVERSE"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 3) {
terminalError(ctx, F("TELNET.REVERSE <HOST> <PORT>"));
return;
}
alignas(4) static constexpr char Reverse[] PROGMEM = "TELNET.REVERSE";
const auto ip = networkGetHostByName(ctx.argv[1]);
if (!ip.isSet()) {
terminalError(ctx, F("Host not found"));
return;
}
void reverse(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 3) {
terminalError(ctx, F("TELNET.REVERSE <HOST> <PORT>"));
return;
}
const auto convert_port = espurna::settings::internal::convert<uint16_t>;
auto port = convert_port(ctx.argv[2]);
if (!port) {
terminalError(ctx, F("Invalid port"));
return;
}
const auto ip = networkGetHostByName(ctx.argv[1]);
if (!ip.isSet()) {
terminalError(ctx, F("Host not found"));
return;
}
const auto address = Address{
.ip = ip,
.port = port,
};
const auto convert_port = espurna::settings::internal::convert<uint16_t>;
auto port = convert_port(ctx.argv[2]);
if (!port) {
terminalError(ctx, F("Invalid port"));
return;
}
if (connect(address)) {
terminalOK(ctx);
return;
}
const auto address = Address{
.ip = ip,
.port = port,
};
terminalError(ctx, F("Unable to connect"));
});
if (connect(address)) {
terminalOK(ctx);
return;
}
terminalError(ctx, F("Unable to connect"));
}
static constexpr ::terminal::Command List[] PROGMEM {
{Reverse, commands::reverse},
};
} // namespace commands
void setup() {
espurna::terminal::add(commands::List);
}
} // namespace terminal


+ 54
- 20
code/espurna/terminal.cpp View File

@ -58,37 +58,49 @@ Stream& SerialPort = TERMINAL_SERIAL_PORT;
namespace commands {
alignas(4) static constexpr char Commands[] PROGMEM = "COMMANDS";
alignas(4) static constexpr char Help[] PROGMEM = "HELP";
void help(CommandContext&& ctx) {
auto names = terminal::names();
std::sort(names.begin(), names.end(),
[](const __FlashStringHelper* lhs, const __FlashStringHelper* rhs) {
[](StringView lhs, StringView rhs) {
// XXX: Core's ..._P funcs only allow 2nd pointer to be in PROGMEM,
// explicitly load the 1st one
// TODO: can we just assume linker already sorted all strings?
const String lhs_as_string(lhs);
return strncasecmp_P(lhs_as_string.c_str(), reinterpret_cast<const char*>(rhs), lhs_as_string.length()) < 0;
return strncasecmp_P(
lhs_as_string.c_str(),
rhs.c_str(),
lhs_as_string.length()) < 0;
});
ctx.output.print(F("Available commands:\n"));
for (auto* name : names) {
ctx.output.printf("> %s\n", reinterpret_cast<const char*>(name));
for (auto name : names) {
ctx.output.printf("> %s\n", name.c_str());
}
terminalOK(ctx);
}
alignas(4) static constexpr char Reset[] PROGMEM = "RESET";
void reset(CommandContext&& ctx) {
prepareReset(CustomResetReason::Terminal);
terminalOK(ctx);
}
alignas(4) static constexpr char EraseConfig[] PROGMEM = "ERASE.CONFIG";
void erase_config(CommandContext&& ctx) {
terminalOK(ctx);
customResetReason(CustomResetReason::Terminal);
forceEraseSDKConfig();
}
alignas(4) static constexpr char Heap[] PROGMEM = "HEAP";
void heap(CommandContext&& ctx) {
const auto stats = systemHeapStats();
ctx.output.printf_P(PSTR("initial: %lu available: %lu contiguous: %lu\n"),
@ -97,12 +109,16 @@ void heap(CommandContext&& ctx) {
terminalOK(ctx);
}
alignas(4) static constexpr char Uptime[] PROGMEM = "UPTIME";
void uptime(CommandContext&& ctx) {
ctx.output.printf_P(PSTR("uptime %s\n"),
prettyDuration(systemUptime()).c_str());
terminalOK(ctx);
}
alignas(4) static constexpr char Info[] PROGMEM = "INFO";
void info(CommandContext&& ctx) {
const auto app = buildApp();
ctx.output.printf_P(PSTR("%s %s built %s\n"),
@ -261,6 +277,8 @@ StringView flash_chip_mode() {
return out;
}
alignas(4) static constexpr char Storage[] PROGMEM = "STORAGE";
void storage(CommandContext&& ctx) {
ctx.output.printf_P(PSTR("flash chip ID: 0x%06X\n"), ESP.getFlashChipId());
ctx.output.printf_P(PSTR("speed: %u\n"), ESP.getFlashChipSpeed());
@ -300,6 +318,8 @@ void storage(CommandContext&& ctx) {
terminalOK(ctx);
}
alignas(4) static constexpr char Adc[] PROGMEM = "ADC";
void adc(CommandContext&& ctx) {
const int pin = (ctx.argv.size() == 2)
? ctx.argv[1].toInt()
@ -310,40 +330,50 @@ void adc(CommandContext&& ctx) {
}
#if SYSTEM_CHECK_ENABLED
alignas(4) static constexpr char Stable[] PROGMEM = "STABLE";
void stable(CommandContext&& ctx) {
systemForceStable();
prepareReset(CustomResetReason::Stability);
}
alignas(4) static constexpr char Unstable[] PROGMEM = "UNSTABLE";
void unstable(CommandContext&& ctx) {
systemForceUnstable();
prepareReset(CustomResetReason::Stability);
}
alignas(4) static constexpr char Trap[] PROGMEM = "TRAP";
void trap(CommandContext&& ctx) {
__builtin_trap();
}
#endif
void setup() {
terminalRegisterCommand(F("COMMANDS"), commands::help);
terminalRegisterCommand(F("HELP"), commands::help);
static constexpr ::terminal::Command List[] PROGMEM {
{Commands, commands::help},
{Help, commands::help},
terminalRegisterCommand(F("INFO"), commands::info);
terminalRegisterCommand(F("STORAGE"), commands::storage);
terminalRegisterCommand(F("UPTIME"), commands::uptime);
terminalRegisterCommand(F("HEAP"), commands::heap);
{Info, commands::info},
{Storage, commands::storage},
{Uptime, commands::uptime},
{Heap, commands::heap},
terminalRegisterCommand(F("ADC"), commands::adc);
{Adc, commands::adc},
terminalRegisterCommand(F("RESET"), commands::reset);
terminalRegisterCommand(F("ERASE.CONFIG"), commands::erase_config);
{Reset, commands::reset},
{EraseConfig, commands::erase_config},
#if SYSTEM_CHECK_ENABLED
terminalRegisterCommand(F("STABLE"), commands::stable);
terminalRegisterCommand(F("UNSTABLE"), commands::unstable);
terminalRegisterCommand(F("TRAP"), commands::trap);
{Stable, commands::stable},
{Unstable, commands::unstable},
{Trap, commands::trap},
#endif
};
void setup() {
espurna::terminal::add(List);
}
} // namespace commands
@ -595,8 +625,8 @@ void setup() {
[](ApiRequest& api) {
api.handle([](AsyncWebServerRequest* request) {
auto* response = request->beginResponseStream(F("text/plain"));
for (auto* name : names()) {
response->print(name);
for (auto name : names()) {
response->write(name.c_str(), name.length());
response->print("\r\n");
}
@ -710,7 +740,11 @@ void terminalError(const espurna::terminal::CommandContext& ctx, const String& m
espurna::terminal::error(ctx, message);
}
void terminalRegisterCommand(const __FlashStringHelper* name, espurna::terminal::CommandFunc func) {
void terminalRegisterCommand(espurna::terminal::Commands commands) {
espurna::terminal::add(commands);
}
void terminalRegisterCommand(espurna::StringView name, espurna::terminal::CommandFunc func) {
espurna::terminal::add(name, func);
}


+ 2
- 1
code/espurna/terminal.h View File

@ -26,7 +26,8 @@ using namespace espurna::terminal;
void terminalOK(const espurna::terminal::CommandContext&);
void terminalError(const espurna::terminal::CommandContext&, const String&);
void terminalRegisterCommand(const __FlashStringHelper* name, espurna::terminal::CommandFunc func);
void terminalRegisterCommand(espurna::StringView name, espurna::terminal::CommandFunc func);
void terminalRegisterCommand(espurna::terminal::Commands);
void terminalWebApiSetup();
void terminalSetup();

+ 39
- 31
code/espurna/terminal_commands.cpp View File

@ -26,60 +26,68 @@ namespace {
namespace internal {
using Commands = std::forward_list<Command>;
Commands commands;
using CommandsView = std::forward_list<Commands>;
CommandsView commands;
} // namespace internal
} // namespace
size_t size() {
return std::distance(internal::commands.begin(), internal::commands.end());
size_t out { 0 };
for (const auto commands : internal::commands) {
out += commands.end - commands.begin;
}
return out;
}
CommandNames names() {
CommandNames out;
out.reserve(size());
for (auto& command : internal::commands) {
out.push_back(command.name);
for (const auto commands : internal::commands) {
for (auto it = commands.begin; it != commands.end; ++it) {
out.push_back((*it).name);
}
}
return out;
}
void add(Command command) {
if (command.func) {
internal::commands.emplace_front(std::move(command));
}
void add(Commands commands) {
internal::commands.emplace_front(std::move(commands));
}
void add(const __FlashStringHelper* name, CommandFunc func) {
add(Command{
void add(StringView name, CommandFunc func) {
const auto cmd = new Command{
.name = name,
.func = func });
.func = func,
};
add(Commands{
.begin = cmd,
.end = cmd + 1,
});
}
const Command* find(StringView name) {
auto found = std::find_if(
internal::commands.begin(),
internal::commands.end(),
// TODO: StringView comparison
// note that `String::equalsIgnoreCase(const __FlashStringHelper*)` does not exist, and will create a temporary `String`
// both use read-1-byte-at-a-time for PROGMEM, however this variant saves around 200μs in time since there's no temporary object
[&](const Command& command) {
const auto* lhs = name.c_str();
const auto* rhs = reinterpret_cast<const char*>(command.name);
const auto len = strlen_P(rhs);
return (name.length() == len)
&& (0 == strncasecmp_P(lhs, rhs, len));
});
if (found == internal::commands.end()) {
return nullptr;
for (const auto commands : internal::commands) {
const auto found = std::find_if(
commands.begin,
commands.end,
[&](const Command& command) {
return (name.length() == command.name.length())
&& (strncasecmp_P(name.c_str(),
command.name.c_str(),
command.name.length()) == 0);
});
if (found != commands.end) {
return found;
}
}
return &(*found);
return nullptr;
}
void ok(Print& out) {
@ -115,7 +123,7 @@ bool find_and_call(StringView cmd, Print& out) {
auto result = parse_line(cmd);
if (result.error != parser::Error::Ok) {
String message;
message += PSTR("TERMINAL: ");
message += STRING_VIEW("TERMINAL: ");
message += parser::error(result.error);
error(out, message);
return false;


+ 18
- 4
code/espurna/terminal_commands.h View File

@ -28,18 +28,32 @@ struct CommandContext {
using CommandFunc = void(*)(CommandContext&&);
struct Command {
const __FlashStringHelper* name;
StringView name;
CommandFunc func;
};
struct Commands {
const Command* begin;
const Command* end;
};
// store name<->func association at runtime
void add(const __FlashStringHelper* name, CommandFunc func);
void add(Command);
void add(Commands);
template <size_t Size>
void add(const Command (&command)[Size]) {
add(Commands{
.begin = &command[0],
.end = &command[Size],
});
}
void add(StringView, CommandFunc);
// total number of registered commands
size_t size();
using CommandNames = std::vector<const __FlashStringHelper*>;
using CommandNames = std::vector<StringView>;
CommandNames names();
// find registered command with 'name' or 'nullptr' on failure


+ 47
- 27
code/espurna/tuya.cpp View File

@ -606,40 +606,60 @@ error:
#endif
void setup() {
// Print all known DP associations
#if TERMINAL_SUPPORT
#if TERMINAL_SUPPORT
alignas(4) static constexpr char TuyaShow[] PROGMEM = "TUYA.SHOW";
terminalRegisterCommand(F("TUYA.SHOW"), [](::terminal::CommandContext&& ctx) {
ctx.output.printf_P(PSTR("Product: %s\n"), product.length() ? product.c_str() : "(unknown)");
static void terminalShow(::terminal::CommandContext&& ctx) {
ctx.output.printf_P(PSTR("Product: %s\n"), product.length() ? product.c_str() : "(unknown)");
ctx.output.print(F("\nConfig:\n"));
for (auto& kv : config) {
ctx.output.printf_P(PSTR("\"%s\" => \"%s\"\n"), kv.key.c_str(), kv.value.c_str());
}
ctx.output.print(F("\nConfig:\n"));
for (auto& kv : config) {
ctx.output.printf_P(
PSTR("\"%s\" => \"%s\"\n"),
kv.key.c_str(), kv.value.c_str());
}
ctx.output.print(F("\nKnown DP(s):\n"));
ctx.output.print(F("\nKnown DP(s):\n"));
#if LIGHT_PROVIDER == LIGHT_PROVIDER_CUSTOM
if (channelStateId) {
ctx.output.printf_P(PSTR("%u (bool) => lights state\n"), channelStateId.id());
}
for (auto& entry : channelIds.map()) {
ctx.output.printf_P(PSTR("%u (int) => %d (channel)\n"), entry.dp_id, entry.local_id);
}
if (channelStateId) {
ctx.output.printf_P(
PSTR("%u (bool) => lights state\n"),
channelStateId.id());
}
for (auto& entry : channelIds.map()) {
ctx.output.printf_P(
PSTR("%u (int) => %d (channel)\n"),
entry.dp_id, entry.local_id);
}
#endif
for (auto& entry : switchIds.map()) {
ctx.output.printf_P(PSTR("%u (bool) => %d (relay)\n"), entry.dp_id, entry.local_id);
}
});
for (auto& entry : switchIds.map()) {
ctx.output.printf_P(
PSTR("%u (bool) => %d (relay)\n"),
entry.dp_id, entry.local_id);
}
}
terminalRegisterCommand(F("TUYA.SAVE"), [](::terminal::CommandContext&&) {
for (auto& kv : config) {
setSetting(kv.key, kv.value);
}
});
alignas(4) static constexpr char TuyaSave[] PROGMEM = "TUYA.SAVE";
static void terminalSave(::terminal::CommandContext&& ctx) {
for (auto& kv : config) {
setSetting(kv.key, kv.value);
}
}
static constexpr ::terminal::Command TuyaCommands[] PROGMEM {
{TuyaShow, terminalShow},
{TuyaSave, terminalSave},
};
void terminalSetup() {
espurna::terminal::add(TuyaCommands);
}
#endif
void setup() {
#if TERMINAL_SUPPORT
tuya::terminalSetup();
#endif
// Print all IN and OUT messages


+ 5
- 0
code/espurna/types.h View File

@ -189,6 +189,11 @@ inline String operator+(String&& lhs, StringView rhs) {
return lhs;
}
inline String operator+=(String& lhs, StringView rhs) {
lhs.concat(rhs.c_str(), rhs.length());
return lhs;
}
#define STRING_VIEW(X) ({\
alignas(4) static constexpr char __pstr__[] PROGMEM = (X);\
::espurna::StringView{__pstr__};\


+ 133
- 106
code/espurna/wifi.cpp View File

@ -2231,142 +2231,169 @@ void configure() {
#if TERMINAL_SUPPORT
namespace terminal {
namespace commands {
void init() {
alignas(4) static constexpr char Stations[] PROGMEM = "WIFI.STATIONS";
terminalRegisterCommand(F("WIFI.STATIONS"), [](::terminal::CommandContext&& ctx) {
size_t stations { 0ul };
for (auto* it = wifi_softap_get_station_info(); it; it = STAILQ_NEXT(it, next), ++stations) {
ctx.output.printf_P(PSTR("%s %s\n"),
wifi::debug::mac(convertBssid(*it)).c_str(),
wifi::debug::ip(it->ip).c_str());
}
void stations(::terminal::CommandContext&& ctx) {
size_t stations { 0ul };
for (auto* it = wifi_softap_get_station_info(); it; it = STAILQ_NEXT(it, next), ++stations) {
ctx.output.printf_P(PSTR("%s %s\n"),
wifi::debug::mac(convertBssid(*it)).c_str(),
wifi::debug::ip(it->ip).c_str());
}
wifi_softap_free_station_info();
wifi_softap_free_station_info();
if (!stations) {
terminalError(ctx, F("No stations connected"));
return;
}
if (!stations) {
terminalError(ctx, F("No stations connected"));
return;
}
terminalOK(ctx);
});
terminalOK(ctx);
}
alignas(4) static constexpr char Network[] PROGMEM = "NETWORK";
terminalRegisterCommand(F("NETWORK"), [](::terminal::CommandContext&& ctx) {
for (auto& addr : addrList) {
ctx.output.printf_P(PSTR("%s%d %4s %6s "),
addr.ifname().c_str(),
addr.ifnumber(),
addr.ifUp() ? "up" : "down",
addr.isLocal() ? "local" : "global");
void network(::terminal::CommandContext&& ctx) {
for (auto& addr : addrList) {
ctx.output.printf_P(PSTR("%s%d %4s %6s "),
addr.ifname().c_str(),
addr.ifnumber(),
addr.ifUp() ? "up" : "down",
addr.isLocal() ? "local" : "global");
#if LWIP_IPV6
if (addr.isV4()) {
if (addr.isV4()) {
#endif
ctx.output.printf_P(PSTR("ip %s gateway %s mask %s\n"),
wifi::debug::ip(addr.ipv4()).c_str(),
wifi::debug::ip(addr.gw()).c_str(),
wifi::debug::ip(addr.netmask()).c_str());
ctx.output.printf_P(PSTR("ip %s gateway %s mask %s\n"),
wifi::debug::ip(addr.ipv4()).c_str(),
wifi::debug::ip(addr.gw()).c_str(),
wifi::debug::ip(addr.netmask()).c_str());
#if LWIP_IPV6
} else {
// TODO: ip6_addr[...] array is included in the list
// we'll just see another entry
// TODO: routing info is not attached to the netif :/
// ref. nd6.h (and figure out what it does)
ctx.output.printf_P(PSTR("ip %s\n"),
wifi::debug::ip(netif->ip6_addr[i]).c_str());
}
} else {
// TODO: ip6_addr[...] array is included in the list
// we'll just see another entry
// TODO: routing info is not attached to the netif :/
// ref. nd6.h (and figure out what it does)
ctx.output.printf_P(PSTR("ip %s\n"),
wifi::debug::ip(netif->ip6_addr[i]).c_str());
}
#endif
}
}
for (int n = 0; n < DNS_MAX_SERVERS; ++n) {
auto ip = IPAddress(dns_getserver(n));
if (!ip.isSet()) {
break;
}
ctx.output.printf_P(PSTR("dns %s\n"), wifi::debug::ip(ip).c_str());
for (int n = 0; n < DNS_MAX_SERVERS; ++n) {
auto ip = IPAddress(dns_getserver(n));
if (!ip.isSet()) {
break;
}
});
ctx.output.printf_P(PSTR("dns %s\n"), wifi::debug::ip(ip).c_str());
}
}
terminalRegisterCommand(F("WIFI"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 2) {
auto id = espurna::settings::internal::convert<size_t>(ctx.argv[1]);
if (id < wifi::sta::build::NetworksMax) {
settingsDump(ctx, wifi::sta::settings::query::Settings, id);
return;
}
alignas(4) static constexpr char Wifi[] PROGMEM = "WIFI";
terminalError(ctx, F("Network ID out of configurable range"));
void wifi(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 2) {
auto id = espurna::settings::internal::convert<size_t>(ctx.argv[1]);
if (id < wifi::sta::build::NetworksMax) {
settingsDump(ctx, wifi::sta::settings::query::Settings, id);
return;
}
const auto mode = wifi::opmode();
ctx.output.printf_P(PSTR("OPMODE: %s\n"), wifi::debug::opmode(mode).c_str());
terminalError(ctx, F("Network ID out of configurable range"));
return;
}
if (mode & OpmodeAp) {
auto current = wifi::ap::current();
const auto mode = wifi::opmode();
ctx.output.printf_P(PSTR("OPMODE: %s\n"), wifi::debug::opmode(mode).c_str());
ctx.output.printf_P(PSTR("SoftAP: bssid %s channel %hhu auth %s\n"),
wifi::debug::mac(current.bssid).c_str(),
current.channel,
wifi::debug::authmode(current.authmode).c_str(),
current.ssid.c_str(),
current.passphrase.c_str());
}
if (mode & OpmodeAp) {
auto current = wifi::ap::current();
if (mode & OpmodeSta) {
if (wifi::sta::connected()) {
station_config config{};
wifi_station_get_config(&config);
ctx.output.printf_P(PSTR("SoftAP: bssid %s channel %hhu auth %s\n"),
wifi::debug::mac(current.bssid).c_str(),
current.channel,
wifi::debug::authmode(current.authmode).c_str(),
current.ssid.c_str(),
current.passphrase.c_str());
}
auto network = wifi::sta::current(config);
ctx.output.printf_P(PSTR("STA: bssid %s rssi %hhd channel %hhu ssid \"%s\"\n"),
wifi::debug::mac(network.bssid).c_str(),
network.rssi, network.channel, network.ssid.c_str());
} else {
ctx.output.printf_P(PSTR("STA: %s\n"),
wifi::sta::connecting() ? "connecting" : "disconnected");
}
if (mode & OpmodeSta) {
if (wifi::sta::connected()) {
station_config config{};
wifi_station_get_config(&config);
auto network = wifi::sta::current(config);
ctx.output.printf_P(PSTR("STA: bssid %s rssi %hhd channel %hhu ssid \"%s\"\n"),
wifi::debug::mac(network.bssid).c_str(),
network.rssi, network.channel, network.ssid.c_str());
} else {
ctx.output.printf_P(PSTR("STA: %s\n"),
wifi::sta::connecting() ? "connecting" : "disconnected");
}
}
settingsDump(ctx, wifi::settings::query::Settings);
terminalOK(ctx);
});
settingsDump(ctx, wifi::settings::query::Settings);
terminalOK(ctx);
}
terminalRegisterCommand(F("WIFI.RESET"), [](::terminal::CommandContext&& ctx) {
wifiDisconnect();
wifi::settings::configure();
terminalOK(ctx);
});
alignas(4) static constexpr char Reset[] PROGMEM = "WIFI.RESET";
terminalRegisterCommand(F("WIFI.STA"), [](::terminal::CommandContext&& ctx) {
wifi::sta::toggle();
terminalOK(ctx);
});
void reset(::terminal::CommandContext&& ctx) {
wifiDisconnect();
wifi::settings::configure();
terminalOK(ctx);
}
terminalRegisterCommand(F("WIFI.AP"), [](::terminal::CommandContext&& ctx) {
wifi::ap::toggle();
terminalOK(ctx);
});
alignas(4) static constexpr char Station[] PROGMEM = "WIFI.STA";
terminalRegisterCommand(F("WIFI.SCAN"), [](::terminal::CommandContext&& ctx) {
wifi::sta::scan::wait(
[&](bss_info* info) {
ctx.output.printf_P(PSTR("BSSID: %s AUTH: %11s RSSI: %3hhd CH: %2hhu SSID: %s\n"),
wifi::debug::mac(convertBssid(*info)).c_str(),
wifi::debug::authmode(info->authmode).c_str(),
info->rssi,
info->channel,
convertSsid(*info).c_str()
);
},
[&](wifi::ScanError error) {
terminalError(ctx, wifi::debug::error(error));
}
);
});
void station(::terminal::CommandContext&& ctx) {
wifi::sta::toggle();
terminalOK(ctx);
}
alignas(4) static constexpr char AccessPoint[] PROGMEM = "WIFI.AP";
void access_point(::terminal::CommandContext&& ctx) {
wifi::ap::toggle();
terminalOK(ctx);
}
alignas(4) static constexpr char Scan[] PROGMEM = "WIFI.SCAN";
void scan(::terminal::CommandContext&& ctx) {
wifi::sta::scan::wait(
[&](bss_info* info) {
ctx.output.printf_P(PSTR("BSSID: %s AUTH: %11s RSSI: %3hhd CH: %2hhu SSID: %s\n"),
wifi::debug::mac(convertBssid(*info)).c_str(),
wifi::debug::authmode(info->authmode).c_str(),
info->rssi,
info->channel,
convertSsid(*info).c_str()
);
},
[&](wifi::ScanError error) {
terminalError(ctx, wifi::debug::error(error));
}
);
}
static constexpr ::terminal::Command List[] PROGMEM {
{Stations, commands::stations},
{Network, commands::network},
{Wifi, commands::wifi},
{Reset, commands::reset},
{Station, commands::station},
{AccessPoint, commands::access_point},
{Scan, commands::scan},
};
} // namespace commands
void init() {
espurna::terminal::add(commands::List);
}
} // namespace terminal


+ 38
- 10
code/test/unit/src/terminal/terminal.cpp View File

@ -47,7 +47,7 @@ void test_hex_codes() {
{
static bool abc_done = false;
add(F("abc"), [](CommandContext&& ctx) {
add("abc", [](CommandContext&& ctx) {
TEST_ASSERT_EQUAL(2, ctx.argv.size());
TEST_ASSERT_EQUAL_STRING("abc", ctx.argv[0].c_str());
TEST_ASSERT_EQUAL_STRING("abc", ctx.argv[1].c_str());
@ -109,27 +109,54 @@ void test_parse_overlap() {
}
}
// recent terminal version also allows a static commands list instead of passing
// each individual name+func one by one
void test_commands_array() {
static int command_calls = 0;
static Command commands[] {
Command{.name = "array.one", .func = [](CommandContext&&) {
++command_calls;
}},
Command{.name = "array.two", .func = [](CommandContext&&) {
++command_calls;
}},
Command{.name = "array.three", .func = [](CommandContext&&) {
++command_calls;
}},
};
const auto before = size();
add(Commands{std::begin(commands), std::end(commands)});
TEST_ASSERT_EQUAL(before + 3, size());
const char input[] = "array.one\narray.two\narray.three\n";
TEST_ASSERT(api_find_and_call(input, DefaultOutput));
TEST_ASSERT_EQUAL(3, command_calls);
}
// Ensure that we can register multiple commands (at least 3, might want to test much more in the future?)
// Ensure that registered commands can be called and they are called in order
void test_multiple_commands() {
// make sure commands execute in sequence
static int command_calls = 0;
add(F("test1"), [](CommandContext&& ctx) {
add("test1", [](CommandContext&& ctx) {
TEST_ASSERT_EQUAL_STRING("test1", ctx.argv[0].c_str());
TEST_ASSERT_EQUAL(1, ctx.argv.size());
TEST_ASSERT_EQUAL(0, command_calls);
++command_calls;
});
add(F("test2"), [](CommandContext&& ctx) {
add("test2", [](CommandContext&& ctx) {
TEST_ASSERT_EQUAL_STRING("test2", ctx.argv[0].c_str());
TEST_ASSERT_EQUAL(1, ctx.argv.size());
TEST_ASSERT_EQUAL(1, command_calls);
++command_calls;
});
add(F("test3"), [](CommandContext&& ctx) {
add("test3", [](CommandContext&& ctx) {
TEST_ASSERT_EQUAL_STRING("test3", ctx.argv[0].c_str());
TEST_ASSERT_EQUAL(1, ctx.argv.size());
TEST_ASSERT_EQUAL(2, command_calls);
@ -144,7 +171,7 @@ void test_multiple_commands() {
void test_command() {
static int counter = 0;
add(F("test.command"), [](CommandContext&& ctx) {
add("test.command", [](CommandContext&& ctx) {
TEST_ASSERT_EQUAL_MESSAGE(1, ctx.argv.size(),
"Command without args should have argc == 1");
++counter;
@ -169,12 +196,12 @@ void test_command() {
void test_command_args() {
static bool waiting = false;
add(F("test.command.arg1"), [](CommandContext&& ctx) {
add("test.command.arg1", [](CommandContext&& ctx) {
TEST_ASSERT_EQUAL(2, ctx.argv.size());
waiting = false;
});
add(F("test.command.arg1_empty"), [](CommandContext&& ctx) {
add("test.command.arg1_empty", [](CommandContext&& ctx) {
TEST_ASSERT_EQUAL(2, ctx.argv.size());
TEST_ASSERT(!ctx.argv[1].length());
waiting = false;
@ -262,11 +289,11 @@ void test_quotes() {
// we specify that commands lowercase == UPPERCASE
// last registered one should be called, we don't check for duplicates at this time
void test_case_insensitive() {
add(F("test.lowercase1"), [](CommandContext&&) {
add("test.lowercase1", [](CommandContext&&) {
TEST_FAIL_MESSAGE("`test.lowercase1` was registered first, but there's another function by the same name. This should not be called");
});
add(F("TEST.LOWERCASE1"), [](CommandContext&&) {
add("TEST.LOWERCASE1", [](CommandContext&&) {
__asm__ volatile ("nop");
});
@ -276,7 +303,7 @@ void test_case_insensitive() {
// We can use command ctx.output to send something back into the stream
void test_output() {
add(F("test.output"), [](CommandContext&& ctx) {
add("test.output", [](CommandContext&& ctx) {
if (ctx.argv.size() == 2) {
ctx.output.print(ctx.argv[1]);
}
@ -406,6 +433,7 @@ int main(int, char**) {
RUN_TEST(test_command);
RUN_TEST(test_command_args);
RUN_TEST(test_parse_overlap);
RUN_TEST(test_commands_array);
RUN_TEST(test_multiple_commands);
RUN_TEST(test_hex_codes);
RUN_TEST(test_quotes);


Loading…
Cancel
Save