Terminal: change command-line parser (#2247)
Change the underlying command line handling:
- switch to a custom parser, inspired by redis / sds
- update terminalRegisterCommand signature, pass only bare minimum
- clean-up `help` & `commands`. update settings `set`, `get` and `del`
- allow our custom test suite to run command-line tests
- clean-up Stream IO to allow us to print large things into debug stream (for example, `eeprom.dump`)
- send parsing errors to the debug log
As a proof of concept, introduce `TERMINAL_MQTT_SUPPORT` and `TERMINAL_WEB_API_SUPPORT`
- MQTT subscribes to the `<root>/cmd/set` and sends response to the `<root>/cmd`. We can't output too much, as we don't have any large-send API.
- Web API listens to the `/api/cmd?apikey=...&line=...` (or PUT, params inside the body). This one is intended as a possible replacement of the `API_SUPPORT`. Internals introduce a 'task' around the AsyncWebServerRequest object that will simulate what WiFiClient does and push data into it continuously, switching between CONT and SYS.
Both are experimental. We only accept a single command and not every command is updated to use Print `ctx.output` object. We are also somewhat limited by the Print / Stream overall, perhaps I am overestimating the usefulness of Arduino compatibility to such an extent :)
Web API handler can also sometimes show only part of the result, whenever the command tries to yield() by itself waiting for something. Perhaps we would need to create a custom request handler for that specific use-case. 4 years ago Terminal: change command-line parser (#2247)
Change the underlying command line handling:
- switch to a custom parser, inspired by redis / sds
- update terminalRegisterCommand signature, pass only bare minimum
- clean-up `help` & `commands`. update settings `set`, `get` and `del`
- allow our custom test suite to run command-line tests
- clean-up Stream IO to allow us to print large things into debug stream (for example, `eeprom.dump`)
- send parsing errors to the debug log
As a proof of concept, introduce `TERMINAL_MQTT_SUPPORT` and `TERMINAL_WEB_API_SUPPORT`
- MQTT subscribes to the `<root>/cmd/set` and sends response to the `<root>/cmd`. We can't output too much, as we don't have any large-send API.
- Web API listens to the `/api/cmd?apikey=...&line=...` (or PUT, params inside the body). This one is intended as a possible replacement of the `API_SUPPORT`. Internals introduce a 'task' around the AsyncWebServerRequest object that will simulate what WiFiClient does and push data into it continuously, switching between CONT and SYS.
Both are experimental. We only accept a single command and not every command is updated to use Print `ctx.output` object. We are also somewhat limited by the Print / Stream overall, perhaps I am overestimating the usefulness of Arduino compatibility to such an extent :)
Web API handler can also sometimes show only part of the result, whenever the command tries to yield() by itself waiting for something. Perhaps we would need to create a custom request handler for that specific use-case. 4 years ago |
|
- /*
-
- INFLUXDB MODULE
-
- Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
-
- */
-
- #include "influxdb.h"
-
- #if INFLUXDB_SUPPORT
-
- #include <map>
- #include <memory>
-
- #include "broker.h"
- #include "rpc.h"
- #include "sensor.h"
- #include "terminal.h"
- #include "ws.h"
- #include "libs/AsyncClientHelpers.h"
-
- #include <ESPAsyncTCP.h>
-
- const char InfluxDb_http_success[] = "HTTP/1.1 204";
- const char InfluxDb_http_template[] PROGMEM = "POST /write?db=%s&u=%s&p=%s HTTP/1.1\r\nHost: %s:%u\r\nContent-Length: %d\r\n\r\n";
-
- class AsyncInfluxDB : public AsyncClient {
- public:
-
- constexpr static const unsigned long ClientTimeout = 5000;
- constexpr static const size_t DataBufferSize = 256;
-
- AsyncClientState state = AsyncClientState::Disconnected;
- String host;
- uint16_t port = 0;
-
- std::map<String, String> values;
- String payload;
-
- bool flush = false;
- uint32_t timestamp = 0;
- };
-
- bool _idb_enabled = false;
- std::unique_ptr<AsyncInfluxDB> _idb_client = nullptr;
-
- // -----------------------------------------------------------------------------
-
- void _idbInitClient() {
-
- _idb_client = std::make_unique<AsyncInfluxDB>();
-
- _idb_client->payload.reserve(AsyncInfluxDB::DataBufferSize);
-
- _idb_client->onDisconnect([](void * s, AsyncClient * ptr) {
- auto *client = reinterpret_cast<AsyncInfluxDB*>(ptr);
- DEBUG_MSG_P(PSTR("[INFLUXDB] Disconnected\n"));
- client->flush = false;
- client->payload = "";
- client->timestamp = 0;
- client->state = AsyncClientState::Disconnected;
- }, nullptr);
-
- _idb_client->onTimeout([](void * s, AsyncClient * client, uint32_t time) {
- DEBUG_MSG_P(PSTR("[INFLUXDB] Network timeout after %ums\n"), time);
- client->close(true);
- }, nullptr);
-
- _idb_client->onData([](void * arg, AsyncClient * ptr, void * response, size_t len) {
- // ref: https://docs.influxdata.com/influxdb/v1.7/tools/api/#summary-table-1
- auto *client = reinterpret_cast<AsyncInfluxDB*>(ptr);
- if (client->state == AsyncClientState::Connected) {
- client->state = AsyncClientState::Disconnecting;
-
- const bool result = (len > sizeof(InfluxDb_http_success) && (0 == strncmp((char*) response, InfluxDb_http_success, strlen(InfluxDb_http_success))));
- DEBUG_MSG_P(PSTR("[INFLUXDB] %s response after %ums\n"), result ? "Success" : "Failure", millis() - client->timestamp);
-
- client->timestamp = millis();
- client->close();
- }
- }, nullptr);
-
- _idb_client->onPoll([](void * arg, AsyncClient * ptr) {
- auto *client = reinterpret_cast<AsyncInfluxDB*>(ptr);
- unsigned long ts = millis() - client->timestamp;
- if (ts > AsyncInfluxDB::ClientTimeout) {
- DEBUG_MSG_P(PSTR("[INFLUXDB] No response after %ums\n"), ts);
- client->close(true);
- return;
- }
-
- if (client->payload.length()) {
- client->write(client->payload.c_str(), client->payload.length());
- client->payload = "";
- }
- });
-
- _idb_client->onConnect([](void * arg, AsyncClient * ptr) {
-
- auto *client = reinterpret_cast<AsyncInfluxDB*>(ptr);
-
- client->timestamp = millis();
- client->state = AsyncClientState::Connected;
-
- DEBUG_MSG_P(PSTR("[INFLUXDB] Connected to %s:%u\n"),
- IPAddress(client->getRemoteAddress()).toString().c_str(),
- client->getRemotePort()
- );
-
- constexpr const int BUFFER_SIZE = 256;
- char headers[BUFFER_SIZE];
- int len = snprintf_P(headers, sizeof(headers), InfluxDb_http_template,
- getSetting("idbDatabase", INFLUXDB_DATABASE).c_str(),
- getSetting("idbUsername", INFLUXDB_USERNAME).c_str(),
- getSetting("idbPassword", INFLUXDB_PASSWORD).c_str(),
- client->host.c_str(), client->port, client->payload.length()
- );
- if ((len < 0) || (len > BUFFER_SIZE - 1)) {
- client->close(true);
- return;
- }
-
- client->write(headers, len);
-
- });
-
- }
-
-
- // -----------------------------------------------------------------------------
-
- bool _idbWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
- return (strncmp(key, "idb", 3) == 0);
- }
-
- void _idbWebSocketOnVisible(JsonObject& root) {
- root["idbVisible"] = 1;
- }
-
- void _idbWebSocketOnConnected(JsonObject& root) {
- root["idbEnabled"] = getSetting("idbEnabled", 1 == INFLUXDB_ENABLED);
- root["idbHost"] = getSetting("idbHost", INFLUXDB_HOST);
- root["idbPort"] = getSetting("idbPort", INFLUXDB_PORT);
- root["idbDatabase"] = getSetting("idbDatabase", INFLUXDB_DATABASE);
- root["idbUsername"] = getSetting("idbUsername", INFLUXDB_USERNAME);
- root["idbPassword"] = getSetting("idbPassword", INFLUXDB_PASSWORD);
- }
-
- void _idbConfigure() {
- _idb_enabled = getSetting("idbEnabled", 1 == INFLUXDB_ENABLED);
- if (_idb_enabled && (getSetting("idbHost", INFLUXDB_HOST).length() == 0)) {
- _idb_enabled = false;
- setSetting("idbEnabled", 0);
- }
- if (_idb_enabled && !_idb_client) _idbInitClient();
- }
-
- void _idbBrokerSensor(const String& topic, unsigned char id, double, const char* value) {
- idbSend(topic.c_str(), id, value);
- }
-
- void _idbBrokerStatus(const String& topic, unsigned char id, unsigned int value) {
- idbSend(topic.c_str(), id, String(int(value)).c_str());
- }
-
- // -----------------------------------------------------------------------------
-
- bool idbSend(const char * topic, const char * payload) {
- if (!_idb_enabled) return false;
- if (_idb_client->state != AsyncClientState::Disconnected) return false;
-
- _idb_client->values[topic] = payload;
- _idb_client->flush = true;
-
- return true;
- }
-
- void _idbSend(const String& host, const uint16_t port) {
- if (_idb_client->state != AsyncClientState::Disconnected) return;
-
- DEBUG_MSG_P(PSTR("[INFLUXDB] Sending to %s:%u\n"), host.c_str(), port);
-
- // TODO: cache `Host: <host>:<port>` header instead of storing things separately?
- _idb_client->host = host;
- _idb_client->port = port;
- _idb_client->timestamp = millis();
- _idb_client->state = _idb_client->connect(host.c_str(), port)
- ? AsyncClientState::Connecting
- : AsyncClientState::Disconnected;
-
- if (_idb_client->state == AsyncClientState::Disconnected) {
- DEBUG_MSG_P(PSTR("[INFLUXDB] Connection to %s:%u failed\n"), host.c_str(), port);
- _idb_client->close(true);
- }
- }
-
- void _idbFlush() {
- // Clean-up client object when not in use
- if (_idb_client && !_idb_enabled && (_idb_client->state == AsyncClientState::Disconnected)) {
- _idb_client = nullptr;
- }
-
- // Wait until current connection is finished
- if (!_idb_client) return;
- if (!_idb_client->flush) return;
- if (_idb_client->state != AsyncClientState::Disconnected) return;
-
- // Wait until connected
- if (!wifiConnected()) return;
-
- const auto host = getSetting("idbHost", INFLUXDB_HOST);
- const auto port = getSetting("idbPort", static_cast<uint16_t>(INFLUXDB_PORT));
-
- // TODO: should we always store specific pairs like tspk keeps relay / sensor readings?
- // note that we also send heartbeat data, persistent values should be flagged
- const String device = getSetting("hostname");
-
- _idb_client->payload = "";
- for (auto& pair : _idb_client->values) {
- if (!isNumber(pair.second.c_str())) {
- String quoted;
- quoted.reserve(pair.second.length() + 2);
- quoted += '"';
- quoted += pair.second;
- quoted += '"';
- pair.second = quoted;
- }
-
- char buffer[128] = {0};
- snprintf_P(buffer, sizeof(buffer),
- PSTR("%s,device=%s value=%s\n"),
- pair.first.c_str(), device.c_str(), pair.second.c_str()
- );
- _idb_client->payload += buffer;
- }
- _idb_client->values.clear();
-
- _idbSend(host, port);
- }
-
- bool idbSend(const char * topic, unsigned char id, const char * payload) {
- char measurement[64];
- snprintf(measurement, sizeof(measurement), "%s,id=%d", topic, id);
- return idbSend(measurement, payload);
- }
-
- bool idbEnabled() {
- return _idb_enabled;
- }
-
- void idbSetup() {
-
- _idbConfigure();
-
- #if WEB_SUPPORT
- wsRegister()
- .onVisible(_idbWebSocketOnVisible)
- .onConnected(_idbWebSocketOnConnected)
- .onKeyCheck(_idbWebSocketOnKeyCheck);
- #endif
-
- StatusBroker::Register(_idbBrokerStatus);
-
- #if SENSOR_SUPPORT
- SensorReportBroker::Register(_idbBrokerSensor);
- #endif
-
- espurnaRegisterReload(_idbConfigure);
- espurnaRegisterLoop(_idbFlush);
-
- #if TERMINAL_SUPPORT
- terminalRegisterCommand(F("IDB.SEND"), [](const terminal::CommandContext& ctx) {
- if (ctx.argc != 4) {
- terminalError(F("idb.send <topic> <id> <value>"));
- return;
- }
-
- idbSend(ctx.argv[1].c_str(), ctx.argv[2].toInt(), ctx.argv[3].c_str());
- });
- #endif
-
- }
-
- #endif
|