/* Theengs OpenMQTTGateway - We Unite Sensors in One Open-Source Interface Act as a wifi or ethernet gateway between your 433mhz/infrared IR/BLE signal and a MQTT broker Send and receiving command by MQTT This gateway enables to: - receive MQTT data from a topic and send LORA signal corresponding to the received MQTT data - publish MQTT data to a different topic related to received LORA signal Copyright: (c)Florian ROBERT This file is part of OpenMQTTGateway. OpenMQTTGateway is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. OpenMQTTGateway is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "User_config.h" #ifdef ZgatewayLORA # include # include # include # include # include "TheengsCommon.h" # include "config_LORA.h" # include "config_mqttDiscovery.h" # define WIPHONE_MESSAGE_MAGIC 0x6c6d # define WIPHONE_MESSAGE_MIN_LEN sizeof(wiphone_message) - WIPHONE_MAX_MESSAGE_LEN # define WIPHONE_MAX_MESSAGE_LEN 230 LORAConfig_s LORAConfig; void LORAConfig_fromJson(JsonObject& LORAdata); void LORAConfig_apply(); String stateLORAMeasures(); # ifdef ZmqttDiscovery # include "config_mqttDiscovery.h" extern void createDiscovery(const char* sensor_type, const char* st_topic, const char* s_name, const char* unique_id, const char* availability_topic, const char* device_class, const char* value_template, const char* payload_on, const char* payload_off, const char* unit_of_meas, int off_delay, const char* payload_available, const char* payload_not_available, bool gateway_entity, const char* cmd_topic, const char* device_name, const char* device_manufacturer, const char* device_model, const char* device_id, bool retainCmd, const char* state_class, const char* state_off, const char* state_on, const char* enum_options, const char* command_template); SemaphoreHandle_t semaphorecreateOrUpdateDeviceLORA; std::vector LORAdevices; int newLORADevices = 0; static LORAdevice NO_LORA_DEVICE_FOUND = {{0}, 0, false}; LORAdevice* getDeviceById(const char* id); // Declared here to avoid pre-compilation issue (misplaced auto declaration by pio) LORAdevice* getDeviceById(const char* id) { THEENGS_LOG_TRACE(F("getDeviceById %s" CR), id); for (std::vector::iterator it = LORAdevices.begin(); it != LORAdevices.end(); ++it) { if ((strcmp((*it)->uniqueId, id) == 0)) { return *it; } } return &NO_LORA_DEVICE_FOUND; } void dumpLORADevices() { for (std::vector::iterator it = LORAdevices.begin(); it != LORAdevices.end(); ++it) { LORAdevice* p = *it; THEENGS_LOG_TRACE(F("uniqueId %s" CR), p->uniqueId); THEENGS_LOG_TRACE(F("modelName %s" CR), p->modelName); THEENGS_LOG_TRACE(F("isDisc %d" CR), p->isDisc); } } void createOrUpdateDeviceLORA(const char* id, const char* model, uint8_t flags) { if (xSemaphoreTake(semaphorecreateOrUpdateDeviceLORA, pdMS_TO_TICKS(30000)) == pdFALSE) { THEENGS_LOG_ERROR(F("[LORA] semaphorecreateOrUpdateDeviceLORA Semaphore NOT taken" CR)); return; } LORAdevice* device = getDeviceById(id); if (device == &NO_LORA_DEVICE_FOUND) { THEENGS_LOG_TRACE(F("add %s" CR), id); //new device device = new LORAdevice(); if (strlcpy(device->uniqueId, id, uniqueIdSize) > uniqueIdSize) { THEENGS_LOG_WARNING(F("[LORA] Device id %s exceeds available space" CR), id); // Remove from production release ? }; if (strlcpy(device->modelName, model, modelNameSize) > modelNameSize) { THEENGS_LOG_WARNING(F("[LORA] Device model %s exceeds available space" CR), id); // Remove from production release ? }; device->isDisc = flags & device_flags_isDisc; LORAdevices.push_back(device); newLORADevices++; } else { THEENGS_LOG_TRACE(F("update %s" CR), id); if (flags & device_flags_isDisc) { device->isDisc = true; } } xSemaphoreGive(semaphorecreateOrUpdateDeviceLORA); } // This function always should be called from the main core as it generates direct mqtt messages // When overrideDiscovery=true, we publish discovery messages of known LORAdevices (even if no new) void launchLORADiscovery(bool overrideDiscovery) { if (!overrideDiscovery && newLORADevices == 0) return; if (xSemaphoreTake(semaphorecreateOrUpdateDeviceLORA, pdMS_TO_TICKS(QueueSemaphoreTimeOutLoop)) == pdFALSE) { THEENGS_LOG_ERROR(F("[LORA] semaphorecreateOrUpdateDeviceLORA Semaphore NOT taken" CR)); return; } newLORADevices = 0; std::vector localDevices = LORAdevices; xSemaphoreGive(semaphorecreateOrUpdateDeviceLORA); for (std::vector::iterator it = localDevices.begin(); it != localDevices.end(); ++it) { LORAdevice* pdevice = *it; THEENGS_LOG_TRACE(F("Device id %s" CR), pdevice->uniqueId); // Do not launch discovery for the LORAdevices already discovered (unless we have overrideDiscovery) or that are not unique by their MAC Address (Ibeacon, GAEN and Microsoft Cdp) if (overrideDiscovery || !isDiscovered(pdevice)) { size_t numRows = sizeof(LORAparameters) / sizeof(LORAparameters[0]); for (int i = 0; i < numRows; i++) { if (strstr(pdevice->uniqueId, LORAparameters[i][0]) != 0) { // Remove the key from the unique id to extract the device id String idWoKey = pdevice->uniqueId; idWoKey.remove(idWoKey.length() - (strlen(LORAparameters[i][0]) + 1)); THEENGS_LOG_TRACE(F("idWoKey %s" CR), idWoKey.c_str()); String value_template = "{{ value_json." + String(LORAparameters[i][0]) + " | is_defined }}"; String topic = idWoKey; topic = String(subjectLORAtoMQTT) + "/" + topic; createDiscovery("sensor", //set Type (char*)topic.c_str(), LORAparameters[i][1], pdevice->uniqueId, //set state_topic,name,uniqueId "", LORAparameters[i][3], (char*)value_template.c_str(), //set availability_topic,device_class,value_template, "", "", LORAparameters[i][2], //set,payload_on,payload_off,unit_of_meas, 0, //set off_delay "", "", false, "", //set,payload_available,payload_not available ,is a gateway entity, command topic (char*)idWoKey.c_str(), "", pdevice->modelName, (char*)idWoKey.c_str(), false, // device name, device manufacturer, device model, device ID, retain stateClassMeasurement //State Class ); pdevice->isDisc = true; // we don't need the semaphore and all the search magic via createOrUpdateDevice dumpLORADevices(); break; } } if (!pdevice->isDisc) { THEENGS_LOG_TRACE(F("Device id %s was not discovered" CR), pdevice->uniqueId); // Remove from production release ? } } else { THEENGS_LOG_TRACE(F("Device already discovered or that doesn't require discovery %s" CR), pdevice->uniqueId); } } } void storeLORADiscovery(JsonObject& RFLORA_ESPdata, const char* model, const char* uniqueid) { //Sanitize model name String modelSanitized = model; modelSanitized.replace(" ", "_"); modelSanitized.replace("/", "_"); modelSanitized.replace(".", "_"); modelSanitized.replace("&", ""); //Sensors translation matrix for sensors that requires statistics by using stateClassMeasurement size_t numRows = sizeof(LORAparameters) / sizeof(LORAparameters[0]); for (int i = 0; i < numRows; i++) { if (RFLORA_ESPdata.containsKey(LORAparameters[i][0])) { String key_id = String(uniqueid) + "-" + String(LORAparameters[i][0]); createOrUpdateDeviceLORA((char*)key_id.c_str(), (char*)modelSanitized.c_str(), device_flags_init); } } } # endif typedef struct __attribute__((packed)) { // WiPhone uses RadioHead library which has additional (unused) headers uint8_t rh_to; uint8_t rh_from; uint8_t rh_id; uint8_t rh_flags; uint16_t magic; uint32_t to; uint32_t from; char message[WIPHONE_MAX_MESSAGE_LEN]; } wiphone_message; enum LORA_ID_NUM { UNKNOWN_DEVICE = -1, WIPHONE, }; typedef enum LORA_ID_NUM LORA_ID_NUM; /* Try and determine device given the payload */ uint8_t _determineDevice(byte* packet, int packetSize) { // Check WiPhone header if (packetSize >= WIPHONE_MESSAGE_MIN_LEN && ((wiphone_message*)packet)->magic == WIPHONE_MESSAGE_MAGIC) return WIPHONE; // No matches return UNKNOWN_DEVICE; } /* Try and determine device given the JSON type */ uint8_t _determineDevice(JsonObject& LORAdata) { const char* protocol_name = LORAdata["type"]; // No type provided if (!protocol_name) return UNKNOWN_DEVICE; if (strcmp(protocol_name, "WiPhone") == 0) return WIPHONE; // No matches return UNKNOWN_DEVICE; } /* Create JSON information from WiPhone packet */ boolean _WiPhonetoX(byte* packet, JsonObject& LORAdata) { // Decode the LoRa packet and send over MQTT wiphone_message* msg = (wiphone_message*)packet; // Set the header information char from[9] = {0}; char to[9] = {0}; snprintf(from, 9, "%06X", msg->from); snprintf(to, 9, "%06X", msg->to); // From and To are the last 3 octets from the WiPhone's ESP32 chip ID // Special case is 0x000000: "broadcast" LORAdata["from"] = from; LORAdata["to"] = to; LORAdata["message"] = msg->message; LORAdata["type"] = "WiPhone"; return true; } /* Create WiPhone packet from JSON */ boolean _MQTTtoWiPhone(JsonObject& LORAdata) { // Prepare a LoRa packet to send to the WiPhone wiphone_message wiphonemsg; wiphonemsg.rh_to = 0xff; wiphonemsg.rh_from = 0xff; wiphonemsg.rh_id = 0x00; wiphonemsg.rh_flags = 0x00; wiphonemsg.magic = WIPHONE_MESSAGE_MAGIC; wiphonemsg.from = strtol(LORAdata["from"], NULL, 16); wiphonemsg.to = strtol(LORAdata["to"], NULL, 16); const char* message = LORAdata["message"]; strlcpy(wiphonemsg.message, message, WIPHONE_MAX_MESSAGE_LEN); LoRa.write((uint8_t*)&wiphonemsg, strlen(message) + WIPHONE_MESSAGE_MIN_LEN + 1); return true; } void LORAConfig_init() { LORAConfig.frequency = LORA_BAND; LORAConfig.txPower = LORA_TX_POWER; LORAConfig.spreadingFactor = LORA_SPREADING_FACTOR; LORAConfig.signalBandwidth = LORA_SIGNAL_BANDWIDTH; LORAConfig.codingRateDenominator = LORA_CODING_RATE; LORAConfig.preambleLength = LORA_PREAMBLE_LENGTH; LORAConfig.syncWord = LORA_SYNC_WORD; LORAConfig.crc = DEFAULT_CRC; LORAConfig.invertIQ = INVERT_IQ; LORAConfig.onlyKnown = LORA_ONLY_KNOWN; } void LORAConfig_load() { StaticJsonDocument jsonBuffer; preferences.begin(Gateway_Short_Name, true); if (preferences.isKey("LORAConfig")) { auto error = deserializeJson(jsonBuffer, preferences.getString("LORAConfig", "{}")); preferences.end(); THEENGS_LOG_NOTICE(F("LORA Config loaded" CR)); if (error) { THEENGS_LOG_ERROR(F("LORA Config deserialization failed: %s, buffer capacity: %u" CR), error.c_str(), jsonBuffer.capacity()); return; } if (jsonBuffer.isNull()) { THEENGS_LOG_WARNING(F("LORA Config is null" CR)); return; } JsonObject jo = jsonBuffer.as(); LORAConfig_fromJson(jo); THEENGS_LOG_NOTICE(F("LORA Config loaded" CR)); } else { preferences.end(); THEENGS_LOG_NOTICE(F("LORA Config not found" CR)); } } byte hexStringToByte(const String& hexString) { return (byte)strtol(hexString.c_str(), NULL, 16); } void LORAConfig_apply() { LoRa.setFrequency(LORAConfig.frequency); LoRa.setTxPower(LORAConfig.txPower); LoRa.setSpreadingFactor(LORAConfig.spreadingFactor); LoRa.setSignalBandwidth(LORAConfig.signalBandwidth); LoRa.setCodingRate4(LORAConfig.codingRateDenominator); LoRa.setPreambleLength(LORAConfig.preambleLength); LoRa.setSyncWord(LORAConfig.syncWord); LORAConfig.crc ? LoRa.enableCrc() : LoRa.disableCrc(); LORAConfig.invertIQ ? LoRa.enableInvertIQ() : LoRa.disableInvertIQ(); } void LORAConfig_fromJson(JsonObject& LORAdata) { Config_update(LORAdata, "frequency", LORAConfig.frequency); Config_update(LORAdata, "txpower", LORAConfig.txPower); Config_update(LORAdata, "spreadingfactor", LORAConfig.spreadingFactor); Config_update(LORAdata, "signalbandwidth", LORAConfig.signalBandwidth); Config_update(LORAdata, "codingrate", LORAConfig.codingRateDenominator); Config_update(LORAdata, "preamblelength", LORAConfig.preambleLength); Config_update(LORAdata, "onlyknown", LORAConfig.onlyKnown); // Handle syncword separately as it requires hex string conversion if (LORAdata.containsKey("syncword")) { String syncWordStr = LORAdata["syncword"].as(); byte newSyncWord = hexStringToByte(syncWordStr); if (newSyncWord != LORAConfig.syncWord) { LORAConfig.syncWord = newSyncWord; THEENGS_LOG_NOTICE(F("Config syncword changed: %d" CR), LORAConfig.syncWord); } else { THEENGS_LOG_NOTICE(F("Config syncword unchanged, currently: %d" CR), LORAConfig.syncWord); } } Config_update(LORAdata, "enablecrc", LORAConfig.crc); Config_update(LORAdata, "invertiq", LORAConfig.invertIQ); if (LORAdata.containsKey("erase") && LORAdata["erase"].as()) { // Erase config from NVS (non-volatile storage) preferences.begin(Gateway_Short_Name, false); if (preferences.isKey("LORAConfig")) { int result = preferences.remove("LORAConfig"); THEENGS_LOG_NOTICE(F("LORA config erase result: %d" CR), result); preferences.end(); return; // Erase prevails on save, so skipping save } else { THEENGS_LOG_NOTICE(F("LORA config not found" CR)); preferences.end(); } } if (LORAdata.containsKey("save") && LORAdata["save"].as()) { StaticJsonDocument jsonBuffer; JsonObject jo = jsonBuffer.to(); jo["frequency"] = LORAConfig.frequency; jo["txpower"] = LORAConfig.txPower; jo["spreadingfactor"] = LORAConfig.spreadingFactor; jo["signalbandwidth"] = LORAConfig.signalBandwidth; jo["codingrate"] = LORAConfig.codingRateDenominator; jo["preamblelength"] = LORAConfig.preambleLength; if (LORAConfig.syncWord < 0 || LORAConfig.syncWord > 255) { THEENGS_LOG_ERROR(F("Invalid syncWord value: %d" CR), LORAConfig.syncWord); } else { char syncWordHex[5]; snprintf(syncWordHex, sizeof(syncWordHex), "0x%02X", LORAConfig.syncWord); jo["syncword"] = syncWordHex; } jo["enablecrc"] = LORAConfig.crc; jo["invertiq"] = LORAConfig.invertIQ; jo["onlyknown"] = LORAConfig.onlyKnown; // Save config into NVS (non-volatile storage) String conf = ""; serializeJson(jsonBuffer, conf); preferences.begin(Gateway_Short_Name, false); int result = preferences.putString("LORAConfig", conf); preferences.end(); THEENGS_LOG_NOTICE(F("LORA Config_save: %s, result: %d" CR), conf.c_str(), result); } } void setupLORA() { LORAConfig_init(); LORAConfig_load(); # ifdef ZmqttDiscovery semaphorecreateOrUpdateDeviceLORA = xSemaphoreCreateBinary(); xSemaphoreGive(semaphorecreateOrUpdateDeviceLORA); # endif THEENGS_LOG_NOTICE(F("LORA Frequency: %d" CR), LORAConfig.frequency); # ifdef ESP8266 SPI.begin(); # else SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS); # endif LoRa.setPins(LORA_SS, LORA_RST, LORA_DI0); if (!LoRa.begin(LORAConfig.frequency)) { THEENGS_LOG_ERROR(F("gatewayLORA setup failed!" CR)); while (1); } LORAConfig_apply(); LoRa.receive(); THEENGS_LOG_NOTICE(F("LORA_SCK: %d" CR), LORA_SCK); THEENGS_LOG_NOTICE(F("LORA_MISO: %d" CR), LORA_MISO); THEENGS_LOG_NOTICE(F("LORA_MOSI: %d" CR), LORA_MOSI); THEENGS_LOG_NOTICE(F("LORA_SS: %d" CR), LORA_SS); THEENGS_LOG_NOTICE(F("LORA_RST: %d" CR), LORA_RST); THEENGS_LOG_NOTICE(F("LORA_DI0: %d" CR), LORA_DI0); THEENGS_LOG_TRACE(F("gatewayLORA setup done" CR)); } void LORAtoX() { int packetSize = LoRa.parsePacket(); if (packetSize) { StaticJsonDocument LORAdataBuffer; JsonObject LORAdata = LORAdataBuffer.to(); THEENGS_LOG_TRACE(F("Rcv. LORA" CR)); # ifdef ESP32 String taskMessage = "LORA Task running on core "; taskMessage = taskMessage + xPortGetCoreID(); //trc(taskMessage); # endif // Create packet and reserve null terminator space byte packet[packetSize + 1]; boolean binary = false; for (int i = 0; i < packetSize; i++) { packet[i] = (char)LoRa.read(); if (packet[i] < 32 || packet[i] > 127) binary = true; } // Terminate with a null character in case we have a string packet[packetSize] = 0; uint8_t deviceId = _determineDevice(packet, packetSize); if (deviceId == WIPHONE) { _WiPhonetoX(packet, LORAdata); } else if (binary) { if (LORAConfig.onlyKnown) { THEENGS_LOG_TRACE(F("Ignoring non identifiable packet" CR)); return; } // We have non-ascii data: create hex string of the data char hex[packetSize * 2 + 1]; TheengsUtils::_rawToHex(packet, hex, packetSize); // Terminate with a null character hex[packetSize * 2] = 0; LORAdata["hex"] = hex; } else { // ascii payload std::string packetStrStd = (char*)packet; auto result = deserializeJson(LORAdataBuffer, packetStrStd); if (result) { THEENGS_LOG_NOTICE(F("LORA packet deserialization failed, not a json, sending raw message" CR)); LORAdata = LORAdataBuffer.to(); LORAdata["message"] = (char*)packet; } else { THEENGS_LOG_TRACE(F("LORA packet deserialization OK" CR)); } } LORAdata["rssi"] = (int)LoRa.packetRssi(); LORAdata["snr"] = (float)LoRa.packetSnr(); LORAdata["pferror"] = (float)LoRa.packetFrequencyError(); LORAdata["packetSize"] = (int)packetSize; if (LORAdata.containsKey("id")) { std::string id = LORAdata["id"]; id.erase(std::remove(id.begin(), id.end(), ':'), id.end()); # ifdef ZmqttDiscovery if (SYSConfig.discovery) { if (!LORAdata.containsKey("model")) LORAdataBuffer["model"] = "LORA_NODE"; storeLORADiscovery(LORAdata, LORAdata["model"].as(), id.c_str()); } # endif buildTopicFromId(LORAdata, subjectLORAtoMQTT); } else { LORAdataBuffer["origin"] = subjectLORAtoMQTT; } enqueueJsonObject(LORAdata); if (repeatLORAwMQTT) { THEENGS_LOG_TRACE(F("Pub LORA for rpt" CR)); LORAdata["origin"] = subjectMQTTtoLORA; enqueueJsonObject(LORAdata); } } } # if jsonReceiving void XtoLORA(const char* topicOri, JsonObject& LORAdata) { // json object decoding if (cmpToMainTopic(topicOri, subjectMQTTtoLORA)) { THEENGS_LOG_TRACE(F("MQTTtoLORA json" CR)); const char* message = LORAdata["message"]; const char* hex = LORAdata["hex"]; LORAConfig_fromJson(LORAdata); LORAConfig_apply(); if (message || hex) { LoRa.beginPacket(); uint8_t deviceId = _determineDevice(LORAdata); if (deviceId == WIPHONE) { _MQTTtoWiPhone(LORAdata); } else if (hex) { // We have hex data: create convert to binary byte raw[strlen(hex) / 2]; TheengsUtils::_hexToRaw(hex, raw, sizeof(raw)); LoRa.write((uint8_t*)raw, sizeof(raw)); } else { // ascii payload LoRa.print(message); } LoRa.endPacket(); THEENGS_LOG_TRACE(F("MQTTtoLORA OK" CR)); // we acknowledge the sending by publishing the value to an acknowledgement topic, for the moment even if it is a signal repetition we acknowledge also LORAdata["origin"] = subjectGTWLORAtoMQTT; enqueueJsonObject(LORAdata); } else { THEENGS_LOG_ERROR(F("MQTTtoLORA Fail json" CR)); } } if (cmpToMainTopic(topicOri, subjectMQTTtoLORAset)) { THEENGS_LOG_TRACE(F("MQTTtoLORA json set" CR)); /* * Configuration modifications priorities: * First `init=true` and `load=true` commands are executed (if both are present, INIT prevails on LOAD) * Then parameters included in json are taken in account * Finally `erase=true` and `save=true` commands are executed (if both are present, ERASE prevails on SAVE) */ if (LORAdata.containsKey("init") && LORAdata["init"].as()) { // Restore the default (initial) configuration LORAConfig_init(); } else if (LORAdata.containsKey("load") && LORAdata["load"].as()) { // Load the saved configuration, if not initialised LORAConfig_load(); } // Load config from json if available LORAConfig_fromJson(LORAdata); LORAConfig_apply(); stateLORAMeasures(); } } # endif # if simpleReceiving void XtoLORA(const char* topicOri, const char* LORAarray) { // json object decoding if (cmpToMainTopic(topicOri, subjectMQTTtoLORA)) { LoRa.beginPacket(); LoRa.print(LORAarray); LoRa.endPacket(); THEENGS_LOG_NOTICE(F("MQTTtoLORA OK" CR)); // we acknowledge the sending by publishing the value to an acknowledgement topic, for the moment even if it is a signal repetition we acknowledge also pub(subjectGTWLORAtoMQTT, LORAarray); } } # endif String stateLORAMeasures() { //Publish LORA state StaticJsonDocument jsonBuffer; JsonObject LORAdata = jsonBuffer.to(); LORAdata["frequency"] = LORAConfig.frequency; LORAdata["txpower"] = LORAConfig.txPower; LORAdata["spreadingfactor"] = LORAConfig.spreadingFactor; LORAdata["signalbandwidth"] = LORAConfig.signalBandwidth; LORAdata["codingrate"] = LORAConfig.codingRateDenominator; LORAdata["preamblelength"] = LORAConfig.preambleLength; if (LORAConfig.syncWord < 0 || LORAConfig.syncWord > 255) { THEENGS_LOG_ERROR(F("Invalid syncWord value: %d" CR), LORAConfig.syncWord); } else { char syncWordHex[5]; snprintf(syncWordHex, sizeof(syncWordHex), "0x%02X", LORAConfig.syncWord); LORAdata["syncword"] = syncWordHex; } LORAdata["enablecrc"] = LORAConfig.crc; LORAdata["invertiq"] = LORAConfig.invertIQ; LORAdata["onlyknown"] = LORAConfig.onlyKnown; LORAdata["origin"] = subjectGTWLORAtoMQTT; enqueueJsonObject(LORAdata); String output; serializeJson(LORAdata, output); return output; } #endif