From 1effc988e95a7c39ed673bbcc840ff20cec4bb75 Mon Sep 17 00:00:00 2001 From: Trygve Laugstøl Date: Sun, 13 Mar 2016 11:10:51 +0100 Subject: o Initial import of Diller Arduino library. --- README.md | 111 ++++++++++++++ library.properties | 9 ++ src/diller_client.h | 209 +++++++++++++++++++++++++ src/diller_core.h | 71 +++++++++ src/diller_serial.h | 45 ++++++ src/diller_utils.h | 348 ++++++++++++++++++++++++++++++++++++++++++ src/impl/diller_core_impl.h | 150 ++++++++++++++++++ src/impl/diller_serial_impl.h | 183 ++++++++++++++++++++++ 8 files changed, 1126 insertions(+) create mode 100644 README.md create mode 100644 library.properties create mode 100644 src/diller_client.h create mode 100644 src/diller_core.h create mode 100644 src/diller_serial.h create mode 100644 src/diller_utils.h create mode 100644 src/impl/diller_core_impl.h create mode 100644 src/impl/diller_serial_impl.h diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b14176 --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +# Diller Arduino Library + +Diller is an IoT environment for makers that want an fast and easy way of connecting their projects to the internet. Everything is open source but everything is also hosted so you can either install everything yourself or just ust the instance running at https://trygvis.io/diller. + +A setup consists of a device with a Diller client, a gateway and a web frontend. + +## Features + +# Diller MQTT + +Diller uses MQTT to communicate between the client and the gateway. + +## Device / Property hierarchy + +All messages are published under the `/diller` path. + + / + /property + / + /value + /type (retained) + /name (retained) + /description (retained) + +The device id is a globally unique id of the device. The MAC address of devices is a useful identifier. The property id is an device-specific identifier chosen by the device. + +# Diller serial API + +## Network settings + +Request: + +Update or query network settings. If no paramters are given, no changes are done. If ip is set to a blank string, it will use DHCP. + + network [ip=..] [gateway=..] [netmask=..] + +Response: + +The command will always return the current values. + + ok ip=.. gateway=.. netmask=.. netmask=.. + +## Wlan settings + +Request: + +Update or query wlan settings. If no paramters are given, no changes are done. + + wlan [ssid=..] [password=..] + +Response: + +The command will always return the current values. + + ok ssid=.. + +## Properties introspection + +Request: + + properties + +Response: + + ok count= + property key=.. [name=..] [description=..] + +## Change property + +TODO: Implement description? A longer string describing the property. + +Request: + + property id=.. [value=..] [name=..] + +Type examples: + +* `temperature` +* `switch` - boolean switches +* `humidity` +* `rtc` + +Response: + + ok + +The value might not be updated directly, but may be buffered on the device if it is not yet connected. + +## Reset the device + +Request: + + reset + +Response + + ok + +# Example session + +Get the current network configuration to update the Arduino's LCD display + + > network + < ok ip=1.3.3.7 netmask=255.255.255.0 gateway=1.3.3.1 ssid=awesome + +Register the properties. This is done on every boot to keep the server in sync with the firmware's features. Old properties will not be removed. + + > set-property id=temp-0 value=12.3 type=temperature name=Water + < ok + + diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..9b87256 --- /dev/null +++ b/library.properties @@ -0,0 +1,9 @@ +name=Diller +version=1.0.0 +author=Trygve Laugstøl +maintainer=Trygve Laugstøl +sentence=A IoT toolkit for makers +paragraph=Diller, awesome, yay! +category=Communication +url=https://github.com/trygvis/diller-arduino +architectures=* diff --git a/src/diller_client.h b/src/diller_client.h new file mode 100644 index 0000000..988db98 --- /dev/null +++ b/src/diller_client.h @@ -0,0 +1,209 @@ +#pragma once + +#include +#include +#include "diller_utils.h" + +namespace diller { +namespace client { + +using namespace diller::utils; + +enum class diller_cmd : uint8_t { + UNKNOWN, + STATUS, + NETWORK, + WLAN, + PROPERTY, + PROPERTIES +}; + +class diller_event_listener { + public: + virtual void on_diller_event(const key_value_map ¶ms) = 0; +}; + +class client { + public: + client() : listener(nullptr) { + } + + virtual ~client() { + } + + virtual void property(const char *property, const char *value, const char *name) = 0; + virtual void property_value(const char *property, const char *value) = 0; + virtual void property_name(const char *property, const char *name) = 0; + virtual void property_description(const char *property, const char *description) = 0; + + void set_diller_event_listener(diller_event_listener *l) { + listener = l; + } + + protected: + diller_event_listener *listener; +}; + +template> +class client_software_serial : public client { + public: + client_software_serial() : client(), params(), parser(params) { + } + + void wlan(const char *ssid, const char *password) { + io_t::print("wlan ssid="); + escape(ssid); + io_t::print(" password="); + escape(password); + } + + void property(const char *property, const char *value, const char *name) { + io_t::print("property id="); + escape(property); + io_t::print(" value="); + escape(value); + io_t::print(" name="); + escape(name); + } + + void property_value(const char *property, const char *value) { + io_t::print("property id="); + escape(property); + io_t::print(" value="); + escape(value); + } + + void property_name(const char *property, const char *name) { + io_t::print("property id="); + escape(property); + io_t::print(" name="); + escape(name); + } + + void property_description(const char *property, const char *description) { + io_t::print("property id="); + escape(property); + io_t::print(" description="); + escape(description); + } + + void loop() { + auto x = tty.readline(); + // Serial.print("x="); + // Serial.println(static_cast(x)); + if (x == tty_status::FULL_LINE) { + parser.parse(tty.line); + + process_command(); + } + } + + protected: + void escape(const char *s) { + char c; + while ((c = *s++) != '\0') { + if (c == ' ') { + io_t::print('\\'); + } + io_t::print(c); + } + } + + void process_command() { + if (params.is_empty()) { + return; + } + + if (!listener) { + return; + } + + listener->on_diller_event(params); + params.clear(); + } + + private: + // SoftwareSerial serial; + key_value_map_t params; + diller::utils::diller_parser parser; + diller::utils::tty tty; +}; + +void print_diller_event(const key_value_map ¶ms) { + if (!params.size()) { + return; + } + + Serial.print("Diller: "); + Serial.print(params.key(0)); + Serial.print(' '); + + for (uint8_t i = 1; i < params.size(); i++) { + auto key = params.key(i); + auto value = params.value(i); + + Serial.print(key); + if (value) { + Serial.print("="); + Serial.print(value); + } + Serial.print(" "); + } + Serial.println(); +} + +class printing_diller_event_listener : public diller_event_listener { + public: + void on_diller_event(const key_value_map ¶ms) { + print_diller_event(params); + } +}; + +class noop_diller_event_listener : public diller_event_listener { + public: + void on_diller_event(const key_value_map ¶ms) { + auto key = params.key(0); + if (strcmp("network", key) == 0) { + on_network(params); + } else if (strcmp("status", key) == 0) { + on_status(params); + } else if (strcmp("property", key) == 0) { + on_property(params); + } else if (strcmp("properties", key) == 0) { + on_properties(params); + } else if (strcmp("debug", key) == 0) { + on_debug(params); + } else { + on_unknown(params); + } + } + + protected: + virtual void on_network(const key_value_map ¶ms) { + static_cast(params); + } + + virtual void on_status(const key_value_map ¶ms) { + static_cast(params); + } + + virtual void on_property(const key_value_map ¶ms) { + static_cast(params); + } + + virtual void on_properties(const key_value_map ¶ms) { + static_cast(params); + } + + virtual void on_unknown(const key_value_map ¶ms) { + static_cast(params); + } + + virtual void on_debug(const key_value_map ¶ms) { + static_cast(params); + } +}; + +} // namespace client +} // namespace diller + diff --git a/src/diller_core.h b/src/diller_core.h new file mode 100644 index 0000000..a38779c --- /dev/null +++ b/src/diller_core.h @@ -0,0 +1,71 @@ +#pragma once + +#include "diller_utils.h" +#include +#include + +namespace diller { +namespace core { + +using diller::utils::property; + +template +using props = diller::utils::properties; + +enum class diller_error : uint8_t { + OK, INVAL, NOMEM +}; + +static +const char * to_string(diller_error err) { + switch (err) { + case diller_error::OK: + return "ok"; + case diller_error::INVAL: + return "inval"; + case diller_error::NOMEM: + return "nomem"; + default: + return "unknown"; + } +} + +enum class property_action : uint8_t { + VALUE, + NAME, + DESCRIPTION, +}; + +class property_action_listener { + public: + virtual void on_property_action(const property *, property_action) = 0; +}; + +template +class core { + public: + core(const String &mqtt_host, int mqtt_port); + void set_property_action_listener(property_action_listener *); + + void setup(); + void loop(); + + bool connected() const; + + diller_error cmd_property(const char *id, const char *value, const char *name); + props properties; + + const String mqtt_host; + const int mqtt_port = 1883; + String mac; + String client_id; + + private: + void callback(char* topic_, byte* payload, unsigned int length); + property_action_listener *property_action_listener_; +}; + +} // namespace core +} // namespace diller + +#include "impl/diller_core_impl.h" diff --git a/src/diller_serial.h b/src/diller_serial.h new file mode 100644 index 0000000..a9a2de4 --- /dev/null +++ b/src/diller_serial.h @@ -0,0 +1,45 @@ +#pragma once + +#include "diller_utils.h" +#include +#include + +namespace diller { +namespace serial { + +using diller::utils::property; +using diller::core::diller_error; +using diller::core::property_action; + +template +class diller_serial : protected diller::core::property_action_listener { + public: + diller_serial(d_core &diller) : diller(diller), params(), diller_parser(params) { + } + + void setup(); + void loop(); + + private: + void process_command(); + void on_property_action(const property *, property_action); + + void cmd_network(); + void cmd_wlan(); + void cmd_wlan(const char* ssid, const char* password); + void cmd_property(const char *id, const char *value, const char *name); + void cmd_list_properties(); + void show_status(wl_status_t wl_status); + + d_core &diller; + diller::utils::fixed_size_key_value_map<10> params; + diller::utils::diller_parser diller_parser; + diller::utils::tty tty; + + static const bool send_wlan_password = false; +}; + +} // namespace serial +} // namespace diller + +#include "impl/diller_serial_impl.h" diff --git a/src/diller_utils.h b/src/diller_utils.h new file mode 100644 index 0000000..5b1224d --- /dev/null +++ b/src/diller_utils.h @@ -0,0 +1,348 @@ +#pragma once + +#include + +namespace diller { +namespace utils { + +enum class tty_status : uint8_t { + NEED_MORE, + TIMEOUT, + EMPTY_LINE, + FULL_LINE, + LINE_OVERFLOW, +}; + +class serial_io { + public: + static auto available() -> decltype(Serial.available()) { + return Serial.available(); + } + + static auto read() -> decltype(Serial.read()) { + return Serial.read(); + } + + template + static auto print(A a) -> decltype(Serial.print(a)) { + return Serial.print(a); + } + + template + static auto println(A a) -> decltype(Serial.println(a)) { + return Serial.println(a); + } +}; + +template +class software_serial_io { + public: + static auto available() -> decltype(instance::software_serial.available()) { + return instance::software_serial.available(); + } + + static auto read() -> decltype(instance::software_serial.read()) { + return instance::software_serial.read(); + } + + template + static auto print(A a) -> decltype(instance::software_serial.print(a)) { + return instance::software_serial.print(a); + } + + template + static auto println(A a) -> decltype(instance::software_serial.println(a)) { + return instance::software_serial.println(a); + } +}; + +template +class tty { + public: + char line[buffer_size]; + uint8_t size = 0; + const bool debug = debug_; + const unsigned long timeout_ms = timeout_ms_; + + void init(bool reset = false) { + if (reset) { + line[0] = '\0'; + } + + size = 0; + last_char = 0; + } + + void quote(const String &s) { + Serial.print(s.c_str()); + } + + void quote(const char *str) { + Serial.print(str); + } + + void escape(const char *str) { + Serial.print(str); + } + + tty_status readline() { + while (io::available()) { + int c = io::read(); + + if (debug) { + Serial.print("read: c="); + Serial.println(c, HEX); + } + + if (c == '\n') { + // ignore + last_char = millis(); + } else if (c == '\r') { + line[size] = '\0'; + auto status = size > 0 ? tty_status::FULL_LINE : tty_status::EMPTY_LINE; + init(); + + if (debug) { + Serial.print("debug src=tty state=\"new line\" size="); + Serial.print(size, DEC); + Serial.print(" line="); + quote(line); + Serial.println(); + } + + return status; + } else { + if (size == buffer_size - 1) { + + if (debug) { + Serial.print("debug src=tty state=overflow size="); + Serial.print(size, DEC); + Serial.print(" line="); + quote(line); + Serial.println(); + } + init(); + + return tty_status::LINE_OVERFLOW; + } + line[size++] = static_cast(c); + last_char = millis(); + } + } + + unsigned long now = millis(); + + if (last_char > 0 && now >= (last_char + timeout_ms)) { + if (debug) { + line[size] = '\0'; + Serial.print("debug src=tty state=\"timeout\" line="); + quote(line); + Serial.println(); + } + + init(true); + return tty_status::TIMEOUT; + } + + return tty_status::NEED_MORE; + } + private: + unsigned long last_char = 0; +}; + +class key_value_map { + public: + virtual void clear(); + virtual void put(char *key, char *value) = 0; + virtual const char *key(uint8_t index) const = 0; + virtual const char *value(uint8_t index) const = 0; + virtual const char *find(const char *key) const = 0; + virtual uint8_t size() const = 0; + virtual bool is_empty() const = 0; + virtual bool is_full() const = 0; +}; + +template +class fixed_size_key_value_map : public key_value_map { + public: + uint8_t size_; + static_assert(max_args <= 256, "max_args is too big"); + char *keys[max_args]; + char *values[max_args]; + + void clear() { + size_ = 0; + } + + void put(char *key, char *value) { + if (size_ < max_args) { + keys[size_] = key; + values[size_] = value; + size_++; + } + } + + const char *key(uint8_t index) const { + return keys[index]; + } + + const char *value(uint8_t index) const { + return values[index]; + } + + const char *find(const char *key) const { + for (int i = 0; i < size_; i++) { + if (strcmp(keys[i], key) == 0) { + return values[i]; + } + } + + return nullptr; + } + + uint8_t size() const { + return size_; + } + + bool is_empty() const { + return size_ >= max_args; + } + + bool is_full() const { + return size_ >= max_args; + } +}; + +class diller_parser { + public: + diller_parser(key_value_map &map) : map(map) { + } + + void parse(char *line) { + char *c = line; + map.clear(); + + while (*c != '\0') { + while (*c == ' ') { + c++; + } + if (*c == '\0') { + break; + } + char *key = c, + *value = nullptr; + + while (*c != ' ' && *c != '\0') { + if (*c == '=') { + *c = '\0'; + c++; + value = c; + } + + c++; + } + + map.put(key, value); + + if (*c == '\0' || map.is_full()) { + break; + } + + *c++ = '\0'; + } + } + protected: + key_value_map ↦ +}; + +class property { + public: + property() : new_(true) { + } + + const String &id() const { + return id_; + } + + const String &value() const { + return value_; + } + + const String &name() const { + return name_; + } + + const String &description() const { + return description_; + } + + bool dirty() const { + return dirty_; + } + + void init(const String &id) { + id_ = id; + dirty_ = false; + } + + bool is_new() const { + return new_; + } + + void set_old() { + new_ = false; + } + + private: + String id_; + boolean dirty_; + boolean new_; + + String value_; + String name_; + String description_; +}; + +template +class properties { + public: + properties() : size_(0) { + } + + property* operator[](uint8_t i) { + if (i >= size_) { + return nullptr; + } + + return &properties_[i]; + } + + property* find(const char *id) { + for (int i = 0; i < size_; i++) { + if (properties_[i].id() == id) { + return &properties_[i]; + } + } + + if (size_ < max_property_count) { + auto p = &properties_[size_]; + size_++; + p->init(id); + return p; + } + + return nullptr; + } + + uint8_t size() { + return size_; + } + + private: + property properties_[max_property_count]; + uint8_t size_; +}; + +} // namespace utils +} // namespace diller + diff --git a/src/impl/diller_core_impl.h b/src/impl/diller_core_impl.h new file mode 100644 index 0000000..1246e59 --- /dev/null +++ b/src/impl/diller_core_impl.h @@ -0,0 +1,150 @@ +#include +#include +#include + +extern WiFiClient wifi_client; +extern PubSubClient mqtt_client; + +namespace diller { +namespace core { + +using diller::utils::tty_status; +using diller::utils::property; + +// core::core + +template +core::core(const String &mqtt_host, int mqtt_port) : + mqtt_host(mqtt_host), mqtt_port(mqtt_port), property_action_listener_(nullptr) { + mac = WiFi.macAddress(); + mac.toLowerCase(); + client_id = "diller-" + mac; +} + +template +void core::callback(char* topic_, byte* payload, unsigned int length) { + Serial.print("got message on "); + Serial.println(topic_); + Serial.println("payload"); + Serial.write(payload, length); + Serial.println(); + + String prefix = "/diller/" + mac + "/property/"; + String topic(topic_); + if (!topic.startsWith(prefix)) { + Serial.print("debug bad-prefix: "); + Serial.println(topic); + return; + } + + topic.remove(0, prefix.length()); + property *p = nullptr; + for (auto i = 0; i < properties.size(); i++) { + auto *x = properties[i]; + if (topic.startsWith(x->id())) { + p = x; + break; + } + } + + if (p) { + topic.remove(0, p->id().length() + 1); + + Serial.print("debug property="); + Serial.print(p->id()); + Serial.print(", topic="); + Serial.println(topic); + + property_action a; + + if (topic == "value") { + Serial.println("debug action=value"); + a = property_action::VALUE; + } else if (topic == "name") { + Serial.println("debug action=name"); + a = property_action::NAME; + } else if (topic == "description") { + Serial.println("debug action=description"); + a = property_action::DESCRIPTION; + } else { + return; + } + + if (property_action_listener_) { + property_action_listener_->on_property_action(p, a); + } + } else { + Serial.print("debug unknown-property"); + } +} + +template +bool core::connected() const { + return mqtt_client.connected(); +} + +template +void core::setup() { + mqtt_client.setServer(mqtt_host.c_str(), mqtt_port); + mqtt_client.setCallback([this](char* topic_, byte* payload, unsigned int length) { + callback(topic_, payload, length); + }); +} + +template +void core::loop() { + auto wl_status = WiFi.status(); + + if (wl_status == WL_CONNECTED) { + if (!mqtt_client.loop()) { + Serial.println("status mqtt=connecting"); + if (mqtt_client.connect(client_id.c_str())) { + Serial.println("status mqtt=connected"); + } else { + Serial.println("status mqtt=disconnected"); + } + } + } +} + +template +diller_error core::cmd_property(const char *id, const char *value, const char *name) { + if (!id) { + return diller_error::INVAL; + } + + auto p = properties.find(id); + + if (!p) { + return diller_error::NOMEM; + } + + if (value) { + String topic = "/diller/" + mac + "/property/" + id + "/value"; + mqtt_client.publish(topic.c_str(), value); + } + + if (name) { + String topic = "/diller/" + mac + "/property/" + id + "/name"; + mqtt_client.publish(topic.c_str(), name); + } + + if (p->is_new()) { + String topic = "/diller/" + mac + "/property/" + id + "/#"; + mqtt_client.subscribe(topic.c_str()); + Serial.print("debug subscribing="); + Serial.println(topic); + + p->set_old(); + } + + return diller_error::OK; +} + +template +void core::set_property_action_listener(property_action_listener *l) { + this->property_action_listener_ = l; +} + +} // namespace core +} // namespace diller diff --git a/src/impl/diller_serial_impl.h b/src/impl/diller_serial_impl.h new file mode 100644 index 0000000..47c18b5 --- /dev/null +++ b/src/impl/diller_serial_impl.h @@ -0,0 +1,183 @@ +// #include +#include +// #include + +extern WiFiClient wifi_client; +extern PubSubClient mqtt_client; + +namespace diller { +namespace serial { + +using diller::utils::tty_status; + +template +void diller_serial::cmd_network() { + Serial.print("ok ip="); + WiFi.localIP().printTo(Serial); + Serial.print(" gateway="); + WiFi.gatewayIP().printTo(Serial); + Serial.print(" netmask="); + WiFi.subnetMask().printTo(Serial); + Serial.println(); +} + +template +void diller_serial::cmd_wlan() { + Serial.print("ok ssid="); + tty.quote(WiFi.SSID()); + if (send_wlan_password) { + Serial.print(" password="); + Serial.print(WiFi.psk()); + } + + Serial.print(" mac="); + Serial.println(diller.mac); +} + +template +void diller_serial::cmd_wlan(const char* ssid, const char* password) { + WiFi.begin(ssid, password); + Serial.println("ok"); +} + +template +void diller_serial::cmd_property(const char *id, const char *value, const char *name) { + auto ret = diller.cmd_property(id, value, name); + + if (ret != diller_error::OK) { + Serial.print("fail error="); + Serial.println(to_string(ret)); + } +} + +template +void diller_serial::cmd_list_properties() { + Serial.print("ok count="); + Serial.println(diller.properties.size()); + for (auto i = 0; i < diller.properties.size(); i++) { + auto p = diller.properties[i]; + Serial.print("property id="); + Serial.print(p->id()); + Serial.print(" name="); + Serial.print(p->name()); + Serial.print(" description="); + Serial.print(p->description()); + Serial.println(); + } +} + +template +void diller_serial::show_status(wl_status_t wl_status) { + const char *wl_status_s; + if (wl_status == WL_IDLE_STATUS) { + wl_status_s = "idle"; + } else if (wl_status == WL_NO_SSID_AVAIL) { + wl_status_s = "no-ssid"; + } else if (wl_status == WL_SCAN_COMPLETED) { + wl_status_s = "scan-completed"; + } else if (wl_status == WL_CONNECTED) { + wl_status_s = "connected"; + } else if (wl_status == WL_CONNECT_FAILED) { + wl_status_s = "connect-failed"; + } else if (wl_status == WL_CONNECTION_LOST) { + wl_status_s = "connection-lost"; + } else if (wl_status == WL_DISCONNECTED) { + wl_status_s = "disconnected"; + } else { + wl_status_s = "unknown"; + } + + const char *mqtt_status_s; + if (diller.connected()) { + mqtt_status_s = "connected"; + } else { + mqtt_status_s = "disconnected"; + } + + Serial.print("status wlan="); + Serial.print(wl_status_s); + Serial.print(" mqtt="); + Serial.println(mqtt_status_s); +} + +template +void diller_serial::process_command() { + if (params.is_empty()) { + return; + } + + auto cmd = params.key(0); + + if (strcmp("network", cmd) == 0) { + if (params.size() == 1) { + cmd_network(); + } else { + Serial.println("fail error=invalid_argument"); + } + } else if (strcmp("wlan", cmd) == 0) { + if (params.size() == 1) { + cmd_wlan(); + } else if (params.size() == 3) { + const char* ssid = params.find("ssid"); + const char* password = params.find("password"); + cmd_wlan(ssid, password); + } else { + Serial.println("fail error=invalid_argument"); + } + } else if (strcmp("property", cmd) == 0) { + auto id = params.find("id"); + auto value = params.find("value"); + auto name = params.find("name"); + cmd_property(id, value, name); + } else if (strcmp("list-properties", cmd) == 0) { + cmd_list_properties(); + } else { + Serial.print("fail error=unknown_command cmd="); + tty.escape(cmd); + Serial.println(); + } +} + +template +void diller_serial::setup() { + diller.set_property_action_listener(this); +} + +template +void diller_serial::loop() { + if (tty.readline() == tty_status::FULL_LINE) { + diller_parser.parse(tty.line); + + process_command(); + } + + static auto last_wl_status = WiFi.status(); + auto wl_status = WiFi.status(); + + static auto last_status = millis(); + auto now = millis(); + static const auto show_status_interval = 5000; + + if (last_wl_status != wl_status) { + show_status(wl_status); + last_wl_status = wl_status; + last_status = now; + } else if (now > (last_status + show_status_interval)) { + show_status(wl_status); + last_status = now; + } +} + +template +void diller_serial::on_property_action(const property *p, property_action) { + Serial.print("property id="); + tty.quote(p->id()); + Serial.print(" value="); + tty.quote(p->value()); + Serial.print(" description="); + tty.quote(p->description()); + Serial.println(); +} + +} // namespace serial +} // namespace diller -- cgit v1.2.3