FlipDot_Telegram/src/main.cpp
2026-01-20 00:59:55 +01:00

462 lines
11 KiB
C++

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <ArduinoOTA.h>
#include <time.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include "Telegram.h"
#include "FlipDotDrv.h"
#include "secrets.h"
/* most important global declaratioen ;-) */
String Text = "";
const char *const HOSTNAME = "flipdottelegram";
/* ------------ MQTT variables -------------- */
WiFiClient espClient;
PubSubClient mqtt(espClient);
#define FLIPDOT_AVAILABILITY_TOPIC "flipdotdisplay/available"
#define FLIPDOT_SWITCH_TOPIC "flipdotdisplay/switch"
#define FLIPDOT_TEXT_TOPIC "flipdotdisplay/text"
void mqttCallback(char *topic, unsigned char *message, unsigned int length);
void publishOpen()
{
mqtt.publish(FLIPDOT_SWITCH_TOPIC "/state", "open");
mqtt.publish(FLIPDOT_TEXT_TOPIC "/state", Text.c_str());
}
void publishClose()
{
mqtt.publish(FLIPDOT_SWITCH_TOPIC "/state", "close");
mqtt.publish(FLIPDOT_TEXT_TOPIC "/state", Text.c_str());
}
void publishHomeAssistantDiscovery()
{
auto mac = WiFi.macAddress();
mac.replace(":", "_");
JsonDocument doc;
JsonObject device = doc["dev"].to<JsonObject>();
device["ids"] = mac;
device["name"] = "FlipDotDisplay";
device["mf"] = "-mf-";
device["mdl"] = "-mdl-";
device["sw"] = "-sw-";
device["sn"] = "-sn-";
device["hw"] = "-hw-";
JsonObject origin = doc["o"].to<JsonObject>();
origin["name"] = "Hobbyhimmel";
origin["sw"] = "-sw-";
origin["url"] = "https://hobbyhimmel.de";
JsonObject components = doc["cmps"].to<JsonObject>();
JsonObject sw = components["FlipDotDisplaySwitch"].to<JsonObject>();
sw["unique_id"] = mac + "FlipDotDisplaySwitch";
sw["p"] = "switch";
sw["command_topic"] = FLIPDOT_SWITCH_TOPIC;
sw["state_topic"] = FLIPDOT_SWITCH_TOPIC "/state";
sw["payload_on"] = "open";
sw["payload_off"] = "close";
sw["qos"] = 0;
sw["retain"] = false;
JsonObject txt = components["FlipDotDisplayText"].to<JsonObject>();
sw["unique_id"] = mac + "FlipDotDisplayText";
sw["p"] = "text";
sw["command_topic"] = FLIPDOT_TEXT_TOPIC;
sw["state_topic"] = FLIPDOT_TEXT_TOPIC "/state";
sw["qos"] = 0;
sw["retain"] = false;
doc["availability_topic "] = FLIPDOT_AVAILABILITY_TOPIC;
String jsonString;
serializeJson(doc, jsonString);
String discoveryTopic = "homeassistant/device/";
discoveryTopic += mac + "/config";
mqtt.publish(discoveryTopic.c_str(), jsonString.c_str());
Serial.print("Homeassistant Discovery was published: ");
Serial.println(jsonString);
}
/* ------------ OTA section and variables -------------- */
bool OTAStarted;
void onOTAStart()
{
OTAStarted = true;
}
/* ------------ FlipDot variables -------------- */
#define WIDTH 96
#define HEIGHT 16
#define FLIPDOT_BUFFER_SIZE (WIDTH * HEIGHT / 8)
#define ROTATION 0 // 0 or 2 for 0 degrees or 180 degrees
#define MAX_CHAR_PER_LINE WIDTH / 6
FlipDotDrv flip(WIDTH, HEIGHT, 0);
GFXcanvas1 canvas(WIDTH, HEIGHT);
void displayText(const char *text, bool background, int16_t x = -1, int16_t y = -1);
/* ------------ Telegram variables -------------- */
// BearSSL::WiFiClientSecure secured_client;
WiFiClientSecure secured_client;
// TelegramCommand callbacks must fulfill a function pointer: String (*) (const String arguments)
String printOpen(String Args);
String printClose(String Args);
String printText(String Args);
TelegramCommand myCmdList[]{
{"/open", printOpen},
{"/close", printClose},
{"/text", printText},
};
uint32_t myCmdListCnt = sizeof(myCmdList) / sizeof(myCmdList[0]);
Telegram tele(secured_client, myCmdList, myCmdListCnt);
void setup()
{
Serial.begin(115200);
Serial.println();
// rotate the Canvas so it is displayed in the correct orientation on the FlipDotDisplay
canvas.setRotation(ROTATION);
/* initialize the display with several checker template */
uint8_t rawBuff[FLIPDOT_BUFFER_SIZE] = {};
Serial.println("Initializing Display");
flip.sendRaw(rawBuff, sizeof(rawBuff));
delay(1000);
Serial.println("draw checker");
memset(rawBuff, 0xAA, FLIPDOT_BUFFER_SIZE);
for (int i = 0; i < FLIPDOT_BUFFER_SIZE; ++i)
{
if ((i % 4) < 2)
rawBuff[i] = 0xAA;
else
rawBuff[i] = 0x55;
}
flip.sendRaw(rawBuff, sizeof(rawBuff));
delay(1000);
Serial.println("bigger checker");
for (int i = 0; i < FLIPDOT_BUFFER_SIZE; ++i)
{
if ((i % 8) < 4)
rawBuff[i] = 0x33;
else
rawBuff[i] = 0xCC;
}
flip.sendRaw(rawBuff, sizeof(rawBuff));
delay(1000);
Serial.println("largest checker");
for (int i = 0; i < FLIPDOT_BUFFER_SIZE; ++i)
{
if ((i % 16) < 8)
rawBuff[i] = 0x0F;
else
rawBuff[i] = 0xF0;
}
flip.sendRaw(rawBuff, sizeof(rawBuff));
/* attempt to connect to Wifi network */
WiFi.setHostname(HOSTNAME);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
/* get internet time */
Serial.print("setup time: ");
configTime(0, 0, "pool.ntp.org"); // get UTC time via NTP
configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "pool.ntp.org");
/* setup ArduinoOTA */
ArduinoOTA.setHostname(HOSTNAME);
ArduinoOTA.onStart(onOTAStart);
/* prepare MQTT client */
mqtt.setServer(MQTT_SERVER, 1883);
mqtt.setCallback(mqttCallback);
}
void loop()
{
static wl_status_t oldWifiStatus = (wl_status_t)254;
unsigned long now = millis();
static unsigned long lastDisplayRefresh = now;
static unsigned long lastTelegramRefresh = now;
if (WiFi.status() == WL_CONNECTED)
{
// if the Wifi is connected do all the internet stuff
if (WiFi.status() != oldWifiStatus)
{
/* we freshly reconnected so tell everybody that we are online */
Serial.print("\nWiFi connected. IP address: ");
Serial.println(WiFi.localIP());
Serial.print("Retrieving time: ");
configTime(0, 0, "pool.ntp.org"); // get UTC time via NTP
configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "pool.ntp.org");
time_t now = time(nullptr);
while (now < 24 * 3600)
{
Serial.print(".");
delay(100);
now = time(nullptr);
}
Serial.println(now);
ArduinoOTA.begin();
tele.init();
printClose("");
oldWifiStatus = WiFi.status();
}
/* handle the OTA update */
ArduinoOTA.handle();
/* handle Telegram messages */
if ((now - lastTelegramRefresh) > 5000)
{
lastTelegramRefresh = now;
tele.cyclic(now);
}
/* handle MQTT transmissions */
if (!mqtt.loop())
{
Serial.println("MQTT not connected");
// try to connect to MQTT broker again
if (mqtt.connect(HOSTNAME, MQTT_USER, MQTT_PASSWORD, FLIPDOT_AVAILABILITY_TOPIC, 2, true, "offline"))
{
// publish availability message
mqtt.subscribe(FLIPDOT_SWITCH_TOPIC);
mqtt.subscribe(FLIPDOT_TEXT_TOPIC);
mqtt.publish(FLIPDOT_AVAILABILITY_TOPIC, "available");
publishHomeAssistantDiscovery();
}
}
}
else
{
// if wifi is not connected wait for connection
if (WiFi.status() != oldWifiStatus)
{
/* we freshly disconnected so tell everybody that we are offline */
Text = "Wifi \"";
Text += WIFI_SSID;
Text += "\" not connected";
ArduinoOTA.end();
mqtt.disconnect();
oldWifiStatus = WiFi.status();
}
}
/* FlipDot Handling */
if ((now - lastDisplayRefresh) > 2000)
{
lastDisplayRefresh = now;
// fill canvas with black pixels
canvas.fillScreen(0);
// if textlength is larger than possible on the display we need to scroll
uint32_t TextLength = Text.length();
if (TextLength > (MAX_CHAR_PER_LINE - 6))
{
static uint32_t offset = 0;
// get the substring ffor this iteration
String SubText = Text.substring(offset, offset + MAX_CHAR_PER_LINE);
if (SubText.length() < MAX_CHAR_PER_LINE)
{
// if the text is the last line start over
offset = 0;
}
else
{
// increase the offset and reset it at the end of the scrol
offset += 4;
}
// write substring to canvas
displayText(SubText.c_str(), true, 0, 4);
}
else
{
// display current time on the left
tm timeinfo;
char buffer[6]{0};
if (getLocalTime(&timeinfo))
{
strftime(buffer, 6, "%R", &timeinfo);
}
displayText(buffer, true, 0, 4);
// display text on the right centered
displayText(Text.c_str(), true, (6 + (MAX_CHAR_PER_LINE - 6 - TextLength) / 2) * 6, 4);
}
// send canvas to display
flip.sendCanvas(&canvas);
}
/* wait here if loop execution was faster then 200ms but yield minimum once */
do
{
yield();
} while ((millis() - now) < 200);
}
/* function to write text onto the canvas
*
* @param text - C string to be printed
* @param background - boolean switch if the background should be transparent (false) or black (true)
* @param x - position in X direction to write the Text, -1 centers the text
* @param y - position in Y direction to write the Text, -1 centers the text
*
* */
void displayText(const char *text, bool background, int16_t x, int16_t y)
{
// canvas.fillScreen(0);
canvas.setFont(NULL);
canvas.setTextSize(1);
canvas.setTextColor(1);
int16_t cursor_x, cursor_y;
int16_t bounds_x, bounds_y;
uint16_t bounds_w, bounds_h;
canvas.getTextBounds(text, 0, 0, &bounds_x, &bounds_y, &bounds_w, &bounds_h);
if (x == -1)
{
// centered
cursor_x = (WIDTH - bounds_w) / 2;
}
else
{
cursor_x = x;
}
if (y == -1)
{
// centered
cursor_y = (HEIGHT - bounds_h) / 2;
}
else
{
cursor_y = y;
}
/* draw a black rectangle in the background of the text if it should not be transparent */
if (background)
{
canvas.fillRect(cursor_x - 1, cursor_y - 1, bounds_w + 1, bounds_h + 1, 0);
}
/* print the text to the destination on canvas */
canvas.setCursor(cursor_x, cursor_y);
canvas.print(text);
}
String printOpen(String Args)
{
Text = "offen";
publishOpen();
return Text;
}
String printClose(String Args)
{
Text = "leider zu";
publishClose();
return Text;
}
String printText(String Args)
{
Serial.print("Display would show '");
Serial.print(Args);
Serial.println("'");
Text = Args;
publishClose();
return Text;
}
void mqttCallback(char *topic, unsigned char *message, unsigned int length)
{
String MessageString;
/* Convert all char* to Strings for simpler handling */
for (int i = 0; i < length; i++)
{
MessageString += message[i];
}
String ToppicString(topic);
/* if topic is the switch topic check for on, off, open, close */
if (ToppicString == FLIPDOT_SWITCH_TOPIC)
{
Serial.println("Switch topic received: " + MessageString);
if ((MessageString == "open") ||
(MessageString == "on"))
{
printOpen(MessageString);
}
else if ((MessageString == "close") ||
(MessageString == "off"))
{
printClose(MessageString);
}
}
/* if topic is the text topic try deserializing the json text */
if (ToppicString == FLIPDOT_TEXT_TOPIC)
{
Serial.println("Text topic received: " + MessageString);
JsonDocument doc;
if (deserializeJson(doc, (const char *)message, length) == DeserializationError::Ok)
{
MessageString = doc["value"] | String("none"); // the pipe operand tells the default value, if "value" is not existing
if ((MessageString != "none"))
{
printText(MessageString);
}
else
{
Serial.print(FLIPDOT_TEXT_TOPIC);
Serial.println(" has no [\"value\"]!");
}
}
else
{
Serial.print(FLIPDOT_TEXT_TOPIC);
Serial.println(" is no json!");
}
}
}