platformio: split code into libraries

This commit is contained in:
Evgeny Zinoviev 2023-05-29 05:44:59 +03:00
parent 6a64c97c79
commit 8b2088103a
36 changed files with 1113 additions and 711 deletions

66
.clang-format Normal file
View File

@ -0,0 +1,66 @@
# Generated from CLion C/C++ Code Style settings
BasedOnStyle: LLVM
AccessModifierOffset: -4
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: None
AlignOperands: Align
AllowAllArgumentsOnNextLine: false
AllowAllConstructorInitializersOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: Always
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: All
AllowShortIfStatementsOnASingleLine: Always
AllowShortLambdasOnASingleLine: All
AllowShortLoopsOnASingleLine: true
AlwaysBreakAfterReturnType: None
AlwaysBreakTemplateDeclarations: Yes
BreakBeforeBraces: Custom
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: Never
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterUnion: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
SplitEmptyFunction: false
SplitEmptyRecord: true
BreakBeforeBinaryOperators: None
BreakBeforeTernaryOperators: true
BreakConstructorInitializers: BeforeColon
BreakInheritanceList: BeforeColon
ColumnLimit: 0
CompactNamespaces: false
ContinuationIndentWidth: 8
IndentCaseLabels: true
IndentPPDirectives: None
IndentWidth: 4
KeepEmptyLinesAtTheStartOfBlocks: true
MaxEmptyLinesToKeep: 2
NamespaceIndentation: All
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PointerAlignment: Left
ReflowComments: false
SpaceAfterCStyleCast: true
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 0
SpacesInAngles: false
SpacesInCStyleCastParentheses: false
SpacesInContainerLiterals: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
TabWidth: 4
UseTab: Never

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
.idea .idea
.vscode
/venv /venv
/node_modules /node_modules
*.pyc *.pyc

View File

@ -1,6 +1,6 @@
#include "led.h" #include "led.h"
namespace homekit { namespace homekit::led {
void Led::on_off(uint16_t delay_ms, bool last_delay) const { void Led::on_off(uint16_t delay_ms, bool last_delay) const {
on(); on();
@ -17,4 +17,11 @@ void Led::blink(uint8_t count, uint16_t delay_ms) const {
} }
} }
#ifdef CONFIG_TARGET_NODEMCU
const Led* board_led = new Led(CONFIG_BOARD_LED_GPIO);
#endif
const Led* mcu_led = new Led(CONFIG_MCU_LED_GPIO);
} }

View File

@ -1,10 +1,10 @@
#ifndef COMMON_HOMEKIT_LED_H #ifndef HOMEKIT_LIB_LED_H
#define COMMON_HOMEKIT_LED_H #define HOMEKIT_LIB_LED_H
#include <Arduino.h> #include <Arduino.h>
#include <stdint.h> #include <stdint.h>
namespace homekit { namespace homekit::led {
class Led { class Led {
private: private:
@ -23,6 +23,11 @@ public:
void blink(uint8_t count, uint16_t delay_ms) const; void blink(uint8_t count, uint16_t delay_ms) const;
}; };
#ifdef CONFIG_TARGET_NODEMCU
extern const Led* board_led;
#endif
extern const Led* mcu_led;
} }
#endif //COMMON_HOMEKIT_LED_H #endif //HOMEKIT_LIB_LED_H

View File

