diff --git a/tasmota/include/i18n.h b/tasmota/include/i18n.h index 3004d1308..30cd7db5b 100644 --- a/tasmota/include/i18n.h +++ b/tasmota/include/i18n.h @@ -36,6 +36,7 @@ #define D_JSON_AP "AP" // Access Point #define D_JSON_APMAC_ADDRESS "APMac" #define D_JSON_APPENDED "Appended" +#define D_JSON_BANDWIDTH "Bandwidth" #define D_JSON_BAUDRATE "Baudrate" #define D_JSON_BLINK "Blink" #define D_JSON_BLOCKED_LOOP "Blocked Loop" @@ -47,6 +48,7 @@ #define D_JSON_BUILDDATETIME "BuildDateTime" #define D_JSON_CHANNEL "Channel" #define D_JSON_CO2 "CarbonDioxide" +#define D_JSON_CODINGRATE4 "CodingRate4" #define D_JSON_COMMAND "Command" #define D_JSON_CONFIDENCE "Confidence" #define D_JSON_CONFIG_HOLDER "CfgHolder" @@ -55,7 +57,9 @@ #define D_JSON_COREVERSION "Core" #define D_JSON_COUNT "Count" #define D_JSON_COUNTER "Counter" +#define D_JSON_CRC_BYTES "CrcBytes" #define D_JSON_CURRENT "Current" // As in Voltage and Current +#define D_JSON_CURRENT_LIMIT "CurrentLimit" #define D_JSON_CURRENT_NEUTRAL "CurrentNeutral" #define D_JSON_DARKNESS "Darkness" #define D_JSON_DATA "Data" @@ -104,6 +108,7 @@ #define D_JSON_HUMIDITY "Humidity" #define D_JSON_ID "Id" #define D_JSON_ILLUMINANCE "Illuminance" +#define D_JSON_IMPLICIT_HEADER "ImplicitHeader" #define D_JSON_IMPORT_ACTIVE "ImportActive" #define D_JSON_IMPORT_POWER "ImportPower" #define D_JSON_IMPORT_REACTIVE "ImportReactive" @@ -130,6 +135,7 @@ #define D_JSON_NONE "None" #define D_JSON_OR "or" #define D_JSON_ORP "ORP" +#define D_JSON_OUTPUT_POWER "OutputPower" #define D_JSON_O2 "Oxygen" #define D_JSON_PERIOD "Period" #define D_JSON_PH "pH" @@ -141,6 +147,7 @@ #define D_JSON_APPARENT_POWERUSAGE "ApparentPower" #define D_JSON_REACTIVE_POWERUSAGE "ReactivePower" #define D_JSON_RANGE "Range" +#define D_JSON_PREAMBLE_LENGTH "PreambleLength" #define D_JSON_PRESSURE "Pressure" #define D_JSON_PRESSUREATSEALEVEL "SeaPressure" #define D_JSON_PRESSURE_UNIT "PressureUnit" @@ -173,6 +180,7 @@ #define D_JSON_SIZE "Size" #define D_JSON_SPEED "Speed" #define D_JSON_SPEED_UNIT "SpeedUnit" +#define D_JSON_SPREADING_FACTOR "SpreadingFactor" #define D_JSON_SSID "SSId" #define D_JSON_STAGE "Stage" #define D_JSON_STARTDST "StartDST" // Start Daylight Savings Time @@ -185,6 +193,7 @@ #define D_JSON_SUNSET "Sunset" #define D_JSON_SWITCH "Switch" #define D_JSON_SYNC "Sync" +#define D_JSON_SYNCWORD "SyncWord" #define D_JSON_TEMPERATURE "Temperature" #define D_JSON_TEMPERATURE_UNIT "TempUnit" #define D_JSON_TIME "Time" diff --git a/tasmota/tasmota_xdrv_driver/xdrv_73_0_lora_struct.ino b/tasmota/tasmota_xdrv_driver/xdrv_73_0_lora_struct.ino index f69675b74..543304a3b 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_73_0_lora_struct.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_73_0_lora_struct.ino @@ -12,12 +12,24 @@ #define LORA_MAX_PACKET_LENGTH 252 // Max packet length allowed (defined by RadioLib driver) struct { + bool (* Config)(void); bool (* Available)(void); int (* Receive)(char*); bool (* Send)(char*, uint32_t); float rssi; float snr; int packet_size; + float frequency; // 868.0 MHz + float bandwidth; // 125.0 kHz + int spreading_factor; // 9 + int coding_rate; // 7 + int sync_word; // 0x12 + int output_power; // 10 dBm + long preamble_length; // 8 symbols + float current_limit; // 60.0 mA (Overcurrent Protection (OCP)) + int implicit_header; // 0 + bool crc_bytes; // 2 bytes + uint8_t gain; volatile bool receivedFlag; // flag to indicate that a packet was received volatile bool enableInterrupt; // disable interrupt when it's not needed bool sendFlag; diff --git a/tasmota/tasmota_xdrv_driver/xdrv_73_3_lora_sx126x.ino b/tasmota/tasmota_xdrv_driver/xdrv_73_3_lora_sx126x.ino index bf585a8cd..a6e5c7cb2 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_73_3_lora_sx126x.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_73_3_lora_sx126x.ino @@ -74,14 +74,76 @@ int LoraReceiveSx126x(char* data) { bool LoraSendSx126x(char* data, uint32_t len) { Lora.sendFlag = true; - int state = LoRaRadio.startTransmit(data, len); - return (RADIOLIB_ERR_NONE == state); +// int state = LoRaRadio.startTransmit(data, len); +// return (RADIOLIB_ERR_NONE == state); + // https://learn.circuit.rocks/battery-powered-lora-sensor-node + uint32_t retry_CAD = 0; + uint32_t retry_send = 0; + bool send_success = false; + while (!send_success) { +// time_t lora_time = millis(); + // Check 200ms for an opportunity to send + while (LoRaRadio.scanChannel() != RADIOLIB_CHANNEL_FREE) { + retry_CAD++; + if (retry_CAD == 20) { + // LoRa channel is busy too long, give up + +// AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: Channel is too busy, give up")); + + retry_send++; + break; + } + } + +// AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: CAD finished after %ldms tried %d times"), (millis() - loraTime), retryCAD); + + if (retry_CAD < 20) { + // Channel is free, start sending +// lora_time = millis(); + int status = LoRaRadio.transmit(data, len); + +// AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: Transmit finished after %ldms with status %d"), (millis() - loraTime), status); + + if (status == RADIOLIB_ERR_NONE) { + send_success = true; + } + else { + retry_send++; + } + } + if (retry_send == 3) { + +// AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: Failed 3 times to send data, giving up")); + + send_success = true; + } + } + return send_success; +} + +bool LoraConfigSx126x(void) { + LoRaRadio.setFrequency(Lora.frequency); + LoRaRadio.setBandwidth(Lora.bandwidth); + LoRaRadio.setSpreadingFactor(Lora.spreading_factor); + LoRaRadio.setCodingRate(Lora.coding_rate); + LoRaRadio.setSyncWord(Lora.sync_word); + LoRaRadio.setOutputPower(Lora.output_power); + LoRaRadio.setPreambleLength(Lora.preamble_length); + LoRaRadio.setCurrentLimit(Lora.current_limit); + LoRaRadio.setCRC(Lora.crc_bytes); + if (Lora.implicit_header) { + LoRaRadio.implicitHeader(Lora.implicit_header); + } else { + LoRaRadio.explicitHeader(); + } + return true; } bool LoraInitSx126x(void) { // LoRa = new Module(Pin(GPIO_LORA_CS), Pin(GPIO_LORA_DI1), Pin(GPIO_LORA_RST), Pin(GPIO_LORA_BUSY)); LoRaRadio = new Module(Pin(GPIO_LORA_CS), 33, Pin(GPIO_LORA_RST), 34); - if (RADIOLIB_ERR_NONE == LoRaRadio.begin(868.0)) { + if (RADIOLIB_ERR_NONE == LoRaRadio.begin(Lora.frequency)) { + LoraConfigSx126x(); LoRaRadio.setDio1Action(LoraOnReceiveSx126x); if (RADIOLIB_ERR_NONE == LoRaRadio.startReceive()) { return true; diff --git a/tasmota/tasmota_xdrv_driver/xdrv_73_3_lora_sx127x.ino b/tasmota/tasmota_xdrv_driver/xdrv_73_3_lora_sx127x.ino index 10f6fba2f..bbc8657a6 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_73_3_lora_sx127x.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_73_3_lora_sx127x.ino @@ -31,22 +31,33 @@ /*********************************************************************************************/ +void LoraOnCadDoneSx127x(boolean signalDetected) { + if (signalDetected) { // detect preamble + + AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: Signal detected")); + + LoRa.receive(); // put the radio into continuous receive mode + } else { + LoRa.channelActivityDetection(); // try next activity dectection + } +} + // this function is called when a complete packet is received by the module void LoraOnReceiveSx127x(int packet_size) { - if (0 == packet_size) { return; } // if there's no packet, return - if (!Lora.enableInterrupt) { return; } // check if the interrupt is enabled - Lora.packet_size = packet_size; // we got a packet, set the flag + if (0 == packet_size) { return; } // if there's no packet, return + if (!Lora.enableInterrupt) { return; } // check if the interrupt is enabled + Lora.packet_size = packet_size; // we got a packet, set the flag } bool LoraAvailableSx127x(void) { - return (Lora.packet_size > 0); // check if the flag is set + return (Lora.packet_size > 0); // check if the flag is set } int LoraReceiveSx127x(char* data) { - Lora.enableInterrupt = false; // disable the interrupt service routine while processing the data + Lora.enableInterrupt = false; // disable the interrupt service routine while processing the data int packet_size = 0; - while (LoRa.available()) { // read packet up to LORA_MAX_PACKET_LENGTH + while (LoRa.available()) { // read packet up to LORA_MAX_PACKET_LENGTH char sdata = LoRa.read(); if (packet_size < LORA_MAX_PACKET_LENGTH -1) { data[packet_size++] = sdata; @@ -55,29 +66,58 @@ int LoraReceiveSx127x(char* data) { packet_size = (Lora.sendFlag) ? 0 : +1; Lora.sendFlag = false; - Lora.packet_size = 0; // reset flag - Lora.enableInterrupt = true; // we're ready to receive more packets, enable interrupt service routine + Lora.packet_size = 0; // reset flag + Lora.enableInterrupt = true; // we're ready to receive more packets, enable interrupt service routine Lora.rssi = LoRa.packetRssi(); Lora.snr = LoRa.packetSnr(); + + LoRa.channelActivityDetection(); // put the radio into CAD mode + return packet_size; } bool LoraSendSx127x(char* data, uint32_t len) { Lora.sendFlag = true; - LoRa.beginPacket(); // start packet - LoRa.write((uint8_t*)data, len); // send message - LoRa.endPacket(); // finish packet and send it - LoRa.receive(); // go back into receive mode + LoRa.beginPacket(Lora.implicit_header); // start packet + LoRa.write((uint8_t*)data, len); // send message + LoRa.endPacket(); // finish packet and send it + LoRa.receive(); // go back into receive mode + return true; +} + +bool LoraConfigSx127x(void) { + LoRa.setFrequency(Lora.frequency * 1000 * 1000); + LoRa.setSignalBandwidth(Lora.bandwidth * 1000); + LoRa.setSpreadingFactor(Lora.spreading_factor); + LoRa.setCodingRate4(Lora.coding_rate); + LoRa.setSyncWord(Lora.sync_word); + LoRa.setTxPower(Lora.output_power); + LoRa.setPreambleLength(Lora.preamble_length); + LoRa.setOCP(Lora.current_limit); + if (Lora.crc_bytes) { + LoRa.enableCrc(); + } else { + LoRa.disableCrc(); + } +/* + if (Lora.implicit_header) { + LoRa.implicitHeaderMode(); + } else { + LoRa.explicitHeaderMode(); + } +*/ return true; } bool LoraInitSx127x(void) { LoRa.setPins(Pin(GPIO_LORA_CS), Pin(GPIO_LORA_RST), Pin(GPIO_LORA_DI0)); - if (LoRa.begin(868E6)) { -// LoRa.setSyncWord(0x12); + if (LoRa.begin(Lora.frequency * 1000 * 1000)) { + LoraConfigSx127x(); + LoRa.onCadDone(LoraOnCadDoneSx127x); // register the channel activity dectection callback LoRa.onReceive(LoraOnReceiveSx127x); - LoRa.receive(); +// LoRa.receive(); + LoRa.channelActivityDetection(); return true; } return false; diff --git a/tasmota/tasmota_xdrv_driver/xdrv_73_9_lora.ino b/tasmota/tasmota_xdrv_driver/xdrv_73_9_lora.ino index 6c1ea814c..8b45dbbee 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_73_9_lora.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_73_9_lora.ino @@ -59,6 +59,19 @@ void LoraInit(void) { SPI.begin(Pin(GPIO_SPI_CLK), Pin(GPIO_SPI_MISO), Pin(GPIO_SPI_MOSI), -1); #endif // ESP32 + Lora.frequency = 868.0; // MHz + Lora.bandwidth = 125.0; // kHz + Lora.spreading_factor = 9; + Lora.coding_rate = 7; + Lora.sync_word = 0x12; + Lora.output_power = 10; // dBm + Lora.preamble_length = 8; // symbols + Lora.current_limit = 60.0; // mA (Overcurrent Protection (OCP)) + Lora.implicit_header = 0; // explicit + Lora.crc_bytes = 2; // bytes + + Lora.enableInterrupt = true; + char hardware[20]; if (false) { } @@ -66,6 +79,7 @@ void LoraInit(void) { else if (PinUsed(GPIO_LORA_DI0)) { // SX1276, RFM95W if (LoraInitSx127x()) { + Lora.Config = &LoraConfigSx127x; Lora.Available = &LoraAvailableSx127x; Lora.Receive = &LoraReceiveSx127x; Lora.Send = &LoraSendSx127x; @@ -77,6 +91,7 @@ void LoraInit(void) { #ifdef USE_LORA_SX126X else if (LoraInitSx126x()) { // SX1262, LilyGoT3S3 + Lora.Config = &LoraConfigSx126x; Lora.Available = &LoraAvailableSx126x; Lora.Receive = &LoraReceiveSx126x; Lora.Send = &LoraSendSx126x; @@ -87,9 +102,6 @@ void LoraInit(void) { else { strcpy_P(hardware, PSTR("Not")); } - if (Lora.present) { - Lora.enableInterrupt = true; - } AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: %s initialized"), hardware); } } @@ -99,12 +111,13 @@ void LoraInit(void) { \*********************************************************************************************/ #define D_CMND_LORASEND "Send" +#define D_CMND_LORACONFIG "Config" const char kLoraCommands[] PROGMEM = "LoRa|" // Prefix - D_CMND_LORASEND; + D_CMND_LORASEND "|" D_CMND_LORACONFIG; void (* const LoraCommand[])(void) PROGMEM = { - &CmndLoraSend }; + &CmndLoraSend, &CmndLoraConfig }; void CmndLoraSend(void) { // LoRaSend "Hello Tiger" - Send "Hello Tiger\n" @@ -168,6 +181,40 @@ void CmndLoraSend(void) { } } +void CmndLoraConfig(void) { + // LoRaConfig - Show all parameters + // LoRaConfig {"Frequency":868.0,"Bandwidth":125.0} - Enter float parameters + // LoRaConfig {"SyncWord":18} - Enter decimal parameter (=0x12) + if (XdrvMailbox.data_len > 0) { + JsonParser parser(XdrvMailbox.data); + JsonParserObject root = parser.getRootObject(); + if (root) { + Lora.frequency = root.getFloat(PSTR(D_JSON_FREQUENCY), Lora.frequency); + Lora.bandwidth = root.getFloat(PSTR(D_JSON_BANDWIDTH), Lora.bandwidth); + Lora.spreading_factor = root.getUInt(PSTR(D_JSON_SPREADING_FACTOR), Lora.spreading_factor); + Lora.coding_rate = root.getUInt(PSTR(D_JSON_CODINGRATE4), Lora.coding_rate); + Lora.sync_word = root.getUInt(PSTR(D_JSON_SYNCWORD), Lora.sync_word); + Lora.output_power = root.getUInt(PSTR(D_JSON_OUTPUT_POWER), Lora.output_power); + Lora.preamble_length = root.getUInt(PSTR(D_JSON_PREAMBLE_LENGTH), Lora.preamble_length); + Lora.current_limit = root.getFloat(PSTR(D_JSON_CURRENT_LIMIT), Lora.current_limit); + Lora.implicit_header = root.getUInt(PSTR(D_JSON_IMPLICIT_HEADER), Lora.implicit_header); + Lora.crc_bytes = root.getUInt(PSTR(D_JSON_CRC_BYTES), Lora.crc_bytes); + Lora.Config(); + } + } + ResponseCmnd(); // {"LoRaConfig": + ResponseAppend_P(PSTR("{\"" D_JSON_FREQUENCY "\":%1_f"), &Lora.frequency); // xxx.x MHz + ResponseAppend_P(PSTR(",\"" D_JSON_BANDWIDTH "\":%1_f"), &Lora.bandwidth); // xxx.x kHz + ResponseAppend_P(PSTR(",\"" D_JSON_SPREADING_FACTOR "\":%d"), Lora.spreading_factor); + ResponseAppend_P(PSTR(",\"" D_JSON_CODINGRATE4 "\":%d"), Lora.coding_rate); + ResponseAppend_P(PSTR(",\"" D_JSON_SYNCWORD "\":%d"), Lora.sync_word); + ResponseAppend_P(PSTR(",\"" D_JSON_OUTPUT_POWER "\":%d"), Lora.output_power); // dBm + ResponseAppend_P(PSTR(",\"" D_JSON_PREAMBLE_LENGTH "\":%d"), Lora.preamble_length); // symbols + ResponseAppend_P(PSTR(",\"" D_JSON_CURRENT_LIMIT "\":%1_f"), &Lora.current_limit); // xx.x mA (Overcurrent Protection - OCP) + ResponseAppend_P(PSTR(",\"" D_JSON_IMPLICIT_HEADER "\":%d"), Lora.implicit_header); // 0 = explicit + ResponseAppend_P(PSTR(",\"" D_JSON_CRC_BYTES "\":%d}}"), Lora.crc_bytes); // bytes +} + /*********************************************************************************************\ * Interface \*********************************************************************************************/