Tasmota/tasmota/tasmota_xdrv_driver/xdrv_73_9_lora.ino

488 lines
17 KiB
C++

/*
xdrv_73_9_lora.ino - LoRa support for Tasmota
SPDX-FileCopyrightText: 2024 Theo Arends
SPDX-License-Identifier: GPL-3.0-only
*/
#ifdef USE_SPI
#ifdef USE_SPI_LORA
/*********************************************************************************************\
* LoRa
\*********************************************************************************************/
#define XDRV_73 73
/*********************************************************************************************/
void LoraDefaults(void) {
Lora->settings.frequency = TAS_LORA_FREQUENCY;
Lora->settings.bandwidth = TAS_LORA_BANDWIDTH;
Lora->settings.spreading_factor = TAS_LORA_SPREADING_FACTOR;
Lora->settings.coding_rate = TAS_LORA_CODING_RATE;
Lora->settings.sync_word = TAS_LORA_SYNC_WORD;
Lora->settings.output_power = TAS_LORA_OUTPUT_POWER;
Lora->settings.preamble_length = TAS_LORA_PREAMBLE_LENGTH;
Lora->settings.current_limit = TAS_LORA_CURRENT_LIMIT;
Lora->settings.implicit_header = TAS_LORA_HEADER;
Lora->settings.crc_bytes = TAS_LORA_CRC_BYTES;
}
void LoraWanDefaults(void) {
Lora->settings.frequency = TAS_LORAWAN_FREQUENCY;
Lora->settings.bandwidth = TAS_LORAWAN_BANDWIDTH;
Lora->settings.spreading_factor = TAS_LORAWAN_SPREADING_FACTOR;
Lora->settings.coding_rate = TAS_LORAWAN_CODING_RATE;
Lora->settings.sync_word = TAS_LORAWAN_SYNC_WORD;
Lora->settings.output_power = TAS_LORAWAN_OUTPUT_POWER;
Lora->settings.preamble_length = TAS_LORAWAN_PREAMBLE_LENGTH;
Lora->settings.current_limit = TAS_LORAWAN_CURRENT_LIMIT;
Lora->settings.implicit_header = TAS_LORAWAN_HEADER;
Lora->settings.crc_bytes = TAS_LORAWAN_CRC_BYTES;
}
void LoraSettings2Json(void) {
ResponseAppend_P(PSTR("\"" D_JSON_FREQUENCY "\":%1_f"), &Lora->settings.frequency); // xxx.x MHz
ResponseAppend_P(PSTR(",\"" D_JSON_BANDWIDTH "\":%1_f"), &Lora->settings.bandwidth); // xxx.x kHz
ResponseAppend_P(PSTR(",\"" D_JSON_SPREADING_FACTOR "\":%d"), Lora->settings.spreading_factor);
ResponseAppend_P(PSTR(",\"" D_JSON_CODINGRATE4 "\":%d"), Lora->settings.coding_rate);
ResponseAppend_P(PSTR(",\"" D_JSON_SYNCWORD "\":%d"), Lora->settings.sync_word);
ResponseAppend_P(PSTR(",\"" D_JSON_OUTPUT_POWER "\":%d"), Lora->settings.output_power); // dBm
ResponseAppend_P(PSTR(",\"" D_JSON_PREAMBLE_LENGTH "\":%d"), Lora->settings.preamble_length); // symbols
ResponseAppend_P(PSTR(",\"" D_JSON_CURRENT_LIMIT "\":%1_f"), &Lora->settings.current_limit); // xx.x mA (Overcurrent Protection - OCP)
ResponseAppend_P(PSTR(",\"" D_JSON_IMPLICIT_HEADER "\":%d"), Lora->settings.implicit_header); // 0 = explicit
ResponseAppend_P(PSTR(",\"" D_JSON_CRC_BYTES "\":%d"), Lora->settings.crc_bytes); // bytes
}
void LoraJson2Settings(JsonParserObject root) {
Lora->settings.frequency = root.getFloat(PSTR(D_JSON_FREQUENCY), Lora->settings.frequency);
Lora->settings.bandwidth = root.getFloat(PSTR(D_JSON_BANDWIDTH), Lora->settings.bandwidth);
Lora->settings.spreading_factor = root.getUInt(PSTR(D_JSON_SPREADING_FACTOR), Lora->settings.spreading_factor);
Lora->settings.coding_rate = root.getUInt(PSTR(D_JSON_CODINGRATE4), Lora->settings.coding_rate);
Lora->settings.sync_word = root.getUInt(PSTR(D_JSON_SYNCWORD), Lora->settings.sync_word);
Lora->settings.output_power = root.getUInt(PSTR(D_JSON_OUTPUT_POWER), Lora->settings.output_power);
Lora->settings.preamble_length = root.getUInt(PSTR(D_JSON_PREAMBLE_LENGTH), Lora->settings.preamble_length);
Lora->settings.current_limit = root.getFloat(PSTR(D_JSON_CURRENT_LIMIT), Lora->settings.current_limit);
Lora->settings.implicit_header = root.getUInt(PSTR(D_JSON_IMPLICIT_HEADER), Lora->settings.implicit_header);
Lora->settings.crc_bytes = root.getUInt(PSTR(D_JSON_CRC_BYTES), Lora->settings.crc_bytes);
}
/*********************************************************************************************\
* Driver Settings load and save
\*********************************************************************************************/
#ifdef USE_UFILESYS
#define XDRV_73_KEY "drvset73"
bool LoraLoadData(void) {
char key[] = XDRV_73_KEY;
String json = UfsJsonSettingsRead(key);
if (json.length() == 0) { return false; }
// {"Crc":1882268982,"Flags":0,"Frequency":868.1,"Bandwidth":125.0,"SpreadingFactor":7,"CodingRate4":5,"SyncWord":52,"OutputPower":10,"PreambleLength":8,"CurrentLimit":60.0,"ImplicitHeader":0,"CrcBytes":2}
JsonParser parser((char*)json.c_str());
JsonParserObject root = parser.getRootObject();
if (!root) { return false; }
Lora->settings.crc32 = root.getUInt(PSTR("Crc"), Lora->settings.crc32);
Lora->settings.flags = root.getUInt(PSTR("Flags"), Lora->settings.flags);
LoraJson2Settings(root);
#ifdef USE_LORAWAN_BRIDGE
if (!LoraWanLoadData()) {
return false;
}
#endif // USE_LORAWAN_BRIDGE
return true;
}
bool LoraSaveData(void) {
Response_P(PSTR("{\"" XDRV_73_KEY "\":{"
"\"Crc\":%u,"
"\"Flags\":%u,"),
Lora->settings.crc32,
Lora->settings.flags);
LoraSettings2Json();
ResponseAppend_P(PSTR("}}"));
if (!UfsJsonSettingsWrite(ResponseData())) {
return false;
}
#ifdef USE_LORAWAN_BRIDGE
if (!LoraWanSaveData()) {
return false;
}
#endif // USE_LORAWAN_BRIDGE
return true;
}
void LoraDeleteData(void) {
char key[] = XDRV_73_KEY;
UfsJsonSettingsDelete(key); // Use defaults
#ifdef USE_LORAWAN_BRIDGE
LoraWanDeleteData();
#endif // USE_LORAWAN_BRIDGE
}
#endif // USE_UFILESYS
/*********************************************************************************************/
void LoraSettingsLoad(bool erase) {
// Called from FUNC_PRE_INIT (erase = 0) once at restart
// Called from FUNC_RESET_SETTINGS (erase = 1) after command reset 4, 5, or 6
// *** Start init default values in case key is not found ***
memset(&Lora->settings, 0x00, sizeof(LoraSettings_t));
// Init any other parameter in struct LoraSettings
LoraDefaults();
// *** End Init default values ***
#ifndef USE_UFILESYS
AddLog(LOG_LEVEL_DEBUG, PSTR("CFG: Lora use defaults as file system not enabled"));
#else
// Try to load key
if (erase) {
LoraDeleteData();
}
else if (LoraLoadData()) {
AddLog(LOG_LEVEL_DEBUG, PSTR("CFG: Lora loaded from file"));
}
else {
// File system not ready: No flash space reserved for file system
AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("CFG: Lora use defaults as file system not ready or key not found"));
}
#endif // USE_UFILESYS
}
void LoraSettingsSave(void) {
// Called from FUNC_SAVE_SETTINGS every SaveData second and at restart
#ifdef USE_UFILESYS
uint32_t crc32 = GetCfgCrc32((uint8_t*)&Lora->settings +4, sizeof(LoraSettings_t) -4); // Skip crc32
if (crc32 != Lora->settings.crc32) {
Lora->settings.crc32 = crc32;
if (LoraSaveData()) {
AddLog(LOG_LEVEL_DEBUG, PSTR("CFG: Lora saved to file"));
} else {
// File system not ready: No flash space reserved for file system
AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("CFG: Lora ERROR File system not ready or unable to save file"));
}
}
#endif // USE_UFILESYS
}
/*********************************************************************************************/
bool LoraSend(uint8_t* data, uint32_t len, bool invert) {
uint32_t lora_time = millis(); // Time is important for LoRaWan RX windows
bool result = Lora->Send(data, len, invert);
AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("LOR: Send (%u) '%*_H', Invert %d, Time %d"),
lora_time, len, data, invert, TimePassedSince(lora_time));
return result;
}
void LoraInput(void) {
if (!Lora->Available()) { return; }
char data[TAS_LORA_MAX_PACKET_LENGTH] = { 0 };
int packet_size = Lora->Receive(data);
if (!packet_size) { return; }
AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("LOR: Rcvd (%u) '%*_H', RSSI %1_f, SNR %1_f"),
Lora->receive_time, packet_size, data, &Lora->rssi, &Lora->snr);
#ifdef USE_LORAWAN_BRIDGE
if (bitRead(Lora->settings.flags, TAS_LORA_FLAG_BRIDGE_ENABLED)) {
if (LoraWanInput((uint8_t*)data, packet_size)) {
return;
}
}
#endif // USE_LORAWAN_BRIDGE
Lora->receive_time = 0;
if (TAS_LORA_REMOTE_COMMAND == data[0]) {
char *payload = data +1; // Skip TAS_LORA_REMOTE_COMMAND
char *command_part;
char *topic_part = strtok_r(payload, " ", &command_part);
if (topic_part && command_part) {
if (!strcasecmp(topic_part, SettingsText(SET_MQTT_TOPIC))) { // Is it mine
ExecuteCommand(command_part, SRC_REMOTE);
return;
} else {
*--command_part = ' '; // Restore strtok_r '/0'
}
}
}
bool raw = Lora->raw;
// Set raw mode if zeroes within data
for (uint32_t i = 0; i < packet_size; i++) {
if (0 == data[i]) {
raw = true;
break;
}
}
bool assume_json = (!raw && (data[0] == '{'));
Response_P(PSTR("{\"LoRaReceived\":"));
if (assume_json) {
ResponseAppend_P(data);
} else {
ResponseAppend_P(PSTR("\""));
if (raw) {
ResponseAppend_P(PSTR("%*_H"), packet_size, data);
} else {
ResponseAppend_P(EscapeJSONString(data).c_str());
}
ResponseAppend_P(PSTR("\""));
}
ResponseAppend_P(PSTR(",\"RSSI\":%1_f,\"SNR\":%1_f}"), &Lora->rssi, &Lora->snr);
MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR("LoRaReceived"));
}
void LoraInit(void) {
if ((SPI_MOSI_MISO == TasmotaGlobal.spi_enabled) &&
(PinUsed(GPIO_LORA_CS)) && (PinUsed(GPIO_LORA_RST))) {
#ifdef ESP8266
SPI.begin();
#endif // ESP8266
#ifdef ESP32
SPI.begin(Pin(GPIO_SPI_CLK), Pin(GPIO_SPI_MISO), Pin(GPIO_SPI_MOSI), -1);
// SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
#endif // ESP32
Lora = (Lora_t*)calloc(sizeof(Lora_t), 1); // Need calloc to reset registers to 0/false
if (nullptr == Lora) { return; }
LoraSettingsLoad(0);
#ifdef USE_LORAWAN_BRIDGE
LoraWanInit();
#endif // USE_LORAWAN_BRIDGE
bool present = false;
char hardware[20];
if (false) {
// Need this as following `else if`s may not be present
}
#ifdef USE_LORA_SX127X
else if (PinUsed(GPIO_LORA_DI0)) {
// SX1276, RFM95W
if (LoraSx127xInit()) {
Lora->Config = &LoraSx127xConfig;
Lora->Available = &LoraSx127xAvailable;
Lora->Receive = &LoraSx127xReceive;
Lora->Send = &LoraSx127xSend;
strcpy_P(hardware, PSTR("SX127x"));
present = true;
}
}
#endif // USE_LORA_SX127X
#ifdef USE_LORA_SX126X
else if (PinUsed(GPIO_LORA_DI1) && PinUsed(GPIO_LORA_BUSY)) {
// SX1262, LilyGoT3S3
if (LoraSx126xInit()) {
Lora->Config = &LoraSx126xConfig;
Lora->Available = &LoraSx126xAvailable;
Lora->Receive = &LoraSx126xReceive;
Lora->Send = &LoraSx126xSend;
strcpy_P(hardware, PSTR("SX126x"));
present = true;
}
}
#endif // USE_LORA_SX126X
if (present) {
AddLog(LOG_LEVEL_INFO, PSTR("LOR: %s initialized"), hardware);
} else {
free(Lora);
Lora = nullptr;
}
}
}
/*********************************************************************************************\
* Commands
\*********************************************************************************************/
#define D_CMND_LORASEND "Send"
#define D_CMND_LORACONFIG "Config"
#define D_CMND_LORACOMMAND "Command"
#define D_CMND_LORAOPTION "Option"
const char kLoraCommands[] PROGMEM = "LoRa|" // Prefix
D_CMND_LORASEND "|" D_CMND_LORACONFIG "|" D_CMND_LORACOMMAND "|" D_CMND_LORAOPTION;
void (* const LoraCommand[])(void) PROGMEM = {
&CmndLoraSend, &CmndLoraConfig, &CmndLoraCommand, &CmndLoraOption };
void CmndLoraOption(void) {
// LoraOption1 1 - Enable LoRaWanBridge
// LoraOption2 1 - Enable LoRaWanBridge Join
// LoraOption3 1 - Enable LoRaWanBridge decoding
if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= 8)) {
uint32_t pindex = XdrvMailbox.index -1;
if (XdrvMailbox.payload >= 0) {
bitWrite(Lora->settings.flags, pindex, XdrvMailbox.payload);
}
ResponseCmndIdxChar(GetStateText(bitRead(Lora->settings.flags, pindex)));
}
}
void CmndLoraCommand(void) {
// LoRaCommand <topic_of_lora_receiver> <command>
// LoRaCommand lorareceiver power 2
// LoRaCommand lorareceiver publish cmnd/anytopic/power 2
// LoRaCommand lorareceiver LoRaCommand thisreceiver status
if (XdrvMailbox.data_len > 0) {
char data[TAS_LORA_MAX_PACKET_LENGTH] = { 0 };
XdrvMailbox.data_len++; // Add Signal CmndLoraCommand to lora receiver
uint32_t len = (XdrvMailbox.data_len < TAS_LORA_MAX_PACKET_LENGTH -1) ? XdrvMailbox.data_len : TAS_LORA_MAX_PACKET_LENGTH -2;
data[0] = TAS_LORA_REMOTE_COMMAND;
strlcpy(data +1, XdrvMailbox.data, len);
LoraSend((uint8_t*)data, len, false);
ResponseCmndDone();
}
}
void CmndLoraSend(void) {
// LoRaSend "Hello Tiger" - Send "Hello Tiger\n"
// LoRaSend - Set to text decoding
// LoRaSend1 "Hello Tiger" - Send "Hello Tiger\n"
// LoRaSend2 "Hello Tiger" - Send "Hello Tiger"
// LoRaSend3 "Hello Tiger" - Send "Hello Tiger\f"
// LoRaSend4 - Set to binary decoding
// LoRaSend4 "Hello Tiger" - Send "Hello Tiger" and set to binary decoding
// LoRaSend5 "AA004566" - Send "AA004566" as hex values
// LoRaSend6 "72,101,108,108" - Send decimals as hex values
// LoRaSend15 "AA004566" - Send "AA004566" as hex values with invert IQ
bool invert = false;
if (XdrvMailbox.index > 9) {
XdrvMailbox.index -= 10;
invert = true;
}
if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= 6)) {
Lora->raw = (XdrvMailbox.index > 3); // Global flag set even without data
if (XdrvMailbox.data_len > 0) {
char data[TAS_LORA_MAX_PACKET_LENGTH] = { 0 };
uint32_t len = (XdrvMailbox.data_len < TAS_LORA_MAX_PACKET_LENGTH -1) ? XdrvMailbox.data_len : TAS_LORA_MAX_PACKET_LENGTH -2;
#ifdef USE_LORA_DEBUG
// AddLog(LOG_LEVEL_DEBUG, PSTR("DBG: Len %d, Send %*_H"), len, len, XdrvMailbox.data);
#endif
if (1 == XdrvMailbox.index) { // "Hello Tiger\n"
memcpy(data, XdrvMailbox.data, len);
data[len++] = '\n';
}
else if ((2 == XdrvMailbox.index) || (4 == XdrvMailbox.index)) { // "Hello Tiger" or "A0"
memcpy(data, XdrvMailbox.data, len);
}
else if (3 == XdrvMailbox.index) { // "Hello\f"
Unescape(XdrvMailbox.data, &len);
memcpy(data, XdrvMailbox.data, len);
}
else if (5 == XdrvMailbox.index) { // "AA004566" as hex values
char *p;
char stemp[3];
char *codes = RemoveSpace(XdrvMailbox.data);
int size = strlen(XdrvMailbox.data);
len = 0;
while (size > 1) {
strlcpy(stemp, codes, sizeof(stemp));
data[len++] = strtol(stemp, &p, 16);
if (len > TAS_LORA_MAX_PACKET_LENGTH -2) { break; }
size -= 2;
codes += 2;
}
}
else if (6 == XdrvMailbox.index) { // "72,101,108,108"
char *p;
uint8_t code;
char *values = XdrvMailbox.data;
len = 0;
for (char* str = strtok_r(values, ",", &p); str; str = strtok_r(nullptr, ",", &p)) {
data[len++] = (uint8_t)atoi(str);
if (len > TAS_LORA_MAX_PACKET_LENGTH -2) { break; }
}
}
else {
len = 0;
}
if (len) {
LoraSend((uint8_t*)data, len, invert);
}
ResponseCmndDone();
}
}
}
void CmndLoraConfig(void) {
// LoRaConfig - Show all parameters
// LoRaConfig 1 - Set default parameters
// LoRaConfig 2 - Set default LoRaWan bridge parameters
// LoRaConfig {"Frequency":868.0,"Bandwidth":125.0} - Enter float parameters
// LoRaConfig {"SyncWord":18} - Enter decimal parameter (=0x12)
if (XdrvMailbox.data_len > 0) {
if (XdrvMailbox.payload == 1) {
LoraDefaults();
Lora->Config();
}
else if (XdrvMailbox.payload == 2) {
LoraWanDefaults();
Lora->Config();
}
else {
JsonParser parser(XdrvMailbox.data);
JsonParserObject root = parser.getRootObject();
if (root) {
LoraJson2Settings(root);
Lora->Config();
}
}
}
ResponseCmnd(); // {"LoRaConfig":
ResponseAppend_P(PSTR("{"));
LoraSettings2Json();
ResponseAppend_P(PSTR("}}"));
}
/*********************************************************************************************\
* Interface
\*********************************************************************************************/
bool Xdrv73(uint32_t function) {
bool result = false;
if (FUNC_INIT == function) {
LoraInit();
}
else if (Lora) {
switch (function) {
case FUNC_LOOP:
case FUNC_SLEEP_LOOP:
LoraInput();
break;
case FUNC_RESET_SETTINGS:
LoraSettingsLoad(1);
break;
case FUNC_SAVE_SETTINGS:
LoraSettingsSave();
break;
case FUNC_COMMAND:
result = DecodeCommand(kLoraCommands, LoraCommand);
#ifdef USE_LORAWAN_BRIDGE
if (!result) {
result = DecodeCommand(kLoraWanCommands, LoraWanCommand);
}
#endif // USE_LORAWAN_BRIDGE
break;
case FUNC_ACTIVE:
result = true;
break;
}
}
return result;
}
#endif // USE_SPI_LORA
#endif // USE_SPI