smartPOC-tx/src/main.cpp
2025-04-05 01:41:32 +02:00

546 lines
No EOL
14 KiB
C++

#include <Arduino.h>
#include <main.h>
SX1278 radio = new Module(LORA_SS, LORA_DIO0, LORA_RST);
POCSAGTransmitter transmitter;
cfg_t globcfg;
bool mqtt_enable = false;
bool wifi_hotspot = false;
bool old_tx_state = false;
char *timeString = new char[33]{};
//
//
//
void setup()
{
setup_leds();
delay(2e2);
Serial.begin(115200);
#ifdef SMART_POC_DISPLAY
display_setup();
#endif
setup_storage(&globcfg);
#ifdef SMART_POC_DISPLAY
display_update_state("loaded cfg");
#endif
// memdebug
Serial.print("Free Heap Size after reading Config:");
Serial.println(ESP.getFreeHeap());
Serial.println("starting up modem");
float bitrate = globcfg.tx_baud / 1000.0;
int state = radio.beginFSK(globcfg.tx_freq, bitrate, globcfg.tx_dev);
if (state == 0) {
Serial.println(F("success!"));
} else {
Serial.print(F("failed, code "));
Serial.println(state);
#ifdef SMART_POC_DISPLAY
display_update_state("modem fail");
#endif
// TODO: add solution to this
while (true) {
digitalWrite(PIN_LED, HIGH);
delay(850);
digitalWrite(PIN_LED, LOW);
delay(250);
}
}
//
radio.setOutputPower(globcfg.tx_power);
Serial.println("initialized modem");
#ifdef SMART_POC_DISPLAY
display_update_state("init modem");
#endif
transmitter.begin(&radio);
Serial.println("initialized transmitter");
#ifdef SMART_POC_DISPLAY
display_update_state("init pocsag-stack");
#endif
//
setup_network();
webserver_setup(&globcfg);
setup_mqtt();
setup_broadcasts();
// memdebug
Serial.print("Free Heap Size after setup:");
Serial.println(ESP.getFreeHeap());
// leds
digitalWrite(PIN_LED, LOW);
}
//
// Network: WiFi 1+2 or Hotspot-Mode
//
void setup_network()
{
WiFi.mode(WIFI_STA);
WiFi.setHostname(globcfg.device_name);
unsigned long connectTimeout = 10e3;
uint8_t connectResult;
bool wifi1valid = strlen(globcfg.wifi1_ssid) > 2;
bool wifi2valid = strlen(globcfg.wifi2_ssid) > 2;
//
if (wifi1valid)
{
#ifdef SMART_POC_DISPLAY
display_update_state("trying wifi 1");
#endif
Serial.println("connecting to WiFi 1");
WiFi.begin(globcfg.wifi1_ssid, globcfg.wifi1_pass);
while ((WiFi.status() != WL_CONNECTED) && (millis() < 10000))
{
delay(10);
}
connectResult = WiFi.status();
Serial.printf("result=%d\n", connectResult);
if (connectResult == WL_CONNECTED)
{
Serial.println(WiFi.localIP());
return;
}
// while (WiFi.status() != WL_CONNECTED)
// {
// delay(500);
// Serial.print(".");
// }
}
if (wifi2valid)
{
#ifdef SMART_POC_DISPLAY
display_update_state("trying wifi 2");
#endif
Serial.println("connecting to WiFi 2");
WiFi.begin(globcfg.wifi2_ssid, globcfg.wifi2_pass);
while ((WiFi.status() != WL_CONNECTED) && (millis() < 22000))
{
delay(10);
}
connectResult = WiFi.status();
Serial.printf("result=%d\n", connectResult);
if (connectResult == WL_CONNECTED)
{
Serial.println(WiFi.localIP());
return;
}
}
//
#ifdef SMART_POC_DISPLAY
display_update_state("init hotspot mode");
#endif
Serial.println("wifi failed to connect, going AP");
WiFi.mode(WIFI_AP);
WiFi.softAP(globcfg.ap_ssid, globcfg.ap_pass);
wifi_hotspot = true;
}
//
// MQTT
//
WiFiClient wifiMqttClient;
PubSubClient mqttClient(wifiMqttClient);
String clear_topic, transmit_topic, state_topic;
void setup_mqtt()
{
if (wifi_hotspot)
{
Serial.println("hotspot prevents mqtt");
return;
} // dont
mqtt_enable = strlen(globcfg.mqtt_host) > 5;
if (!mqtt_enable)
{
Serial.println("MQTT not wanted, exiting setup");
return;
} // if we should not do this
mqttClient.setServer(globcfg.mqtt_host, globcfg.mqtt_port);
#ifdef SMART_POC_DISPLAY
display_update_state("connecting to MQTT");
#endif
Serial.printf("connecting to mqtt %s\n", globcfg.mqtt_host);
if (mqttClient.connect(globcfg.mqtt_user, globcfg.mqtt_user, globcfg.mqtt_pass))
{
// succ
mqttClient.setCallback(mqtt_callback);
//
transmit_topic = String(globcfg.mqtt_topic);
transmit_topic.concat("/transmit");
mqttClient.subscribe(transmit_topic.c_str());
clear_topic = String(globcfg.mqtt_topic);
clear_topic.concat("/clear");
mqttClient.subscribe(clear_topic.c_str());
state_topic = String(globcfg.mqtt_topic);
state_topic.concat("/state");
mqttClient.publish(state_topic.c_str(), "false");
//
Serial.println("connected to mqtt");
}
else
{
Serial.printf("failed to connect to mqtt, state= %d\n", mqttClient.state());
}
}
void mqtt_callback(char *topic, byte *payload, unsigned int length)
{
JsonDocument doc;
deserializeJson(doc, payload);
if (clear_topic.equals(topic))
{
txControllerBatchClear();
return;
}
Serial.println("rx mqtt");
if (doc["ric"].is<int>() && doc["msg"].is<String>())
{
String text = doc["msg"].as<String>();
text = txHandleUmlauts(text);
int addr = doc["ric"].as<int>();
int func = 3;
if (doc["fun"].is<int>())
{
func = doc["fun"].as<int>();
}
char *messageText = new char[text.length() + 1]{};
text.toCharArray(messageText, text.length() + 1);
txControllerInsert((uint32_t)addr, (uint8_t)func, messageText);
delete []messageText;
// if (transmitter.isActive() == false)
// {
// if (mqtt_enable)
// {
// mqttClient.publish(state_topic.c_str(), "true");
// }
// }
txControllerBatchStart();
}
}
void mqtt_loop(bool tx_state) {
mqttClient.loop();
if (old_tx_state != tx_state) {
if (tx_state)
mqttClient.publish(state_topic.c_str(), "true");
else
mqttClient.publish(state_topic.c_str(), "false");
}
}
//
// LEDs
//
void setup_leds() {
pinMode(PIN_LED, OUTPUT);
digitalWrite(PIN_LED, HIGH);
}
void leds_loop(bool tx_state) {
if (old_tx_state != tx_state)
digitalWrite(PIN_LED, tx_state);
}
//
//
//
void serlog_loop(bool tx_state) {
if (tx_state != old_tx_state)
if (tx_state)
Serial.println("Transmitter Active");
else
Serial.println("Transmitter Standby");
}
//
//
//
//
void loop()
{
bool tx_state = transmitter.isActive();
#ifdef SMART_POC_DISPLAY
display_loop(tx_state, &transmitter, &globcfg, timeString);
#endif
if (old_tx_state == true && tx_state == false && globcfg.tx_empty_queue)
txControllerBatchClear();
//
serlog_loop(tx_state);
leds_loop(tx_state);
if (mqtt_enable)
mqtt_loop(tx_state);
broadcast_loop();
heap_loop();
old_tx_state = tx_state;
}
//
// TX Stack
//
struct UmlautMap {
char utf8;
char replacement;
};
UmlautMap umlautMapGermany[] = {
{0xDC, 0x5D}, {0xC4, 0x5B}, {0xD6, 0x5C},
{0xFC, 0x7D}, {0xE4, 0x7B}, {0xF6, 0x7C},
{0xDF, 0x7E}
};
UmlautMap umlautMapIntl[] = {
{0xDC, 0x56}, {0xC4, 0x41}, {0xD6, 0x4F},
{0xFC, 0x75}, {0xE4, 0x61}, {0xF6, 0x6F},
{0xDF, 0x73}
};
#ifdef SMART_POC_DEBUG_UMLAUT_HEX
void printHex(const char* text) {
while (*text) {
if ((uint8_t)*text < 0x10) {
Serial.print("0"); // Ensure two-digit hex format
}
Serial.print((uint8_t)*text, HEX);
Serial.print(" ");
text++;
}
Serial.println();
}
#endif
void replaceStringWithChar(String &input, const String &find, char replace) {
int index = input.indexOf(find); // Find the first occurrence
while (index != -1) {
input = input.substring(0, index) + replace + input.substring(index + find.length());
index = input.indexOf(find, index + 1); // Find next occurrence
}
}
String txHandleUmlauts(String input) {
bool useGermanMap = globcfg.pocsag_german;
UmlautMap* map = useGermanMap ? umlautMapGermany : umlautMapIntl;
int mapSize = useGermanMap ? sizeof(umlautMapGermany) / sizeof(UmlautMap)
: sizeof(umlautMapIntl) / sizeof(UmlautMap);
for (int i = 0; i < mapSize; i++) {
input.replace(map[i].utf8, map[i].replacement);
}
if (useGermanMap) {
replaceStringWithChar(input, "Ä", '[');
replaceStringWithChar(input, "Ö", '\\');
replaceStringWithChar(input, "Ü", ']');
replaceStringWithChar(input, "ä", '{');
replaceStringWithChar(input, "ö", '|');
replaceStringWithChar(input, "ü", '}');
replaceStringWithChar(input, "ß", '~');
} else {
replaceStringWithChar(input, "Ä", 'A');
replaceStringWithChar(input, "Ö", 'O');
replaceStringWithChar(input, "Ü", 'U');
replaceStringWithChar(input, "ä", 'a');
replaceStringWithChar(input, "ö", 'o');
replaceStringWithChar(input, "ü", 'u');
replaceStringWithChar(input, "ß", 's');
}
return input;
}
void txControllerInsert(uint32_t ric, uint8_t functionBit, char *text)
{
transmitter.queuePage(ric, functionBit, text);
}
void txControllerInsert(uint32_t ric, uint8_t functionBit, bool numeric, char *text)
{
transmitter.queuePage(ric, functionBit, numeric, text);
}
void txControllerBatchStart()
{
transmitter.transmitBatch();
}
void txControllerBatchClear()
{
transmitter.clearQueue();
}
int txControllerBatchQueueCount()
{
return transmitter.getQueueCount();
}
//
// DWD + MoWaS + Time + Idle
//
WiFiClient wifiHttpClient;
#ifdef SMART_POC_MODULE_DWD
DWDClient dwdClient(&wifiHttpClient);
#endif
#ifdef SMART_POC_MODULE_DWD_WX
DWDWXClient dwdWXClient(&wifiHttpClient);
#endif
#ifdef SMART_POC_MODULE_MOWAS
MoWaSClient mowasClient(&wifiHttpClient);
#endif
Timezone utcTime;
Timezone localTime;
bool time_beacon_first = true;
unsigned long lastTimeBeaconTime = 0;
String currentMessage;
String formattedTime;
char *idleBeaconText = new char[1]{};
bool idle_beacon_first = true;
unsigned long lastIdleBeaconTime = 0;
//
void setup_broadcasts()
{
if (wifi_hotspot) {
mqtt_enable = false;
globcfg.dwd_enable = false;
globcfg.mowas_enable = false;
globcfg.time_enable = false;
}
#ifdef SMART_POC_MODULE_DWD
// DWD
if (globcfg.dwd_interval < 1)
globcfg.dwd_enable = false;
if (globcfg.dwd_enable)
dwdClient.begin(globcfg.dwd_interval, globcfg.dwd_region);
#endif
#ifdef SMART_POC_MODULE_DWD_WX
// DWD
if (globcfg.dwdwx_enable)
dwdWXClient.begin(60*6, globcfg.dwdwx_lat, globcfg.dwdwx_lng);
#endif
#ifdef SMART_POC_MODULE_MOWAS
// MoWaS
if (globcfg.mowas_interval < 1)
globcfg.mowas_enable = false;
if (globcfg.mowas_enable)
mowasClient.begin(globcfg.mowas_interval, globcfg.mowas_region);
#endif
// Time
if (globcfg.time_enable) {
waitForSync(30e3);
localTime.setLocation(globcfg.time_zone);
}
}
void broadcast_loop()
{
#ifdef SMART_POC_MODULE_DWD
if (globcfg.dwd_enable)
{
dwdClient.loop();
if (dwdClient.isDirty())
{
currentMessage = txHandleUmlauts(dwdClient.currentMessage);
#ifdef SMART_POC_DEBUG_UMLAUT_HEX
Serial.print("Hex: ");
printHex(currentMessage.c_str());
#endif
char *messageText = new char[currentMessage.length() + 1]{};
currentMessage.toCharArray(messageText, currentMessage.length() + 1);
Serial.println(F("[DWD] Transmitting Warning"));
Serial.println(messageText);
transmitter.queuePage(globcfg.broadcast_ric, globcfg.dwd_fun + 0, messageText);
delete []messageText;
dwdClient.currentMessage.clear();
currentMessage.clear();
transmitter.transmitBatch();
}
}
#endif
#ifdef SMART_POC_MODULE_DWD_WX
if (globcfg.dwdwx_enable)
{
dwdWXClient.loop();
if (dwdWXClient.isDirty())
{
currentMessage = txHandleUmlauts(dwdWXClient.currentMessage);
#ifdef SMART_POC_DEBUG_UMLAUT_HEX
Serial.print("Hex: ");
printHex(currentMessage.c_str());
#endif
char *messageText = new char[currentMessage.length() + 1]{};
currentMessage.toCharArray(messageText, currentMessage.length() + 1);
Serial.println(F("[DWD-WX] Transmitting"));
transmitter.queuePage(globcfg.broadcast_ric, globcfg.dwdwx_fun + 0, messageText);
delete []messageText;
dwdWXClient.currentMessage.clear();
currentMessage.clear();
transmitter.transmitBatch();
}
}
#endif
#ifdef SMART_POC_MODULE_MOWAS
if (globcfg.mowas_enable)
{
mowasClient.loop();
if (mowasClient.isDirty())
{
currentMessage = txHandleUmlauts(mowasClient.currentMessage);
#ifdef SMART_POC_DEBUG_UMLAUT_HEX
Serial.print("Hex: ");
printHex(currentMessage.c_str());
#endif
char *messageText = new char[currentMessage.length() + 1]{};
currentMessage.toCharArray(messageText, currentMessage.length() + 1);
Serial.println(F("[MoWaS] Transmitting Warning"));
Serial.println(messageText);
transmitter.queuePage(globcfg.broadcast_ric, globcfg.mowas_fun + 0, messageText);
delete []messageText;
currentMessage.clear();
transmitter.transmitBatch();
}
}
#endif
if (globcfg.time_enable) {
events();
// re-use this var ;)
formattedTime = utcTime.dateTime("Y-m-d H:i:s");
formattedTime.toCharArray(timeString, formattedTime.length() + 1);
if (time_beacon_first || millis() - lastTimeBeaconTime >= globcfg.time_interval * 60e3) {
lastTimeBeaconTime = millis();
time_beacon_first = false;
switch (globcfg.time_mode) {
case 0: // TPL-Eng SubRIC 1(B) @ Service OTA
formattedTime = "***1***" + utcTime.dateTime("Hidmy");
break;
case 1: // TPL-Ger
formattedTime = "#ZEIT=" + localTime.dateTime("Hidmy") + "#ZEIT=" + localTime.dateTime("Hidmy");
break;
case 2: // Shitphone
formattedTime = ";TIME=" + localTime.dateTime("Hidmy") + ";TIME=" + localTime.dateTime("Hidmy");
break;
}
char *messageText = new char[formattedTime.length() + 1]{};
formattedTime.toCharArray(messageText, formattedTime.length() + 1);
Serial.println(F("[Time] Transmitting Time"));
transmitter.queuePage(globcfg.time_ric, globcfg.time_fun + 0, messageText);
delete []messageText;
formattedTime.clear();
transmitter.transmitBatch();
}
}
if (globcfg.idle_enable) {
if (idle_beacon_first || millis() - lastIdleBeaconTime >= globcfg.idle_interval * 60e3) {
lastIdleBeaconTime = millis();
idle_beacon_first = false;
Serial.println(F("[Idle] Transmitting Beacon"));
transmitter.queuePage(0, 0, idleBeaconText);
transmitter.transmitBatch();
}
}
}
//
void broadcastTriggerTimeBeacon() {
time_beacon_first = true;
}
//
// Heap Management Logs
//
unsigned long lastReportTime = 0;
const unsigned long reportInterval = 60000;
void heap_loop()
{
if (millis() - lastReportTime >= reportInterval)
{
lastReportTime = millis();
Serial.print(F("Free Heap Size: "));
Serial.print(ESP.getFreeHeap());
Serial.print(F("Free min Heap Size: "));
Serial.println(ESP.getMinFreeHeap());
}
}