@ -1,6 +1,6 @@
{ {
"name": "homekit_led", "name": "homekit_led",
"version": "1.0.6", "version": "1.0.8",
"build": { "build": {
"flags": "-I../../include" "flags": "-I../../include"
} }

View File

@ -0,0 +1,194 @@
#include "./main.h"
#include <homekit/led.h>
#include <homekit/mqtt/mqtt.h>
#include <homekit/mqtt/module/diagnostics.h>
#include <homekit/mqtt/module/ota.h>
namespace homekit::main {
enum WorkingMode working_mode = WorkingMode::NORMAL;
static const uint16_t recovery_boot_detection_ms = 2000;
static const uint8_t recovery_boot_delay_ms = 100;
static volatile enum WiFiConnectionState wifi_state = WiFiConnectionState::WAITING;
static void* service = nullptr;
static WiFiEventHandler wifiConnectHandler, wifiDisconnectHandler;
static Ticker wifiTimer;
static mqtt::MqttDiagnosticsModule* mqttDiagModule;
static mqtt::MqttOtaModule* mqttOtaModule;
#if MQTT_BLINK
static StopWatch blinkStopWatch;
#endif
#ifndef CONFIG_TARGET_ESP01
static DNSServer* dnsServer = nullptr;
#endif
static void onWifiConnected(const WiFiEventStationModeGotIP& event);
static void onWifiDisconnected(const WiFiEventStationModeDisconnected& event);
static void wifiConnect() {
const char *ssid, *psk, *hostname;
auto cfg = config::read();
wifi::getConfig(cfg, &ssid, &psk, &hostname);
PRINTF("Wi-Fi STA creds: ssid=%s, psk=%s, hostname=%s\n", ssid, psk, hostname);
wifi_state = WiFiConnectionState::WAITING;
WiFi.mode(WIFI_STA);
WiFi.hostname(hostname);
WiFi.begin(ssid, psk);
PRINT("connecting to wifi..");
}
#ifndef CONFIG_TARGET_ESP01
static void wifiHotspot() {
led::mcu_led->on();
auto scanResults = wifi::scan();
WiFi.mode(WIFI_AP);
WiFi.softAP(wifi::AP_SSID);
dnsServer = new DNSServer();
dnsServer->start(53, "*", WiFi.softAPIP());
service = new HttpServer(scanResults);
((HttpServer*)service)->start();
}
static void waitForRecoveryPress() {
pinMode(CONFIG_FLASH_GPIO, INPUT_PULLUP);
for (uint16_t i = 0; i < recovery_boot_detection_ms; i += recovery_boot_delay_ms) {
delay(recovery_boot_delay_ms);
if (digitalRead(CONFIG_FLASH_GPIO) == LOW) {
working_mode = WorkingMode::RECOVERY;
break;
}
}
}
#endif
void setup() {
WiFi.disconnect();
#ifndef CONFIG_TARGET_ESP01
homekit::main::waitForRecoveryPress();
#endif
#ifdef DEBUG
Serial.begin(115200);
#endif
auto cfg = config::read();
if (config::isDirty(cfg)) {
PRINTLN("config is dirty, erasing...");
config::erase(cfg);
#ifdef CONFIG_TARGET_NODEMCU
led::board_led->blink(10, 50);
#else
led::mcu_led->blink(10, 50);
#endif
}
#ifndef CONFIG_TARGET_ESP01
switch (working_mode) {
case WorkingMode::RECOVERY:
wifiHotspot();
break;
case WorkingMode::NORMAL:
#endif
wifiConnectHandler = WiFi.onStationModeGotIP(onWifiConnected);
wifiDisconnectHandler = WiFi.onStationModeDisconnected(onWifiDisconnected);
wifiConnect();
#ifndef CONFIG_TARGET_ESP01
break;
}
#endif
}
void loop(LoopConfig* config) {
#ifndef CONFIG_TARGET_ESP01
if (working_mode == WorkingMode::NORMAL) {
#endif
if (wifi_state == WiFiConnectionState::WAITING) {
PRINT(".");
led::mcu_led->blink(2, 50);
delay(1000);
return;
}
if (wifi_state == WiFiConnectionState::JUST_CONNECTED) {
#ifdef CONFIG_TARGET_NODEMCU
led::board_led->blink(3, 300);
#else
led::mcu_led->blink(3, 300);
#endif
wifi_state = WiFiConnectionState::CONNECTED;
if (service == nullptr) {
service = new mqtt::Mqtt();
mqttDiagModule = new mqtt::MqttDiagnosticsModule();
mqttOtaModule = new mqtt::MqttOtaModule();
((mqtt::Mqtt*)service)->addModule(mqttDiagModule);
((mqtt::Mqtt*)service)->addModule(mqttOtaModule);
if (config != nullptr)
config->onMqttCreated(*(mqtt::Mqtt*)service);
}
((mqtt::Mqtt*)service)->connect();
#if MQTT_BLINK
blinkStopWatch.save();
#endif
}
auto mqtt = (mqtt::Mqtt*)service;
if (static_cast<int>(wifi_state) >= 1 && mqtt != nullptr) {
mqtt->loop();
if (mqttOtaModule != nullptr && mqttOtaModule->isReadyToRestart()) {
mqtt->disconnect();
}
#if MQTT_BLINK
// periodically blink board led
if (blinkStopWatch.elapsed(5000)) {
#ifdef CONFIG_TARGET_NODEMCU
board_led->blink(1, 10);
#endif
blinkStopWatch.save();
}
#endif
}
#ifndef CONFIG_TARGET_ESP01
} else {
if (dnsServer != nullptr)
dnsServer->processNextRequest();
auto httpServer = (HttpServer*)service;
if (httpServer != nullptr)
httpServer->loop();
}
#endif
}
static void onWifiConnected(const WiFiEventStationModeGotIP& event) {
PRINTF("connected (%s)\n", WiFi.localIP().toString().c_str());
wifi_state = WiFiConnectionState::JUST_CONNECTED;
}
static void onWifiDisconnected(const WiFiEventStationModeDisconnected& event) {
PRINTLN("disconnected from wi-fi");
wifi_state = WiFiConnectionState::WAITING;
if (service != nullptr)
((mqtt::Mqtt*)service)->disconnect();
wifiTimer.once(2, wifiConnect);
}
}

View File

@ -0,0 +1,48 @@
#ifndef HOMEKIT_LIB_MAIN_H
#define HOMEKIT_LIB_MAIN_H
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <DNSServer.h>
#include <Ticker.h>
#include <Wire.h>
#include <homekit/config.h>
#include <homekit/logging.h>
#ifndef CONFIG_TARGET_ESP01
#include <homekit/http_server.h>
#endif
#include <homekit/wifi.h>
#include <homekit/mqtt/mqtt.h>
#include <functional>
namespace homekit::main {
#ifndef CONFIG_TARGET_ESP01
enum class WorkingMode {
RECOVERY, // AP mode, http server with configuration
NORMAL, // MQTT client
};
extern enum WorkingMode working_mode;
#endif
enum class WiFiConnectionState {
WAITING = 0,
JUST_CONNECTED = 1,
CONNECTED = 2
};
struct LoopConfig {
std::function<void(mqtt::Mqtt&)> onMqttCreated;
};
void setup();
void loop(LoopConfig* config);
}
#endif //HOMEKIT_LIB_MAIN_H

View File

@ -0,0 +1,12 @@
{
"name": "homekit_main",
"version": "1.0.8",
"build": {
"flags": "-I../../include"
},
"dependencies": {
"homekit_mqtt_module_ota": "file://../common/libs/mqtt_module_ota",
"homekit_mqtt_module_diagnostics": "file://../common/libs/mqtt_module_diagnostics"
}
}

View File

@ -1,18 +0,0 @@
#include "./mqtt.h"
namespace homekit::mqtt {
const uint8_t MQTT_CA_FINGERPRINT[] = { \
0x0e, 0xb6, 0x3a, 0x02, 0x1f, \
0x4e, 0x1e, 0xe1, 0x6a, 0x67, \
0x62, 0xec, 0x64, 0xd4, 0x84, \
0x8a, 0xb0, 0xc9, 0x9c, 0xbb \
};;
const char MQTT_SERVER[] = "mqtt.solarmon.ru";
const uint16_t MQTT_PORT = 8883;
const char MQTT_USERNAME[] = CONFIG_MQTT_USERNAME;
const char MQTT_PASSWORD[] = CONFIG_MQTT_PASSWORD;
const char MQTT_CLIENT_ID[] = CONFIG_MQTT_CLIENT_ID;
const char MQTT_SECRET[CONFIG_NODE_SECRET_SIZE+1] = CONFIG_NODE_SECRET;
}

View File

@ -1,18 +0,0 @@
#ifndef COMMON_HOMEKIT_MQTT_H
#define COMMON_HOMEKIT_MQTT_H
#include <stdint.h>
namespace homekit::mqtt {
extern const uint8_t MQTT_CA_FINGERPRINT[];
extern const char MQTT_SERVER[];
extern const uint16_t MQTT_PORT;
extern const char MQTT_USERNAME[];
extern const char MQTT_PASSWORD[];
extern const char MQTT_CLIENT_ID[];
extern const char MQTT_SECRET[CONFIG_NODE_SECRET_SIZE+1];
}
#endif //COMMON_HOMEKIT_MQTT_H

View File

@ -0,0 +1,26 @@
#include "./module.h"
#include <homekit/logging.h>
namespace homekit::mqtt {
bool MqttModule::tickElapsed() {
if (!tickSw.elapsed(tickInterval*1000))
return false;
tickSw.save();
return true;
}
void MqttModule::handlePayload(Mqtt& mqtt, String& topic, uint16_t packetId, const uint8_t* payload, size_t length,
size_t index, size_t total) {
if (length != total)
PRINTLN("mqtt: received partial message, not supported");
// TODO
}
void MqttModule::handleOnPublish(uint16_t packetId) {}
void MqttModule::handleOnDisconnect(espMqttClientTypes::DisconnectReason reason) {}
}

View File

@ -0,0 +1,47 @@
#ifndef HOMEKIT_LIB_MQTT_MODULE_H
#define HOMEKIT_LIB_MQTT_MODULE_H
#include "./mqtt.h"
#include "./payload.h"
#include <homekit/stopwatch.h>
namespace homekit::mqtt {
class Mqtt;
class MqttModule {
protected:
bool initialized;
StopWatch tickSw;
short tickInterval;
bool receiveOnPublish;
bool receiveOnDisconnect;
bool tickElapsed();
public:
MqttModule(short _tickInterval, bool _receiveOnPublish = false, bool _receiveOnDisconnect = false)
: initialized(false)
, tickInterval(_tickInterval)
, receiveOnPublish(_receiveOnPublish)
, receiveOnDisconnect(_receiveOnDisconnect) {}
virtual void init(Mqtt& mqtt) = 0;
virtual void tick(Mqtt& mqtt) = 0;
virtual void handlePayload(Mqtt& mqtt, String& topic, uint16_t packetId, const uint8_t *payload, size_t length, size_t index, size_t total);
virtual void handleOnPublish(uint16_t packetId);
virtual void handleOnDisconnect(espMqttClientTypes::DisconnectReason reason);
inline void setInitialized() {
initialized = true;
}
friend class Mqtt;
};
}
#endif //HOMEKIT_LIB_MQTT_MODULE_H

View File

@ -0,0 +1,164 @@
#include "./mqtt.h"
#include <homekit/config.h>
#include <homekit/wifi.h>
#include <homekit/logging.h>
namespace homekit::mqtt {
const uint8_t MQTT_CA_FINGERPRINT[] = { \
0x0e, 0xb6, 0x3a, 0x02, 0x1f, \
0x4e, 0x1e, 0xe1, 0x6a, 0x67, \
0x62, 0xec, 0x64, 0xd4, 0x84, \
0x8a, 0xb0, 0xc9, 0x9c, 0xbb \
};;
const char MQTT_SERVER[] = "mqtt.solarmon.ru";
const uint16_t MQTT_PORT = 8883;
const char MQTT_USERNAME[] = CONFIG_MQTT_USERNAME;
const char MQTT_PASSWORD[] = CONFIG_MQTT_PASSWORD;
const char MQTT_CLIENT_ID[] = CONFIG_MQTT_CLIENT_ID;
const char MQTT_SECRET[CONFIG_NODE_SECRET_SIZE+1] = CONFIG_NODE_SECRET;
static const uint16_t MQTT_KEEPALIVE = 30;
using namespace espMqttClientTypes;
Mqtt::Mqtt() {
auto cfg = config::read();
homeId = String(cfg.flags.node_configured ? cfg.node_id : wifi::NODE_ID);
randomSeed(micros());
client.onConnect([&](bool sessionPresent) {
PRINTLN("mqtt: connected");
for (auto* module: modules) {
if (!module->initialized) {
module->init(*this);
module->setInitialized();
}
}
connected = true;
});
client.onDisconnect([&](DisconnectReason reason) {
PRINTF("mqtt: disconnected, reason=%d\n", static_cast<int>(reason));
#ifdef DEBUG
if (reason == DisconnectReason::TLS_BAD_FINGERPRINT)
PRINTLN("reason: bad fingerprint");
#endif
for (auto* module: modules) {
if (module->receiveOnDisconnect) {
module->handleOnDisconnect(reason);
}
}
// if (ota.readyToRestart) {
// restartTimer.once(1, restart);
// } else {
reconnectTimer.once(2, [&]() {
reconnect();
});
// }
});
client.onSubscribe([&](uint16_t packetId, const SubscribeReturncode* returncodes, size_t len) {
PRINTF("mqtt: subscribe ack, packet_id=%d\n", packetId);
for (size_t i = 0; i < len; i++) {
PRINTF(" return code: %u\n", static_cast<unsigned int>(*(returncodes+i)));
}
});
client.onUnsubscribe([&](uint16_t packetId) {
PRINTF("mqtt: unsubscribe ack, packet_id=%d\n", packetId);
});
client.onMessage([&](const MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) {
PRINTF("mqtt: message received, topic=%s, qos=%d, dup=%d, retain=%d, len=%ul, index=%ul, total=%ul\n",
topic, properties.qos, (int)properties.dup, (int)properties.retain, len, index, total);
const char *ptr = topic + homeId.length() + 10;
String relevantTopic(ptr);
auto it = moduleSubscriptions.find(relevantTopic);
if (it != moduleSubscriptions.end()) {
auto module = it->second;
module->handlePayload(*this, relevantTopic, properties.packetId, payload, len, index, total);
} else {
PRINTF("error: module subscription for topic %s not found\n", topic);
}
});
client.onPublish([&](uint16_t packetId) {
PRINTF("mqtt: publish ack, packet_id=%d\n", packetId);
for (auto* module: modules) {
if (module->receiveOnPublish) {
module->handleOnPublish(packetId);
}
}
});
client.setServer(MQTT_SERVER, MQTT_PORT);
client.setClientId(MQTT_CLIENT_ID);
client.setCredentials(MQTT_USERNAME, MQTT_PASSWORD);
client.setCleanSession(true);
client.setFingerprint(MQTT_CA_FINGERPRINT);
client.setKeepAlive(MQTT_KEEPALIVE);
}
void Mqtt::connect() {
reconnect();
}
void Mqtt::reconnect() {
if (client.connected()) {
PRINTLN("warning: already connected");
return;
}
client.connect();
}
void Mqtt::disconnect() {
// TODO test how this works???
reconnectTimer.detach();
client.disconnect();
}
void Mqtt::loop() {
client.loop();
for (auto& module: modules) {
module->tick(*this);
}
}
uint16_t Mqtt::publish(const String& topic, uint8_t* payload, size_t length) {
String fullTopic = "hk/" + homeId + "/temphum/" + topic;
return client.publish(fullTopic.c_str(), 1, false, payload, length);
}
uint16_t Mqtt::subscribe(const String& topic, uint8_t qos) {
String fullTopic = "hk/" + homeId + "/temphum/" + topic;
PRINTF("mqtt: subscribing to %s...\n", fullTopic.c_str());
uint16_t packetId = client.subscribe(fullTopic.c_str(), qos);
if (!packetId)
PRINTF("error: failed to subscribe to %s\n", fullTopic.c_str());
return packetId;
}
void Mqtt::addModule(MqttModule* module) {
modules.emplace_back(module);
if (connected) {
module->init(*this);
module->setInitialized();
}
}
void Mqtt::subscribeModule(String& topic, MqttModule* module) {
moduleSubscriptions[topic] = module;
}
}

View File

@ -0,0 +1,48 @@
#ifndef HOMEKIT_LIB_MQTT_H
#define HOMEKIT_LIB_MQTT_H
#include <vector>
#include <map>
#include <cstdint>
#include <espMqttClient.h>
#include <Ticker.h>
#include "./module.h"
namespace homekit::mqtt {
extern const uint8_t MQTT_CA_FINGERPRINT[];
extern const char MQTT_SERVER[];
extern const uint16_t MQTT_PORT;
extern const char MQTT_USERNAME[];
extern const char MQTT_PASSWORD[];
extern const char MQTT_CLIENT_ID[];
extern const char MQTT_SECRET[CONFIG_NODE_SECRET_SIZE+1];
class MqttModule;
class Mqtt {
private:
String homeId;
WiFiClientSecure httpsSecureClient;
espMqttClientSecure client;
Ticker reconnectTimer;
std::vector<MqttModule*> modules;
std::map<String, MqttModule*> moduleSubscriptions;
bool connected;
uint16_t subscribe(const String& topic, uint8_t qos = 0);
public:
Mqtt();
void connect();
void disconnect();
void reconnect();
void loop();
void addModule(MqttModule* module);
void subscribeModule(String& topic, MqttModule* module);
uint16_t publish(const String& topic, uint8_t* payload, size_t length);
};
}
#endif //HOMEKIT_LIB_MQTT_H

View File

@ -0,0 +1,15 @@
#ifndef HOMEKIT_MQTT_PAYLOAD_H
#define HOMEKIT_MQTT_PAYLOAD_H
#include <unistd.h>
namespace homekit::mqtt {
struct MqttPayload {
virtual ~MqttPayload() = default;
virtual size_t size() const = 0;
};
}
#endif

View File

@ -1,8 +1,7 @@
{ {
"name": "homekit_mqtt", "name": "homekit_mqtt",
"version": "1.0.2", "version": "1.0.8",
"build": { "build": {
"flags": "-I../../include" "flags": "-I../../include"
} }
} }

View File

@ -0,0 +1,47 @@
#include "./diagnostics.h"
#include <homekit/wifi.h>
#include <ESP8266WiFi.h>
namespace homekit::mqtt {
static const char TOPIC_DIAGNOSTICS[] = "stat";
static const char TOPIC_INITIAL_DIAGNOSTICS[] = "stat1";
void MqttDiagnosticsModule::init(Mqtt& mqtt) {}
void MqttDiagnosticsModule::tick(Mqtt& mqtt) {
if (!tickElapsed())
return;
auto cfg = config::read();
if (!initialSent) {
MqttInitialDiagnosticsPayload stat{
.ip = wifi::getIPAsInteger(),
.fw_version = CONFIG_FW_VERSION,
.rssi = wifi::getRSSI(),
.free_heap = ESP.getFreeHeap(),
.flags = DiagnosticsFlags{
.state = 1,
.config_changed_value_present = 1,
.config_changed = static_cast<uint8_t>(cfg.flags.node_configured ||
cfg.flags.wifi_configured ? 1 : 0)
}
};
mqtt.publish(TOPIC_INITIAL_DIAGNOSTICS, reinterpret_cast<uint8_t*>(&stat), sizeof(stat));
initialSent = true;
} else {
MqttDiagnosticsPayload stat{
.rssi = wifi::getRSSI(),
.free_heap = ESP.getFreeHeap(),
.flags = DiagnosticsFlags{
.state = 1,
.config_changed_value_present = 0,
.config_changed = 0
}
};
mqtt.publish(TOPIC_DIAGNOSTICS, reinterpret_cast<uint8_t*>(&stat), sizeof(stat));
}
}
}

View File

@ -0,0 +1,46 @@
#ifndef HOMEKIT_LIB_MQTT_MODULE_DIAGNOSTICS_H
#define HOMEKIT_LIB_MQTT_MODULE_DIAGNOSTICS_H
#include <stdint.h>
#include <homekit/mqtt/module.h>
namespace homekit::mqtt {
struct DiagnosticsFlags {
uint8_t state: 1;
uint8_t config_changed_value_present: 1;
uint8_t config_changed: 1;
uint8_t reserved: 5;
} __attribute__((packed));
struct MqttInitialDiagnosticsPayload {
uint32_t ip;
uint8_t fw_version;
int8_t rssi;
uint32_t free_heap;
DiagnosticsFlags flags;
} __attribute__((packed));
struct MqttDiagnosticsPayload {
int8_t rssi;
uint32_t free_heap;
DiagnosticsFlags flags;
} __attribute__((packed));
class MqttDiagnosticsModule: public MqttModule {
private:
bool initialSent;
public:
MqttDiagnosticsModule()
: MqttModule(30)
, initialSent(false) {}
void init(Mqtt& mqtt) override;
void tick(Mqtt& mqtt) override;
};
}
#endif //HOMEKIT_LIB_MQTT_MODULE_DIAGNOSTICS_H

View File

@ -0,0 +1,7 @@
{
"name": "homekit_mqtt_module_diagnostics",
"version": "1.0.0",
"build": {
"flags": "-I../../include"
}
}

View File

@ -0,0 +1,162 @@
#include "./ota.h"
#include <homekit/logging.h>
#include <homekit/util.h>
#include <homekit/led.h>
namespace homekit::mqtt {
using homekit::led::mcu_led;
#define MD5_SIZE 16
static const char TOPIC_OTA[] = "ota";
static const char TOPIC_OTA_RESPONSE[] = "otares";
void MqttOtaModule::init(Mqtt& mqtt) {
String topic(TOPIC_OTA);
mqtt.subscribeModule(topic, this);
}
void MqttOtaModule::tick(Mqtt& mqtt) {
if (!tickElapsed())
return;
}
void MqttOtaModule::handlePayload(Mqtt& mqtt, String& topic, uint16_t packetId, const uint8_t *payload, size_t length, size_t index, size_t total) {
char md5[33];
char* md5Ptr = md5;
if (index != 0 && ota.dataPacketId != packetId) {
PRINTLN("mqtt/ota: non-matching packet id");
return;
}
Update.runAsync(true);
if (index == 0) {
if (length < CONFIG_NODE_SECRET_SIZE + MD5_SIZE) {
PRINTLN("mqtt/ota: failed to check secret, first packet size is too small");
return;
}
if (memcmp((const char*)payload, CONFIG_NODE_SECRET, CONFIG_NODE_SECRET_SIZE) != 0) {
PRINTLN("mqtt/ota: invalid secret");
return;
}
PRINTF("mqtt/ota: starting update, total=%ul\n", total-CONFIG_NODE_SECRET_SIZE);
for (int i = 0; i < MD5_SIZE; i++) {
md5Ptr += sprintf(md5Ptr, "%02x", *((unsigned char*)(payload+CONFIG_NODE_SECRET_SIZE+i)));
}
md5[32] = '\0';
PRINTF("mqtt/ota: md5 is %s\n", md5);
PRINTF("mqtt/ota: first packet is %ul bytes length\n", length);
md5[32] = '\0';
if (Update.isRunning()) {
Update.end();
Update.clearError();
}
if (!Update.setMD5(md5)) {
PRINTLN("mqtt/ota: setMD5 failed");
return;
}
ota.dataPacketId = packetId;
if (!Update.begin(total - CONFIG_NODE_SECRET_SIZE - MD5_SIZE)) {
ota.clean();
#ifdef DEBUG
Update.printError(Serial);
#endif
sendResponse(mqtt, OtaResult::UPDATE_ERROR, Update.getError());
}
ota.written = Update.write(const_cast<uint8_t*>(payload)+CONFIG_NODE_SECRET_SIZE + MD5_SIZE, length-CONFIG_NODE_SECRET_SIZE - MD5_SIZE);
ota.written += CONFIG_NODE_SECRET_SIZE + MD5_SIZE;
mcu_led->blink(1, 1);
PRINTF("mqtt/ota: updating %u/%u\n", ota.written, Update.size());
} else {
if (!Update.isRunning()) {
PRINTLN("mqtt/ota: update is not running");
return;
}
if (index == ota.written) {
size_t written;
if ((written = Update.write(const_cast<uint8_t*>(payload), length)) != length) {
PRINTF("mqtt/ota: error: tried to write %ul bytes, write() returned %ul\n",
length, written);
ota.clean();
Update.end();
Update.clearError();
sendResponse(mqtt, OtaResult::WRITE_ERROR);
return;
}
ota.written += length;
mcu_led->blink(1, 1);
PRINTF("mqtt/ota: updating %u/%u\n",
ota.written - CONFIG_NODE_SECRET_SIZE - MD5_SIZE,
Update.size());
} else {
PRINTF("mqtt/ota: position is invalid, expected %ul, got %ul\n", ota.written, index);
ota.clean();
Update.end();
Update.clearError();
}
}
if (Update.isFinished()) {
ota.dataPacketId = 0;
if (Update.end()) {
ota.finished = true;
ota.publishResultPacketId = sendResponse(mqtt, OtaResult::OK);
PRINTF("mqtt/ota: ok, otares packet_id=%d\n", ota.publishResultPacketId);
} else {
ota.clean();
PRINTF("mqtt/ota: error: %u\n", Update.getError());
#ifdef DEBUG
Update.printError(Serial);
#endif
Update.clearError();
sendResponse(mqtt, OtaResult::UPDATE_ERROR, Update.getError());
}
}
}
uint16_t MqttOtaModule::sendResponse(Mqtt& mqtt, OtaResult status, uint8_t error_code) const {
MqttOtaResponsePayload resp{
.status = status,
.error_code = error_code
};
return mqtt.publish(TOPIC_OTA_RESPONSE, reinterpret_cast<uint8_t*>(&resp), sizeof(resp));
}
void MqttOtaModule::handleOnDisconnect(espMqttClientTypes::DisconnectReason reason) {
if (ota.started()) {
PRINTLN("mqtt: update was in progress, canceling..");
ota.clean();
Update.end();
Update.clearError();
}
if (ota.readyToRestart) {
restartTimer.once(1, restart);
}
}
void MqttOtaModule::handleOnPublish(uint16_t packetId) {
if (ota.finished && packetId == ota.publishResultPacketId) {
ota.readyToRestart = true;
}
}
}

View File

@ -0,0 +1,72 @@
#ifndef HOMEKIT_LIB_MQTT_MODULE_OTA_H
#define HOMEKIT_LIB_MQTT_MODULE_OTA_H
#include <stdint.h>
#include <Ticker.h>
#include <homekit/mqtt/module.h>
namespace homekit::mqtt {
enum class OtaResult: uint8_t {
OK = 0,
UPDATE_ERROR = 1,
WRITE_ERROR = 2,
};
struct OtaStatus {
uint16_t dataPacketId;
uint16_t publishResultPacketId;
bool finished;
bool readyToRestart;
size_t written;
OtaStatus()
: dataPacketId(0)
, publishResultPacketId(0)
, finished(false)
, readyToRestart(false)
, written(0)
{}
inline void clean() {
dataPacketId = 0;
publishResultPacketId = 0;
finished = false;
readyToRestart = false;
written = 0;
}
inline bool started() const {
return dataPacketId != 0;
}
};
struct MqttOtaResponsePayload {
OtaResult status;
uint8_t error_code;
} __attribute__((packed));
class MqttOtaModule: public MqttModule {
private:
OtaStatus ota;
Ticker restartTimer;
uint16_t sendResponse(Mqtt& mqtt, OtaResult status, uint8_t error_code = 0) const;
public:
MqttOtaModule() : MqttModule(0, true, true) {}
void init(Mqtt& mqtt) override;
void tick(Mqtt& mqtt) override;
void handlePayload(Mqtt& mqtt, String& topic, uint16_t packetId, const uint8_t *payload, size_t length, size_t index, size_t total) override;
void handleOnPublish(uint16_t packetId) override;
void handleOnDisconnect(espMqttClientTypes::DisconnectReason reason) override;
inline bool isReadyToRestart() const {
return ota.readyToRestart;
}
};
}
#endif //HOMEKIT_LIB_MQTT_MODULE_OTA_H

View File

@ -0,0 +1,10 @@
{
"name": "homekit_mqtt_module_ota",
"version": "1.0.1",
"build": {
"flags": "-I../../include"
},
"dependencies": {
"homekit_led": "file://../common/libs/led"
}
}

View File

@ -0,0 +1,23 @@
#include "temphum.h"
namespace homekit::mqtt {
static const char TOPIC_TEMPHUM_DATA[] = "data";
void MqttTemphumModule::init(Mqtt &mqtt) {}
void MqttTemphumModule::tick(homekit::mqtt::Mqtt& mqtt) {
if (!tickElapsed())
return;
temphum::SensorData sd = sensor->read();
MqttTemphumPayload payload {
.temp = sd.temp,
.rh = sd.rh,
.error = sd.error
};
mqtt.publish(TOPIC_TEMPHUM_DATA, reinterpret_cast<uint8_t*>(&payload), sizeof(payload));
}
}

View File

@ -0,0 +1,28 @@
#ifndef HOMEKIT_LIB_MQTT_MODULE_TEMPHUM_H
#define HOMEKIT_LIB_MQTT_MODULE_TEMPHUM_H
#include <homekit/mqtt/module.h>
#include <homekit/temphum.h>
namespace homekit::mqtt {
struct MqttTemphumPayload {
double temp = 0;
double rh = 0;
uint8_t error = 0;
} __attribute__((packed));
class MqttTemphumModule : public MqttModule {
private:
temphum::Sensor* sensor;
public:
MqttTemphumModule(temphum::Sensor* _sensor) : MqttModule(10), sensor(_sensor) {}
void init(Mqtt& mqtt) override;
void tick(Mqtt& mqtt) override;
};
}
#endif //HOMEKIT_LIB_MQTT_MODULE_TEMPHUM_H

View File

@ -0,0 +1,10 @@
{
"name": "homekit_mqtt_module_temphum",
"version": "1.0.8",
"build": {
"flags": "-I../../include"
},
"dependencies": {
"homekit_temphum": "file://../common/libs/temphum"
}
}

View File

@ -6,7 +6,7 @@
namespace homekit::temphum { namespace homekit::temphum {
void BaseSensor::setup() const { void Sensor::setup() const {
#ifndef CONFIG_TARGET_ESP01 #ifndef CONFIG_TARGET_ESP01
pinMode(CONFIG_SDA_GPIO, OUTPUT); pinMode(CONFIG_SDA_GPIO, OUTPUT);
pinMode(CONFIG_SCL_GPIO, OUTPUT); pinMode(CONFIG_SCL_GPIO, OUTPUT);
@ -17,7 +17,7 @@ void BaseSensor::setup() const {
#endif #endif
} }
void BaseSensor::writeCommand(int reg) const { void Sensor::writeCommand(int reg) const {
Wire.beginTransmission(dev_addr); Wire.beginTransmission(dev_addr);
Wire.write(reg); Wire.write(reg);
Wire.endTransmission(); Wire.endTransmission();
@ -25,25 +25,34 @@ void BaseSensor::writeCommand(int reg) const {
} }
SensorData Si7021::read() { SensorData Si7021::read() {
uint8_t error = 0;
writeCommand(0xf3); // command to measure temperature writeCommand(0xf3); // command to measure temperature
Wire.requestFrom(dev_addr, 2); Wire.requestFrom(dev_addr, 2);
if (Wire.available() < 2) {
PRINTLN("Si7021: 0xf3: could not read 2 bytes");
}
uint16_t temp_raw = Wire.read() << 8 | Wire.read(); uint16_t temp_raw = Wire.read() << 8 | Wire.read();
double temperature = ((175.72 * temp_raw) / 65536.0) - 46.85; double temperature = ((175.72 * temp_raw) / 65536.0) - 46.85;
writeCommand(0xf5); // command to measure humidity writeCommand(0xf5); // command to measure humidity
Wire.requestFrom(dev_addr, 2); Wire.requestFrom(dev_addr, 2);
if (Wire.available() < 2) {
PRINTLN("Si7021: 0xf5: could not read 2 bytes");
}
uint16_t hum_raw = Wire.read() << 8 | Wire.read(); uint16_t hum_raw = Wire.read() << 8 | Wire.read();
double humidity = ((125.0 * hum_raw) / 65536.0) - 6.0; double humidity = ((125.0 * hum_raw) / 65536.0) - 6.0;
return { return {
.temp = temperature, .error = error,
.rh = humidity .temp = temperature,
.rh = humidity
}; };
} }
SensorData DHT12::read() { SensorData DHT12::read() {
SensorData sd; SensorData sd;
byte raw[5]; byte raw[5];
sd.error = 1;
writeCommand(0); writeCommand(0);
Wire.requestFrom(dev_addr, 5); Wire.requestFrom(dev_addr, 5);
@ -69,6 +78,8 @@ SensorData DHT12::read() {
sd.rh = raw[0] + raw[1] * 0.1; sd.rh = raw[0] + raw[1] * 0.1;
sd.error = 0;
end: end:
return sd; return sd;
} }

View File

@ -5,33 +5,34 @@
namespace homekit::temphum { namespace homekit::temphum {
struct SensorData { struct SensorData {
uint8_t error = 0;
double temp = 0; // celsius double temp = 0; // celsius
double rh = 0; // relative humidity percentage double rh = 0; // relative humidity percentage
}; };
class BaseSensor { class Sensor {
protected: protected:
int dev_addr; int dev_addr;
public: public:
explicit BaseSensor(int dev) : dev_addr(dev) {} explicit Sensor(int dev) : dev_addr(dev) {}
void setup() const; void setup() const;
void writeCommand(int reg) const; void writeCommand(int reg) const;
virtual SensorData read() = 0; virtual SensorData read() = 0;
}; };
class Si7021 : public BaseSensor { class Si7021 : public Sensor {
public: public:
SensorData read() override; SensorData read() override;
Si7021() : BaseSensor(0x40) {} Si7021() : Sensor(0x40) {}
}; };
class DHT12 : public BaseSensor { class DHT12 : public Sensor {
public: public:
SensorData read() override; SensorData read() override;
DHT12() : BaseSensor(0x5c) {} DHT12() : Sensor(0x5c) {}
}; };
} }

View File

@ -0,0 +1,8 @@
{
"name": "homekit_temphum",
"version": "1.0.2",
"build": {
"flags": "-I../../include"
}
}

3
platformio/dumb_mqtt/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.pio
CMakeListsPrivate.txt
cmake-build-*/

View File

@ -0,0 +1,12 @@
#include <Arduino.h>
#include <homekit/main.h>
using namespace homekit;
void setup() {
main::setup();
}
void loop() {
main::loop(nullptr);
}

View File

@ -1,11 +0,0 @@
#include "leds.h"
namespace homekit {
#ifdef CONFIG_TARGET_NODEMCU
Led* board_led = new Led(CONFIG_BOARD_LED_GPIO);
#endif
Led* mcu_led = new Led(CONFIG_MCU_LED_GPIO);
}

View File

@ -1,15 +0,0 @@
#ifndef HOMEKIT_TEMPHUM_LEDS_H
#define HOMEKIT_TEMPHUM_LEDS_H
#include <homekit/led.h>
namespace homekit {
#ifdef CONFIG_TARGET_NODEMCU
extern Led* board_led;
#endif
extern Led* mcu_led;
}
#endif //HOMEKIT_TEMPHUM_LEDS_H

View File

@ -1,109 +1,26 @@
#include <Arduino.h> #include <Arduino.h>
#include <ESP8266WiFi.h>
#include <DNSServer.h>
#include <Ticker.h>
#include <Wire.h> #include <Wire.h>
#include <homekit/main.h>
#include <homekit/config.h> #include <homekit/mqtt/mqtt.h>
#include <homekit/logging.h> #include <homekit/mqtt/module/temphum.h>
#ifndef CONFIG_TARGET_ESP01 #include <homekit/temphum.h>
#include <homekit/http_server.h>
#endif
#include <homekit/wifi.h>
#include "mqtt.h"
#include "leds.h"
#include "temphum.h"
using namespace homekit; using namespace homekit;
using main::LoopConfig;
using mqtt::Mqtt;
using mqtt::MqttTemphumModule;
#ifndef CONFIG_TARGET_ESP01 temphum::Sensor* sensor = nullptr;
enum class WorkingMode { MqttTemphumModule* mqttTemphumModule = nullptr;
RECOVERY, // AP mode, http server with configuration
NORMAL, // MQTT client static void onMqttCreated(Mqtt& mqtt);
LoopConfig loopConfig = {
.onMqttCreated = onMqttCreated
}; };
static enum WorkingMode working_mode = WorkingMode::NORMAL;
static const uint16_t recovery_boot_detection_ms = 2000;
static const uint8_t recovery_boot_delay_ms = 100;
#endif
enum class WiFiConnectionState {
WAITING = 0,
JUST_CONNECTED = 1,
CONNECTED = 2
};
static volatile enum WiFiConnectionState wifi_state = WiFiConnectionState::WAITING;
static void* service = nullptr;
static WiFiEventHandler wifiConnectHandler, wifiDisconnectHandler;
static Ticker wifiTimer;
temphum::BaseSensor* sensor = nullptr;
#if MQTT_BLINK
static StopWatch blinkStopWatch;
#endif
#ifndef CONFIG_TARGET_ESP01
static DNSServer* dnsServer = nullptr;
#endif
static void onWifiConnected(const WiFiEventStationModeGotIP& event);
static void onWifiDisconnected(const WiFiEventStationModeDisconnected& event);
static void wifiConnect() {
const char *ssid, *psk, *hostname;
auto cfg = config::read();
wifi::getConfig(cfg, &ssid, &psk, &hostname);
PRINTF("Wi-Fi STA creds: ssid=%s, psk=%s, hostname=%s\n", ssid, psk, hostname);
wifi_state = WiFiConnectionState::WAITING;
WiFi.mode(WIFI_STA);
WiFi.hostname(hostname);
WiFi.begin(ssid, psk);
PRINT("connecting to wifi..");
}
#ifndef CONFIG_TARGET_ESP01
static void wifiHotspot() {
mcu_led->on();
auto scanResults = wifi::scan();
WiFi.mode(WIFI_AP);
WiFi.softAP(wifi::AP_SSID);
dnsServer = new DNSServer();
dnsServer->start(53, "*", WiFi.softAPIP());
service = new HttpServer(scanResults);
((HttpServer*)service)->start();
}
static void waitForRecoveryPress() {
pinMode(CONFIG_FLASH_GPIO, INPUT_PULLUP);
for (uint16_t i = 0; i < recovery_boot_detection_ms; i += recovery_boot_delay_ms) {
delay(recovery_boot_delay_ms);
if (digitalRead(CONFIG_FLASH_GPIO) == LOW) {
working_mode = WorkingMode::RECOVERY;
break;
}
}
}
#endif
void setup() { void setup() {
WiFi.disconnect(); main::setup();
#ifndef CONFIG_TARGET_ESP01
waitForRecoveryPress();
#endif
#ifdef DEBUG
Serial.begin(115200);
#endif
#if CONFIG_MODULE == HOMEKIT_SI7021 #if CONFIG_MODULE == HOMEKIT_SI7021
sensor = new temphum::Si7021(); sensor = new temphum::Si7021();
@ -111,111 +28,15 @@ void setup() {
sensor = new temphum::DHT12(); sensor = new temphum::DHT12();
#endif #endif
sensor->setup(); sensor->setup();
auto cfg = config::read();
if (config::isDirty(cfg)) {
PRINTLN("config is dirty, erasing...");
config::erase(cfg);
#ifdef CONFIG_TARGET_NODEMCU
board_led->blink(10, 50);
#else
mcu_led->blink(10, 50);
#endif
}
#ifndef CONFIG_TARGET_ESP01
switch (working_mode) {
case WorkingMode::RECOVERY:
wifiHotspot();
break;
case WorkingMode::NORMAL:
#endif
wifiConnectHandler = WiFi.onStationModeGotIP(onWifiConnected);
wifiDisconnectHandler = WiFi.onStationModeDisconnected(onWifiDisconnected);
wifiConnect();
#ifndef CONFIG_TARGET_ESP01
break;
}
#endif
} }
void loop() { void loop() {
#ifndef CONFIG_TARGET_ESP01 main::loop(&loopConfig);
if (working_mode == WorkingMode::NORMAL) { }
#endif
if (wifi_state == WiFiConnectionState::WAITING) {
PRINT(".");
mcu_led->blink(2, 50);
delay(1000);
return;
}
if (wifi_state == WiFiConnectionState::JUST_CONNECTED) { static void onMqttCreated(Mqtt& mqtt) {
#ifdef CONFIG_TARGET_NODEMCU if (mqttTemphumModule == nullptr) {
board_led->blink(3, 300); mqttTemphumModule = new MqttTemphumModule(sensor);
#endif mqtt.addModule(mqttTemphumModule);
wifi_state = WiFiConnectionState::CONNECTED;
if (service == nullptr)
service = new mqtt::MQTT();
((mqtt::MQTT*)service)->connect();
#if MQTT_BLINK
blinkStopWatch.save();
#endif
}
auto mqtt = (mqtt::MQTT*)service;
if (static_cast<int>(wifi_state) >= 1 && mqtt != nullptr) {
mqtt->loop();
if (mqtt->ota.readyToRestart) {
mqtt->disconnect();
} else if (mqtt->diagnosticsStopWatch.elapsed(10000)) {
mqtt->sendDiagnostics();
auto data = sensor->read();
PRINT("temp:");
PRINT(data.temp);
PRINT(", rh: ");
PRINTLN(data.rh);
mqtt->sendTempHumData(data.temp, data.rh);
}
#if MQTT_BLINK
// periodically blink board led
if (blinkStopWatch.elapsed(5000)) {
#ifdef CONFIG_TARGET_NODEMCU
board_led->blink(1, 10);
#endif
blinkStopWatch.save();
}
#endif
}
#ifndef CONFIG_TARGET_ESP01
} else {
if (dnsServer != nullptr)
dnsServer->processNextRequest();
auto httpServer = (HttpServer*)service;
if (httpServer != nullptr)
httpServer->loop();
} }
#endif
}
static void onWifiConnected(const WiFiEventStationModeGotIP& event) {
PRINTF("connected (%s)\n", WiFi.localIP().toString().c_str());
wifi_state = WiFiConnectionState::JUST_CONNECTED;
}
static void onWifiDisconnected(const WiFiEventStationModeDisconnected& event) {
PRINTLN("disconnected from wi-fi");
wifi_state = WiFiConnectionState::WAITING;
if (service != nullptr)
((mqtt::MQTT*)service)->disconnect();
wifiTimer.once(2, wifiConnect);
} }

View File

@ -1,316 +0,0 @@
#include <ESP8266httpUpdate.h>
#include <homekit/logging.h>
#include <homekit/config.h>
#include <homekit/util.h>
#include <homekit/wifi.h>
#include "mqtt.h"
#include "leds.h"
namespace homekit::mqtt {
static const char TOPIC_DIAGNOSTICS[] = "stat";
static const char TOPIC_INITIAL_DIAGNOSTICS[] = "stat1";
static const char TOPIC_OTA_RESPONSE[] = "otares";
static const char TOPIC_TEMPHUM_DATA[] = "data";
static const char TOPIC_ADMIN_OTA[] = "admin/ota";
static const uint16_t MQTT_KEEPALIVE = 30;
enum class IncomingMessage {
UNKNOWN,
OTA
};
using namespace espMqttClientTypes;
#define MD5_SIZE 16
MQTT::MQTT() {
auto cfg = config::read();
homeId = String(cfg.flags.node_configured ? cfg.node_id : wifi::NODE_ID);
randomSeed(micros());
client.onConnect([&](bool sessionPresent) {
PRINTLN("mqtt: connected");
sendInitialDiagnostics();
subscribe(TOPIC_ADMIN_OTA);
});
client.onDisconnect([&](DisconnectReason reason) {
PRINTF("mqtt: disconnected, reason=%d\n", static_cast<int>(reason));
#ifdef DEBUG
if (reason == DisconnectReason::TLS_BAD_FINGERPRINT)
PRINTLN("reason: bad fingerprint");
#endif
if (ota.started()) {
PRINTLN("mqtt: update was in progress, canceling..");
ota.clean();
Update.end();
Update.clearError();
}
if (ota.readyToRestart) {
restartTimer.once(1, restart);
} else {
reconnectTimer.once(2, [&]() {
reconnect();
});
}
});
client.onSubscribe([&](uint16_t packetId, const SubscribeReturncode* returncodes, size_t len) {
PRINTF("mqtt: subscribe ack, packet_id=%d\n", packetId);
for (size_t i = 0; i < len; i++) {
PRINTF(" return code: %u\n", static_cast<unsigned int>(*(returncodes+i)));
}
});
client.onUnsubscribe([&](uint16_t packetId) {
PRINTF("mqtt: unsubscribe ack, packet_id=%d\n", packetId);
});
client.onMessage([&](const MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) {
PRINTF("mqtt: message received, topic=%s, qos=%d, dup=%d, retain=%d, len=%ul, index=%ul, total=%ul\n",
topic, properties.qos, (int)properties.dup, (int)properties.retain, len, index, total);
IncomingMessage msgType = IncomingMessage::UNKNOWN;
const char *ptr = topic + homeId.length() + 10;
String relevantTopic(ptr);
if (relevantTopic == TOPIC_ADMIN_OTA)
msgType = IncomingMessage::OTA;
if (len != total && msgType != IncomingMessage::OTA) {
PRINTLN("mqtt: received partial message, not supported");
return;
}
switch (msgType) {
case IncomingMessage::OTA:
if (ota.finished)
break;
handleAdminOtaPayload(properties.packetId, payload, len, index, total);
break;
case IncomingMessage::UNKNOWN:
PRINTF("error: invalid topic %s\n", topic);
break;
}
});
client.onPublish([&](uint16_t packetId) {
PRINTF("mqtt: publish ack, packet_id=%d\n", packetId);
if (ota.finished && packetId == ota.publishResultPacketId) {
ota.readyToRestart = true;
}
});
client.setServer(MQTT_SERVER, MQTT_PORT);
client.setClientId(MQTT_CLIENT_ID);
client.setCredentials(MQTT_USERNAME, MQTT_PASSWORD);
client.setCleanSession(true);
client.setFingerprint(MQTT_CA_FINGERPRINT);
client.setKeepAlive(MQTT_KEEPALIVE);
}
void MQTT::connect() {
reconnect();
}
void MQTT::reconnect() {
if (client.connected()) {
PRINTLN("warning: already connected");
return;
}
client.connect();
}
void MQTT::disconnect() {
// TODO test how this works???
reconnectTimer.detach();
client.disconnect();
}
uint16_t MQTT::publish(const String &topic, uint8_t *payload, size_t length) {
String fullTopic = "hk/" + homeId + "/temphum/" + topic;
return client.publish(fullTopic.c_str(), 1, false, payload, length);
}
void MQTT::loop() {
client.loop();
}
uint16_t MQTT::subscribe(const String &topic, uint8_t qos) {
String fullTopic = "hk/" + homeId + "/temphum/" + topic;
PRINTF("mqtt: subscribing to %s...\n", fullTopic.c_str());
uint16_t packetId = client.subscribe(fullTopic.c_str(), qos);
if (!packetId)
PRINTF("error: failed to subscribe to %s\n", fullTopic.c_str());
return packetId;
}
void MQTT::sendInitialDiagnostics() {
auto cfg = config::read();
InitialDiagnosticsPayload stat{
.ip = wifi::getIPAsInteger(),
.fw_version = CONFIG_FW_VERSION,
.rssi = wifi::getRSSI(),
.free_heap = ESP.getFreeHeap(),
.flags = DiagnosticsFlags{
.state = 1,
.config_changed_value_present = 1,
.config_changed = static_cast<uint8_t>(cfg.flags.node_configured ||
cfg.flags.wifi_configured ? 1 : 0)
}
};
publish(TOPIC_INITIAL_DIAGNOSTICS, reinterpret_cast<uint8_t*>(&stat), sizeof(stat));
diagnosticsStopWatch.save();
}
void MQTT::sendDiagnostics() {
DiagnosticsPayload stat{
.rssi = wifi::getRSSI(),
.free_heap = ESP.getFreeHeap(),
.flags = DiagnosticsFlags{
.state = 1,
.config_changed_value_present = 0,
.config_changed = 0
}
};
publish(TOPIC_DIAGNOSTICS, reinterpret_cast<uint8_t*>(&stat), sizeof(stat));
diagnosticsStopWatch.save();
}
void MQTT::sendTempHumData(double temp, double rh) {
TempHumDataPayload data {
.temp = temp,
.rh = rh
};
publish(TOPIC_TEMPHUM_DATA, reinterpret_cast<uint8_t*>(&data), sizeof(data));
}
uint16_t MQTT::sendOtaResponse(OTAResult status, uint8_t error_code) {
OTAResponse resp{
.status = status,
.error_code = error_code
};
return publish(TOPIC_OTA_RESPONSE, reinterpret_cast<uint8_t*>(&resp), sizeof(resp));
}
void MQTT::handleAdminOtaPayload(uint16_t packetId, const uint8_t *payload, size_t length, size_t index, size_t total) {
char md5[33];
char* md5Ptr = md5;
if (index != 0 && ota.dataPacketId != packetId) {
PRINTLN("mqtt/ota: non-matching packet id");
return;
}
Update.runAsync(true);
if (index == 0) {
if (length < CONFIG_NODE_SECRET_SIZE + MD5_SIZE) {
PRINTLN("mqtt/ota: failed to check secret, first packet size is too small");
return;
}
if (memcmp((const char*)payload, CONFIG_NODE_SECRET, CONFIG_NODE_SECRET_SIZE) != 0) {
PRINTLN("mqtt/ota: invalid secret");
return;
}
PRINTF("mqtt/ota: starting update, total=%ul\n", total-CONFIG_NODE_SECRET_SIZE);
for (int i = 0; i < MD5_SIZE; i++) {
md5Ptr += sprintf(md5Ptr, "%02x", *((unsigned char*)(payload+CONFIG_NODE_SECRET_SIZE+i)));
}
md5[32] = '\0';
PRINTF("mqtt/ota: md5 is %s\n", md5);
PRINTF("mqtt/ota: first packet is %ul bytes length\n", length);
md5[32] = '\0';
if (Update.isRunning()) {
Update.end();
Update.clearError();
}
if (!Update.setMD5(md5)) {
PRINTLN("mqtt/ota: setMD5 failed");
return;
}
ota.dataPacketId = packetId;
if (!Update.begin(total - CONFIG_NODE_SECRET_SIZE - MD5_SIZE)) {
ota.clean();
#ifdef DEBUG
Update.printError(Serial);
#endif
sendOtaResponse(OTAResult::UPDATE_ERROR, Update.getError());
}
ota.written = Update.write(const_cast<uint8_t*>(payload)+CONFIG_NODE_SECRET_SIZE + MD5_SIZE, length-CONFIG_NODE_SECRET_SIZE - MD5_SIZE);
ota.written += CONFIG_NODE_SECRET_SIZE + MD5_SIZE;
mcu_led->blink(1, 1);
PRINTF("mqtt/ota: updating %u/%u\n", ota.written, Update.size());
} else {
if (!Update.isRunning()) {
PRINTLN("mqtt/ota: update is not running");
return;
}
if (index == ota.written) {
size_t written;
if ((written = Update.write(const_cast<uint8_t*>(payload), length)) != length) {
PRINTF("mqtt/ota: error: tried to write %ul bytes, write() returned %ul\n",
length, written);
ota.clean();
Update.end();
Update.clearError();
sendOtaResponse(OTAResult::WRITE_ERROR);
return;
}
ota.written += length;
mcu_led->blink(1, 1);
PRINTF("mqtt/ota: updating %u/%u\n",
ota.written - CONFIG_NODE_SECRET_SIZE - MD5_SIZE,
Update.size());
} else {
PRINTF("mqtt/ota: position is invalid, expected %ul, got %ul\n", ota.written, index);
ota.clean();
Update.end();
Update.clearError();
}
}
if (Update.isFinished()) {
ota.dataPacketId = 0;
if (Update.end()) {
ota.finished = true;
ota.publishResultPacketId = sendOtaResponse(OTAResult::OK);
PRINTF("mqtt/ota: ok, otares packet_id=%d\n", ota.publishResultPacketId);
} else {
ota.clean();
PRINTF("mqtt/ota: error: %u\n", Update.getError());
#ifdef DEBUG
Update.printError(Serial);
#endif
Update.clearError();
sendOtaResponse(OTAResult::UPDATE_ERROR, Update.getError());
}
}
}
}

View File

@ -1,114 +0,0 @@
#ifndef HOMEKIT_TEMPHUM_MQTT_H
#define HOMEKIT_TEMPHUM_MQTT_H
#include <ESP8266WiFi.h>
#include <espMqttClient.h>
#include <Ticker.h>
#include <homekit/stopwatch.h>
#include <homekit/mqtt.h>
namespace homekit { namespace mqtt {
enum class OTAResult: uint8_t {
OK = 0,
UPDATE_ERROR = 1,
WRITE_ERROR = 2,
};
struct OTAStatus {
uint16_t dataPacketId;
uint16_t publishResultPacketId;
bool finished;
bool readyToRestart;
size_t written;
OTAStatus()
: dataPacketId(0)
, publishResultPacketId(0)
, finished(false)
, readyToRestart(false)
, written(0)
{}
inline void clean() {
dataPacketId = 0;
publishResultPacketId = 0;
finished = false;
readyToRestart = false;
written = 0;
}
inline bool started() const {
return dataPacketId != 0;
}
};
class MQTT {
private:
String homeId;
WiFiClientSecure httpsSecureClient;
espMqttClientSecure client;
Ticker reconnectTimer;
Ticker restartTimer;
void handleAdminOtaPayload(uint16_t packetId, const uint8_t* payload, size_t length, size_t index, size_t total);
uint16_t publish(const String& topic, uint8_t* payload, size_t length);
uint16_t subscribe(const String& topic, uint8_t qos = 0);
void sendInitialDiagnostics();
uint16_t sendOtaResponse(OTAResult status, uint8_t error_code = 0);
public:
StopWatch diagnosticsStopWatch;
OTAStatus ota;
MQTT();
void connect();
void disconnect();
void reconnect();
void loop();
void sendDiagnostics();
void sendTempHumData(double temp, double rh);
};
struct DiagnosticsFlags {
uint8_t state: 1;
uint8_t config_changed_value_present: 1;
uint8_t config_changed: 1;
uint8_t reserved: 5;
} __attribute__((packed));
struct InitialDiagnosticsPayload {
uint32_t ip;
uint8_t fw_version;
int8_t rssi;
uint32_t free_heap;
DiagnosticsFlags flags;
} __attribute__((packed));
struct DiagnosticsPayload {
int8_t rssi;
uint32_t free_heap;
DiagnosticsFlags flags;
} __attribute__((packed));
struct PowerPayload {
char secret[12];
uint8_t state;
} __attribute__((packed));
struct TempHumDataPayload {
double temp;
double rh;
} __attribute__((packed));
struct OTAResponse {
OTAResult status;
uint8_t error_code;
} __attribute__((packed));
} }
#endif //HOMEKIT_TEMPHUM_MQTT_H

View File

@ -4,7 +4,7 @@ two_digits_precision = lambda x: round(x, 2)
class TempHumDataPayload(MqttPayload): class TempHumDataPayload(MqttPayload):
FORMAT = '=dd' FORMAT = '=ddb'
UNPACKER = { UNPACKER = {
'temp': two_digits_precision, 'temp': two_digits_precision,
'rh': two_digits_precision 'rh': two_digits_precision
@ -12,3 +12,4 @@ class TempHumDataPayload(MqttPayload):
temp: float temp: float
rh: float rh: float
error: int