[LORA] Improvements, change frequency, webUI, Oled support, add boards (#1742)

* [LORA] Capability to change LORA frequency and Add OLED

Merge of the 2 environments for TTGO V1

* Merge messages keys with upper level json

* Id as a sub topic

* Add display and state measures

* Add frequency configuration through WebUI

* Add parameters

* Add auto discovery

* Add node example for ttgo lora32 v1

* Add environments and fix scope

* Support for sync word as a byte

* Fix TX Power setting

* Add LoRa gateway description

* Add onlyknown filter
This commit is contained in:
Florian authored and GitHub committed 2023-09-27 11:12:55 -05:00
1 parent 3d20995b3a
commit 246423a8a8
18 files changed
+990 -121

No files matched your search

+317 -41
View File
@@ -37,6 +37,141 @@
# define WIPHONE_MESSAGE_MIN_LEN sizeof(wiphone_message) - WIPHONE_MAX_MESSAGE_LEN
# define WIPHONE_MAX_MESSAGE_LEN 230
LORAConfig_s LORAConfig;
# ifdef ZmqttDiscovery
SemaphoreHandle_t semaphorecreateOrUpdateDeviceLORA;
std::vector<LORAdevice*> 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) {
Log.trace(F("getDeviceById %s" CR), id);
for (std::vector<LORAdevice*>::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<LORAdevice*>::iterator it = LORAdevices.begin(); it != LORAdevices.end(); ++it) {
LORAdevice* p = *it;
Log.trace(F("uniqueId %s" CR), p->uniqueId);
Log.trace(F("modelName %s" CR), p->modelName);
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) {
Log.error(F("[LORA] semaphorecreateOrUpdateDeviceLORA Semaphore NOT taken" CR));
return;
}
LORAdevice* device = getDeviceById(id);
if (device == &NO_LORA_DEVICE_FOUND) {
Log.trace(F("add %s" CR), id);
//new device
device = new LORAdevice();
if (strlcpy(device->uniqueId, id, uniqueIdSize) > uniqueIdSize) {
Log.warning(F("[LORA] Device id %s exceeds available space" CR), id); // Remove from production release ?
};
if (strlcpy(device->modelName, model, modelNameSize) > modelNameSize) {
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 {
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(1000)) == pdFALSE) {
Log.error(F("[LORA] semaphorecreateOrUpdateDeviceLORA Semaphore NOT taken" CR));
return;
}
newLORADevices = 0;
std::vector<LORAdevice*> localDevices = LORAdevices;
xSemaphoreGive(semaphorecreateOrUpdateDeviceLORA);
for (std::vector<LORAdevice*>::iterator it = localDevices.begin(); it != localDevices.end(); ++it) {
LORAdevice* pdevice = *it;
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));
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) {
Log.trace(F("Device id %s was not discovered" CR), pdevice->uniqueId); // Remove from production release ?
}
} else {
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;
@@ -127,7 +262,109 @@ boolean _MQTTtoWiPhone(JsonObject& LORAdata) {
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<JSON_MSG_BUFFER> jsonBuffer;
preferences.begin(Gateway_Short_Name, true);
if (preferences.isKey("LORAConfig")) {
auto error = deserializeJson(jsonBuffer, preferences.getString("LORAConfig", "{}"));
preferences.end();
Log.notice(F("LORA Config loaded" CR));
if (error) {
Log.error(F("LORA Config deserialization failed: %s, buffer capacity: %u" CR), error.c_str(), jsonBuffer.capacity());
return;
}
if (jsonBuffer.isNull()) {
Log.warning(F("LORA Config is null" CR));
return;
}
JsonObject jo = jsonBuffer.as<JsonObject>();
LORAConfig_fromJson(jo);
Log.notice(F("LORA Config loaded" CR));
} else {
preferences.end();
Log.notice(F("LORA Config not found" CR));
}
}
byte hexStringToByte(const String& hexString) {
return (byte)strtol(hexString.c_str(), NULL, 16);
}
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
if (LORAdata.containsKey("syncword")) {
String syncWordStr = LORAdata["syncword"].as<String>();
LORAConfig.syncWord = hexStringToByte(syncWordStr);
Log.notice(F("Config syncword changed: %d" CR), LORAConfig.syncWord);
}
Config_update(LORAdata, "enablecrc", LORAConfig.crc);
Config_update(LORAdata, "invertiq", LORAConfig.invertIQ);
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();
if (LORAdata.containsKey("erase") && LORAdata["erase"].as<bool>()) {
// Erase config from NVS (non-volatile storage)
preferences.begin(Gateway_Short_Name, false);
if (preferences.isKey("LORAConfig")) {
int result = preferences.remove("LORAConfig");
Log.notice(F("LORA config erase result: %d" CR), result);
preferences.end();
return; // Erase prevails on save, so skipping save
} else {
Log.notice(F("LORA config not found" CR));
preferences.end();
}
}
if (LORAdata.containsKey("save") && LORAdata["save"].as<bool>()) {
StaticJsonDocument<JSON_MSG_BUFFER> jsonBuffer;
JsonObject jo = jsonBuffer.to<JsonObject>();
jo["frequency"] = LORAConfig.frequency;
// 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();
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
Log.notice(F("LORA Frequency: %d" CR), LORAConfig.frequency);
# ifdef ESP8266
SPI.begin();
# else
@@ -136,7 +373,7 @@ void setupLORA() {
LoRa.setPins(LORA_SS, LORA_RST, LORA_DI0);
if (!LoRa.begin(LORA_BAND)) {
if (!LoRa.begin(LORAConfig.frequency)) {
Log.error(F("ZgatewayLORA setup failed!" CR));
while (1)
;
@@ -148,12 +385,6 @@ void setupLORA() {
Log.notice(F("LORA_SS: %d" CR), LORA_SS);
Log.notice(F("LORA_RST: %d" CR), LORA_RST);
Log.notice(F("LORA_DI0: %d" CR), LORA_DI0);
LoRa.setTxPower(LORA_TX_POWER);
LoRa.setSpreadingFactor(LORA_SPREADING_FACTOR);
LoRa.setSignalBandwidth(LORA_SIGNAL_BANDWIDTH);
LoRa.setCodingRate4(LORA_CODING_RATE);
LoRa.setPreambleLength(LORA_PREAMBLE_LENGTH);
LoRa.setSyncWord(LORA_SYNC_WORD);
Log.trace(F("ZgatewayLORA setup done" CR));
}
@@ -179,16 +410,14 @@ void LORAtoMQTT() {
}
// Terminate with a null character in case we have a string
packet[packetSize] = 0;
LORAdata["rssi"] = (int)LoRa.packetRssi();
LORAdata["snr"] = (float)LoRa.packetSnr();
LORAdata["pferror"] = (float)LoRa.packetFrequencyError();
LORAdata["packetSize"] = (int)packetSize;
uint8_t deviceId = _determineDevice(packet, packetSize);
if (deviceId == WIPHONE) {
_WiPhoneToMQTT(packet, LORAdata);
} else if (binary) {
if (LORAConfig.onlyKnown) {
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];
_rawToHex(packet, hex, packetSize);
@@ -198,9 +427,37 @@ void LORAtoMQTT() {
LORAdata["hex"] = hex;
} else {
// ascii payload
LORAdata["message"] = packet;
deserializeJson(jsonBuffer, packet, packetSize);
}
LORAdata["rssi"] = (int)LoRa.packetRssi();
LORAdata["snr"] = (float)LoRa.packetSnr();
LORAdata["pferror"] = (float)LoRa.packetFrequencyError();
LORAdata["packetSize"] = (int)packetSize;
if (LORAdata.containsKey("id")) {
// Replace ":" in topic
std::string topic = LORAdata["id"].as<std::string>();
size_t pos = topic.find(":");
while (pos != std::string::npos) {
topic.erase(pos, 1);
pos = topic.find(":", pos);
}
std::string id = topic;
std::string subjectStr(subjectLORAtoMQTT);
topic = subjectStr + "/" + topic;
# ifdef ZmqttDiscovery
if (SYSConfig.discovery) {
if (!LORAdata.containsKey("model"))
LORAdata["model"] = "LORA_NODE";
storeLORADiscovery(LORAdata, LORAdata["model"].as<char*>(), id.c_str());
}
# endif
pub(topic.c_str(), LORAdata);
} else {
pub(subjectLORAtoMQTT, LORAdata);
}
pub(subjectLORAtoMQTT, LORAdata);
if (repeatLORAwMQTT) {
Log.trace(F("Pub LORA for rpt" CR));
pub(subjectMQTTtoLORA, LORAdata);
@@ -214,33 +471,8 @@ void MQTTtoLORA(char* topicOri, JsonObject& LORAdata) { // json object decoding
Log.trace(F("MQTTtoLORA json" CR));
const char* message = LORAdata["message"];
const char* hex = LORAdata["hex"];
int txPower = LORAdata["txpower"] | LORA_TX_POWER;
int spreadingFactor = LORAdata["spreadingfactor"] | LORA_SPREADING_FACTOR;
long int frequency = LORAdata["frequency "] | LORA_BAND;
long int signalBandwidth = LORAdata["signalbandwidth"] | LORA_SIGNAL_BANDWIDTH;
int codingRateDenominator = LORAdata["codingrate"] | LORA_CODING_RATE;
int preambleLength = LORAdata["preamblelength"] | LORA_PREAMBLE_LENGTH;
byte syncWord = LORAdata["syncword"] | LORA_SYNC_WORD;
bool crc = LORAdata["enablecrc"] | DEFAULT_CRC;
bool invertIQ = LORAdata["invertiq"] | INVERT_IQ;
LORAConfig_fromJson(LORAdata);
if (message || hex) {
LoRa.setTxPower(txPower);
LoRa.setFrequency(frequency);
LoRa.setSpreadingFactor(spreadingFactor);
LoRa.setSignalBandwidth(signalBandwidth);
LoRa.setCodingRate4(codingRateDenominator);
LoRa.setPreambleLength(preambleLength);
LoRa.setSyncWord(syncWord);
if (crc)
LoRa.enableCrc();
else
LoRa.disableCrc();
if (invertIQ)
LoRa.enableInvertIQ();
else
LoRa.disableInvertIQ();
LoRa.beginPacket();
uint8_t deviceId = _determineDevice(LORAdata);
if (deviceId == WIPHONE) {
@@ -262,6 +494,26 @@ void MQTTtoLORA(char* topicOri, JsonObject& LORAdata) { // json object decoding
Log.error(F("MQTTtoLORA Fail json" CR));
}
}
if (cmpToMainTopic(topicOri, subjectMQTTtoLORAset)) {
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<bool>()) {
// Restore the default (initial) configuration
LORAConfig_init();
} else if (LORAdata.containsKey("load") && LORAdata["load"].as<bool>()) {
// Load the saved configuration, if not initialised
LORAConfig_load();
}
// Load config from json if available
LORAConfig_fromJson(LORAdata);
stateLORAMeasures();
}
}
# endif
# if simpleReceiving
@@ -275,4 +527,28 @@ void MQTTtoLORA(char* topicOri, char* LORAdata) { // json object decoding
}
}
# endif
String stateLORAMeasures() {
//Publish LORA state
StaticJsonDocument<JSON_MSG_BUFFER> jsonBuffer;
JsonObject LORAdata = jsonBuffer.to<JsonObject>();
LORAdata["frequency"] = LORAConfig.frequency;
LORAdata["txpower"] = LORAConfig.txPower;
LORAdata["spreadingfactor"] = LORAConfig.spreadingFactor;
LORAdata["signalbandwidth"] = LORAConfig.signalBandwidth;
LORAdata["codingrate"] = LORAConfig.codingRateDenominator;
LORAdata["preamblelength"] = LORAConfig.preambleLength;
// Convert syncWord to a hexadecimal string and store it in the JSON
char syncWordHex[5]; // Enough space for 0xXX and null terminator
snprintf(syncWordHex, sizeof(syncWordHex), "0x%02X", LORAConfig.syncWord);
LORAdata["syncword"] = syncWordHex;
LORAdata["enablecrc"] = LORAConfig.crc;
LORAdata["invertiq"] = LORAConfig.invertIQ;
LORAdata["onlyknown"] = LORAConfig.onlyKnown;
pub(subjectGTWLORAtoMQTT, LORAdata);
String output;
serializeJson(LORAdata, output);
return output;
}
#endif