Skip to content

Commit fb789e0

Browse files
authored
More extensive MQTT + OTA support (#266)
Extend existing MQTT demo code: * Add debug info (IP and MQTT connection status) to the ESP32 display * Adjust MQTT home assistant auto config, add availability topic and last will * Add support for ArduinoOTA uploads (only available via the MQTT task for now; not available for the HTTP demo)
1 parent fa79c56 commit fb789e0

8 files changed

Lines changed: 189 additions & 23 deletions

File tree

firmware/esp32/core/splitflap_task.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -373,11 +373,12 @@ void SplitflapTask::log(const char* msg) {
373373
}
374374
}
375375

376-
void SplitflapTask::showString(const char* str, uint8_t length, bool force_full_rotation) {
376+
void SplitflapTask::showString(const char* str, uint8_t length, bool force_full_rotation, bool default_unspecified_home) {
377377
Command command = {};
378378
command.command_type = CommandType::MODULES;
379-
for (uint8_t i = 0; i < length && i < NUM_MODULES; i++) {
380-
int8_t index = findFlapIndex(str[i]);
379+
uint8_t num_to_update = default_unspecified_home ? NUM_MODULES : length;
380+
for (uint8_t i = 0; i < num_to_update && i < NUM_MODULES; i++) {
381+
int8_t index = i >= length ? 0 : findFlapIndex(str[i]);
381382
if (index != -1) {
382383
if (force_full_rotation || index != modules[i]->GetTargetFlapIndex()) {
383384
command.data.module_command[i] = QCMD_FLAP + index;

firmware/esp32/core/splitflap_task.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ class SplitflapTask : public Task<SplitflapTask> {
129129

130130
SplitflapState getState();
131131

132-
void showString(const char *str, uint8_t length, bool force_full_rotation = FORCE_FULL_ROTATION);
132+
void showString(const char *str, uint8_t length, bool force_full_rotation = FORCE_FULL_ROTATION, bool default_unspecified_home = false);
133133
void resetAll();
134134
void disableAll();
135135
void setLed(uint8_t id, bool on);

firmware/esp32/splitflap/display_layouts.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
#include <stdint.h>
1919

2020
// Customize these settings and select a layout algorithm at the bottom if you have a different arrangement of modules:
21-
#define DISPLAY_COLUMNS 6
21+
#define DISPLAY_COLUMNS (NUM_MODULES)
2222

2323

2424
// EXAMPLE LAYOUT ALGORITHMS:

firmware/esp32/splitflap/main.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ BaseSupervisorTask baseSupervisorTask(splitflapTask, serialTask, 0);
4242

4343
#if MQTT
4444
#include "mqtt_task.h"
45-
MQTTTask mqttTask(splitflapTask, serialTask, 0);
45+
MQTTTask mqttTask(splitflapTask, displayTask, serialTask, 0);
4646
#endif
4747

4848
#if HTTP

firmware/esp32/splitflap/mqtt_task.cpp

Lines changed: 139 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,25 @@
1414
limitations under the License.
1515
*/
1616
#if MQTT
17+
#include <ArduinoOTA.h>
18+
1719
#include "mqtt_task.h"
1820
#include "secrets.h"
1921

22+
using namespace json11;
23+
24+
// This MQTT demo assumes a home assistant MQTT instance, and will do automatic registration
25+
// via the home assistant config topic when it comes online and connects to MQTT. You shouldn't
26+
// need to change any of this to get it to work with home assistant.
27+
#define MQTT_CONFIG_TOPIC "homeassistant/text/" DEVICE_INSTANCE_NAME "/config"
28+
#define MQTT_COMMAND_TOPIC "home/" DEVICE_INSTANCE_NAME "/command"
29+
#define MQTT_STATE_TOPIC "home/" DEVICE_INSTANCE_NAME "/state"
30+
#define MQTT_AVAILABILITY_TOPIC "home/" DEVICE_INSTANCE_NAME "/availability"
2031

21-
MQTTTask::MQTTTask(SplitflapTask& splitflap_task, Logger& logger, const uint8_t task_core) :
32+
MQTTTask::MQTTTask(SplitflapTask& splitflap_task, DisplayTask& display_task, Logger& logger, const uint8_t task_core) :
2233
Task("MQTT", 8192, 1, task_core),
2334
splitflap_task_(splitflap_task),
35+
display_task_(display_task),
2436
logger_(logger),
2537
wifi_client_(),
2638
mqtt_client_(wifi_client_) {
@@ -30,56 +42,173 @@ MQTTTask::MQTTTask(SplitflapTask& splitflap_task, Logger& logger, const uint8_t
3042

3143
void MQTTTask::connectWifi() {
3244
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
33-
3445
// Disable WiFi sleep as it causes glitches on pin 39; see https://github.com/espressif/arduino-esp32/issues/4903#issuecomment-793187707
3546
WiFi.setSleep(WIFI_PS_NONE);
3647

48+
char buf[256];
49+
50+
snprintf(buf, sizeof(buf), "Wifi connecting to %s", WIFI_SSID);
51+
display_task_.setMessage(0, String(buf));
52+
3753
while (WiFi.status() != WL_CONNECTED) {
3854
delay(1000);
3955
logger_.log("Establishing connection to WiFi..");
4056
}
4157

42-
char buf[256];
4358
snprintf(buf, sizeof(buf), "Connected to network %s", WIFI_SSID);
4459
logger_.log(buf);
60+
61+
snprintf(buf, sizeof(buf), "Wifi IP: %s", WiFi.localIP().toString().c_str());
62+
display_task_.setMessage(0, String(buf));
4563
}
4664

4765
void MQTTTask::mqttCallback(char *topic, byte *payload, unsigned int length) {
4866
char buf[256];
4967
snprintf(buf, sizeof(buf), "Received mqtt callback for topic %s, length %u", topic, length);
5068
logger_.log(buf);
51-
splitflap_task_.showString((const char *)payload, length);
69+
70+
71+
splitflap_task_.showString((const char *)payload, length, false, true);
5272
}
5373

5474
void MQTTTask::connectMQTT() {
55-
char buf[256];
56-
mqtt_client_.setServer(MQTT_SERVER, 1883);
75+
char buf[400];
76+
mqtt_client_.setServer(MQTT_SERVER, MQTT_PORT);
5777
logger_.log("Attempting MQTT connection...");
58-
if (mqtt_client_.connect(HOSTNAME "-" MQTT_USER, MQTT_USER, MQTT_PASSWORD)) {
78+
snprintf(buf, sizeof(buf), "MQTT connecting to %s:%d", MQTT_SERVER, MQTT_PORT);
79+
display_task_.setMessage(1, String(buf));
80+
81+
if (mqtt_client_.connect(DEVICE_INSTANCE_NAME, MQTT_USER, MQTT_PASSWORD, MQTT_AVAILABILITY_TOPIC, 1, true, "offline")) {
5982
logger_.log("MQTT connected");
6083
mqtt_client_.subscribe(MQTT_COMMAND_TOPIC);
61-
char buf[256];
62-
snprintf(buf, sizeof(buf), "{\"name\": \"%s\", \"command_topic\": \"%s\", \"state_topic\": \"%s\", \"unique_id\": \"%s\"}", HOSTNAME, MQTT_COMMAND_TOPIC, MQTT_COMMAND_TOPIC, HOSTNAME);
63-
mqtt_client_.publish("homeassistant/text/splitflap/config", buf);
84+
85+
// TODO: I believe it's possible to do more complex config to register as a device with multiple
86+
// entities; it'd be great to explore additional entities like a display backlight control,
87+
// detailed module status info, etc. Though at a certain point it may be preferable to make the
88+
// splitflap library an ESPHome external component for even more options, easier programming,
89+
// and more customization than MQTT offers...
90+
Json config = Json::object {
91+
{ "name", DEVICE_INSTANCE_NAME },
92+
{ "command_topic", MQTT_COMMAND_TOPIC },
93+
{ "state_topic", MQTT_STATE_TOPIC },
94+
{ "availability_topic", MQTT_AVAILABILITY_TOPIC },
95+
{ "payload_available", "online" },
96+
{ "payload_not_available", "offline" },
97+
{ "unique_id", DEVICE_INSTANCE_NAME },
98+
{ "max", NUM_MODULES },
99+
};
100+
std::string json_str = config.dump();
101+
boolean result = mqtt_client_.publish(MQTT_CONFIG_TOPIC, json_str.c_str());
102+
snprintf(buf, sizeof(buf), "Result of publish: %d", result);
103+
logger_.log(buf);
64104
logger_.log("Published MQTT discovery message");
105+
logger_.log(json_str.c_str());
106+
107+
mqtt_client_.publish(MQTT_AVAILABILITY_TOPIC, "online", true);
108+
109+
snprintf(buf, sizeof(buf), "MQTT connected! (%s:%d)", MQTT_SERVER, MQTT_PORT);
110+
display_task_.setMessage(1, String(buf));
65111
} else {
66112
snprintf(buf, sizeof(buf), "MQTT failed rc=%d will try again in 5 seconds", mqtt_client_.state());
67113
logger_.log(buf);
114+
115+
snprintf(buf, sizeof(buf), "MQTT failed rc=%d", mqtt_client_.state());
116+
display_task_.setMessage(1, String(buf));
68117
}
69118
}
70119

71120
void MQTTTask::run() {
121+
char buf[256];
122+
display_task_.setMessage(0, "");
123+
display_task_.setMessage(1, "");
72124
connectWifi();
73125
connectMQTT();
74126

127+
ArduinoOTA
128+
.onStart([this]() {
129+
if (ArduinoOTA.getCommand() == U_FLASH) {
130+
logger_.log("Start OTA (flash)");
131+
} else { // U_SPIFFS
132+
logger_.log("Start OTA (filesystem)");
133+
// NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
134+
}
135+
})
136+
.onEnd([this]() {
137+
logger_.log("OTA End");
138+
})
139+
.onProgress([this](unsigned int progress, unsigned int total) {
140+
char buf2[256];
141+
static uint32_t last_progress;
142+
if (millis() - last_progress > 1000) {
143+
snprintf(buf2, sizeof(buf2), "OTA Progress: %d%%", (int)(progress * 100 / total));
144+
logger_.log(buf2);
145+
last_progress = millis();
146+
}
147+
})
148+
.onError([this](ota_error_t error) {
149+
char buf2[256];
150+
snprintf(buf2, sizeof(buf2), "OTA Error: %u", error);
151+
logger_.log(buf2);
152+
if (error == OTA_AUTH_ERROR) logger_.log("Auth Failed");
153+
else if (error == OTA_BEGIN_ERROR) logger_.log("Begin Failed");
154+
else if (error == OTA_CONNECT_ERROR) logger_.log("Connect Failed");
155+
else if (error == OTA_RECEIVE_ERROR) logger_.log("Receive Failed");
156+
else if (error == OTA_END_ERROR) logger_.log("End Failed");
157+
})
158+
.setHostname(DEVICE_INSTANCE_NAME)
159+
.setPassword(OTA_PASSWORD)
160+
.begin();
161+
162+
wl_status_t wifi_last_status = WL_DISCONNECTED;
163+
uint32_t last_state_publish = 0;
164+
SplitflapState last_state = {};
165+
uint32_t last_availability_publish = 0;
75166
while(1) {
76167
long now = millis();
168+
wl_status_t wifi_new_status = WiFi.status();
169+
if (wifi_new_status != wifi_last_status) {
170+
if (wifi_new_status == WL_CONNECTED) {
171+
snprintf(buf, sizeof(buf), "Wifi IP: %s", WiFi.localIP().toString().c_str());
172+
display_task_.setMessage(0, String(buf));
173+
} else {
174+
snprintf(buf, sizeof(buf), "Wifi connecting to %s", WIFI_SSID);
175+
display_task_.setMessage(0, String(buf));
176+
}
177+
wifi_last_status = wifi_new_status;
178+
}
77179
if (!mqtt_client_.connected() && (now - mqtt_last_connect_time_) > 5000) {
78180
logger_.log("Reconnecting MQTT");
79181
mqtt_last_connect_time_ = now;
80182
connectMQTT();
81183
}
184+
if (mqtt_client_.connected()) {
185+
SplitflapState state = splitflap_task_.getState();
186+
if (state != last_state) {
187+
char flap_buf[NUM_MODULES+1];
188+
bool all_idle = true;
189+
for (uint8_t i = 0; i < NUM_MODULES; i++) {
190+
flap_buf[i] = flaps[state.modules[i].flap_index];
191+
if (state.modules[i].moving) {
192+
all_idle = false;
193+
}
194+
}
195+
flap_buf[NUM_MODULES] = 0;
196+
197+
if (all_idle && (now - last_state_publish) > 200) {
198+
last_state = state;
199+
last_state_publish = now;
200+
snprintf(buf, sizeof(buf), "Publishing state: %s", flap_buf);
201+
logger_.log(buf);
202+
mqtt_client_.publish(MQTT_STATE_TOPIC, flap_buf);
203+
}
204+
}
205+
if (now > last_availability_publish + 1800000) {
206+
mqtt_client_.publish(MQTT_AVAILABILITY_TOPIC, "online", true);
207+
last_availability_publish = now;
208+
}
209+
}
82210
mqtt_client_.loop();
211+
ArduinoOTA.handle();
83212
delay(1);
84213
}
85214
}

firmware/esp32/splitflap/mqtt_task.h

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,29 @@
1616
#pragma once
1717

1818
#include <Arduino.h>
19+
#include <PubSubClient.h>
20+
#include <WiFi.h>
21+
22+
#include <json11.hpp>
1923

2024
#include "../core/logger.h"
2125
#include "../core/splitflap_task.h"
2226
#include "../core/task.h"
2327

24-
#include <PubSubClient.h>
25-
#include <WiFi.h>
26-
28+
#include "display_task.h"
2729

2830
class MQTTTask : public Task<MQTTTask> {
2931
friend class Task<MQTTTask>; // Allow base Task to invoke protected run()
3032

3133
public:
32-
MQTTTask(SplitflapTask& splitflapTask, Logger& logger, const uint8_t taskCore);
34+
MQTTTask(SplitflapTask& splitflapTask, DisplayTask& DisplayTask, Logger& logger, const uint8_t taskCore);
3335

3436
protected:
3537
void run();
3638

3739
private:
3840
SplitflapTask& splitflap_task_;
41+
DisplayTask& display_task_;
3942
Logger& logger_;
4043
WiFiClient wifi_client_;
4144
PubSubClient mqtt_client_;

firmware/esp32/splitflap/secrets.h.example

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,29 @@
44
#define WIFI_SSID "myssid"
55
#define WIFI_PASSWORD "supersecretpassword"
66

7-
#define HOSTNAME "splitflap" // e.g. splitflap.local
7+
// Hostname for this device, which is also used for MDNS, e.g. "splitflap" will result
8+
// in an mdns name of "splitflap.local"
9+
// If you plan to use OTA uploads, make sure to update the "upload_port" in the "env:chainlink_ota"
10+
// section of platformio.ini to match.
11+
#define DEVICE_INSTANCE_NAME "splitflap"
812

9-
#define MQTT_SERVER "10.0.0.2"
13+
#define MQTT_SERVER "192.168.0.242"
14+
#define MQTT_PORT (1883)
1015
#define MQTT_USER "mqttuser"
1116
#define MQTT_PASSWORD "megasecretpassword"
12-
#define MQTT_COMMAND_TOPIC "homeassistant/text/splitflap/state"
17+
18+
// Password for wireless firmware uploads (only works with MQTT=1 in build_flags!).
19+
// To use OTA functionality:
20+
// Setup:
21+
// 1. Change this value to something unique in your secrets.h file
22+
// 2. Upload the firmware via USB using the "chainlink" env in platformio - this must be
23+
// done via USB first to install the initial firmware, including this password for
24+
// future wifi uploads.
25+
// 3. If you've changed the DEVICE_INSTANCE_NAME, make sure to update the "upload_port" in
26+
// the "env:chainlink_ota" section of platformio.ini to match, so the OTA uploads know
27+
// who to talk to.
28+
// OTA updates:
29+
// 1. Make sure the ESP32 is online
30+
// 2. Upload the firmware using the "chainlink_ota" environment instead of the "chainlink"
31+
// environment
32+
#define OTA_PASSWORD "replace_me!"

platformio.ini

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ build_flags =
9292

9393
; Set to true to enable MQTT support (see secrets.h.example for configuration)
9494
-DMQTT=false
95+
-DMQTT_MAX_PACKET_SIZE=512
9596

9697
; Set to true to enable HTTP support (see secrets.h.example for configuration)
9798
-DHTTP=false
@@ -122,6 +123,18 @@ build_flags =
122123
-DCHAINLINK
123124
-DNUM_MODULES=6
124125

126+
[env:chainlink_ota]
127+
extends=env:chainlink
128+
upload_protocol = espota
129+
upload_port = splitflap.local ; Replace with your device's IP address
130+
upload_flags =
131+
--auth=replace_me!
132+
--port=3232
133+
--host_port=16106
134+
--debug
135+
--progress
136+
--timeout=30
137+
125138
[env:advanced_chainlinkBase]
126139
extends=esp32base
127140
build_src_filter = ${esp32base.build_src_filter} +<../esp32/base>

0 commit comments

Comments
 (0)