#include "src/Helpers/_CPlugin_Helper.h" #ifdef USES_C018 // ####################################################################################################### // ########################### Controller Plugin 018: LoRa TTN - RN2483/RN2903 ########################### // ####################################################################################################### # define CPLUGIN_018 # define CPLUGIN_ID_018 18 # define CPLUGIN_NAME_018 "LoRa TTN - RN2483/RN2903" # include # include "src/ControllerQueue/C018_queue_element.h" # include "src/Controller_config/C018_config.h" # include "src/Controller_struct/C018_data_struct.h" # include "src/DataTypes/ESPEasy_plugin_functions.h" # include "src/Globals/CPlugins.h" # include "src/Helpers/_Plugin_Helper_serial.h" # include "src/Helpers/StringGenerator_GPIO.h" # include "src/WebServer/Markup.h" # include "src/WebServer/Markup_Forms.h" # include "src/WebServer/HTML_wrappers.h" // Have this define after the includes, so we can set it in Custom.h # ifndef C018_FORCE_SW_SERIAL # define C018_FORCE_SW_SERIAL false # endif // ifndef C018_FORCE_SW_SERIAL // FIXME TD-er: Must add a controller data struct vector, like with plugins. C018_data_struct *C018_data = nullptr; // Forward declarations bool C018_init(struct EventStruct *event); String c018_add_joinChanged_script_element_line(const String& id, bool forOTAA); bool CPlugin_018(CPlugin::Function function, struct EventStruct *event, String& string) { bool success = false; switch (function) { case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: { ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_018; proto.usesMQTT = false; proto.usesAccount = true; proto.usesPassword = true; proto.defaultPort = 1; proto.usesID = true; proto.usesHost = false; proto.usesCheckReply = false; proto.usesTimeout = false; proto.usesSampleSets = true; proto.needsNetwork = false; break; } case CPlugin::Function::CPLUGIN_GET_DEVICENAME: { string = F(CPLUGIN_NAME_018); break; } case CPlugin::Function::CPLUGIN_WEBFORM_SHOW_HOST_CONFIG: { if ((C018_data != nullptr) && C018_data->isInitialized()) { string = F("Dev addr: "); string += C018_data->getDevaddr(); string += C018_data->useOTAA() ? F(" (OTAA)") : F(" (ABP)"); } else { string = F("-"); } break; } case CPlugin::Function::CPLUGIN_INIT: { success = init_c018_delay_queue(event->ControllerIndex); if (success) { C018_init(event); } break; } case CPlugin::Function::CPLUGIN_EXIT: { if (C018_data != nullptr) { C018_data->reset(); delete C018_data; C018_data = nullptr; } exit_c018_delay_queue(); break; } case CPlugin::Function::CPLUGIN_WEBFORM_LOAD: { { // Script to toggle visibility of OTAA/ABP field, based on the activation method selector. protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(event->ControllerIndex); html_add_script(false); addHtml(F("function joinChanged(elem){ var styleOTAA = elem.value == 0 ? '' : 'none'; var styleABP = elem.value == 1 ? '' : 'none';")); addHtml(c018_add_joinChanged_script_element_line(getControllerParameterInternalName(ProtocolIndex, ControllerSettingsStruct::CONTROLLER_USER), true)); addHtml(c018_add_joinChanged_script_element_line(getControllerParameterInternalName(ProtocolIndex, ControllerSettingsStruct::CONTROLLER_PASS), true)); addHtml(c018_add_joinChanged_script_element_line(F("deveui"), true)); addHtml(c018_add_joinChanged_script_element_line(F("deveui_note"), true)); addHtml(c018_add_joinChanged_script_element_line(F("devaddr"), false)); addHtml(c018_add_joinChanged_script_element_line(F("nskey"), false)); addHtml(c018_add_joinChanged_script_element_line(F("appskey"), false)); addHtml('}'); html_add_script_end(); } { // Keep this object in a small scope so we can destruct it as soon as possible again. std::shared_ptr customConfig; { // Try to allocate on 2nd heap # ifdef USE_SECOND_HEAP // HeapSelectIram ephemeral; # endif // ifdef USE_SECOND_HEAP std::shared_ptr tmp_shared(new (std::nothrow) C018_ConfigStruct); customConfig = std::move(tmp_shared); } if (!customConfig) { break; } LoadCustomControllerSettings(event->ControllerIndex, reinterpret_cast(customConfig.get()), sizeof(C018_ConfigStruct)); customConfig->webform_load(C018_data); } break; } case CPlugin::Function::CPLUGIN_WEBFORM_SAVE: { std::shared_ptr customConfig; { // Try to allocate on 2nd heap # ifdef USE_SECOND_HEAP // HeapSelectIram ephemeral; # endif // ifdef USE_SECOND_HEAP std::shared_ptr tmp_shared(new (std::nothrow) C018_ConfigStruct); customConfig = std::move(tmp_shared); } if (customConfig) { customConfig->webform_save(); SaveCustomControllerSettings(event->ControllerIndex, reinterpret_cast(customConfig.get()), sizeof(C018_ConfigStruct)); } break; } case CPlugin::Function::CPLUGIN_GET_PROTOCOL_DISPLAY_NAME: { success = true; switch (event->idx) { case ControllerSettingsStruct::CONTROLLER_USER: string = F("AppEUI"); break; case ControllerSettingsStruct::CONTROLLER_PASS: string = F("AppKey"); break; case ControllerSettingsStruct::CONTROLLER_TIMEOUT: string = F("Module Timeout"); break; case ControllerSettingsStruct::CONTROLLER_PORT: string = F("Port"); break; default: success = false; break; } break; } case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: { if (C018_DelayHandler == nullptr) { break; } if (C018_DelayHandler->queueFull(event->ControllerIndex)) { break; } if (C018_data != nullptr) { std::unique_ptr element(new C018_queue_element(event, C018_data->getSampleSetCount(event->TaskIndex))); success = C018_DelayHandler->addToQueue(std::move(element)); Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C018_DELAY_QUEUE, C018_DelayHandler->getNextScheduleTime()); if (!C018_data->isInitialized()) { // Sometimes the module does need some time after power on to respond. // So it may not be initialized well at the call of CPLUGIN_INIT // We try to trigger its init again when sending data. C018_init(event); } } break; } case CPlugin::Function::CPLUGIN_PROTOCOL_RECV: { // FIXME TD-er: WHen should this be scheduled? // protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(event->ControllerIndex); // schedule_controller_event_timer(ProtocolIndex, CPlugin::Function::CPLUGIN_PROTOCOL_RECV, event); break; } case CPlugin::Function::CPLUGIN_WRITE: { if (C018_data != nullptr) { if (C018_data->isInitialized()) { const String command = parseString(string, 1); if (equals(command, F("lorawan"))) { const String subcommand = parseString(string, 2); if (equals(subcommand, F("write"))) { const String loraWriteCommand = parseStringToEnd(string, 3); const String res = C018_data->sendRawCommand(loraWriteCommand); String logstr = F("LoRaWAN cmd: "); logstr += loraWriteCommand; logstr += F(" -> "); logstr += res; addLog(LOG_LEVEL_INFO, logstr); SendStatus(event, logstr); success = true; } } } } break; } case CPlugin::Function::CPLUGIN_FIFTY_PER_SECOND: { if (C018_data != nullptr) { C018_data->async_loop(); } // FIXME TD-er: Handle reading error state or return values. break; } case CPlugin::Function::CPLUGIN_FLUSH: { process_c018_delay_queue(); delay(0); break; } default: break; } return success; } bool C018_init(struct EventStruct *event) { String AppEUI; String AppKey; taskIndex_t SampleSetInitiator = INVALID_TASK_INDEX; unsigned int Port = 0; // Check if the object is already created. // If so, delete it to make sure the module is initialized according to the full set parameters. if (C018_data != nullptr) { C018_data->reset(); delete C018_data; C018_data = nullptr; } C018_data = new (std::nothrow) C018_data_struct; if (C018_data == nullptr) { return false; } { // Allocate ControllerSettings object in a scope, so we can destruct it as soon as possible. MakeControllerSettings(ControllerSettings); // -V522 if (!AllocatedControllerSettings()) { return false; } LoadControllerSettings(event->ControllerIndex, *ControllerSettings); C018_DelayHandler->cacheControllerSettings(*ControllerSettings); AppEUI = getControllerUser(event->ControllerIndex, *ControllerSettings); AppKey = getControllerPass(event->ControllerIndex, *ControllerSettings); SampleSetInitiator = ControllerSettings->SampleSetInitiator; Port = ControllerSettings->Port; } std::shared_ptr customConfig; { // Try to allocate on 2nd heap # ifdef USE_SECOND_HEAP // HeapSelectIram ephemeral; # endif // ifdef USE_SECOND_HEAP std::shared_ptr tmp_shared(new (std::nothrow) C018_ConfigStruct); customConfig = std::move(tmp_shared); } if (!customConfig) { return false; } LoadCustomControllerSettings(event->ControllerIndex, reinterpret_cast(customConfig.get()), sizeof(C018_ConfigStruct)); customConfig->validate(); if (!C018_data->init(customConfig->serialPort, customConfig->rxpin, customConfig->txpin, customConfig->baudrate, (customConfig->joinmethod == C018_USE_OTAA), SampleSetInitiator, customConfig->resetpin)) { return false; } C018_data->setFrequencyPlan(static_cast(customConfig->frequencyplan), customConfig->rx2_freq); if (!C018_data->setSF(customConfig->sf)) { return false; } if (!C018_data->setAdaptiveDataRate(customConfig->adr != 0)) { return false; } if (!C018_data->setTTNstack(static_cast(customConfig->stackVersion))) { return false; } if (customConfig->joinmethod == C018_USE_OTAA) { if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = F("OTAA: AppEUI: "); log += AppEUI; log += F(" AppKey: "); log += AppKey; log += F(" DevEUI: "); log += customConfig->DeviceEUI; addLogMove(LOG_LEVEL_INFO, log); } if (!C018_data->initOTAA(AppEUI, AppKey, customConfig->DeviceEUI)) { return false; } } else { if (!C018_data->initABP(customConfig->DeviceAddr, customConfig->AppSessionKey, customConfig->NetworkSessionKey)) { return false; } } if (!C018_data->txUncnf(F("ESPeasy (TTN)"), Port)) { return false; } return true; } // Uncrustify may change this into multi line, which will result in failed builds // *INDENT-OFF* bool do_process_c018_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { const C018_queue_element& element = static_cast(element_base); // *INDENT-ON* uint8_t pl = (element.packed.length() / 2); float airtime_ms = C018_data->getLoRaAirTime(pl); bool mustSetDelay = false; bool success = false; if (!C018_data->command_finished()) { mustSetDelay = true; } else { success = C018_data->txHexBytes(element.packed, ControllerSettings.Port); if (success) { if (airtime_ms > 0.0f) { ADD_TIMER_STAT(C018_AIR_TIME, static_cast(airtime_ms * 1000)); if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = F("LoRaWAN : Payload Length: "); log += pl + 13; // We have a LoRaWAN header of 13 bytes. log += F(" Air Time: "); log += toString(airtime_ms, 3); log += F(" ms"); addLogMove(LOG_LEVEL_INFO, log); } } } } String error = C018_data->getLastError(); // Clear the error string. if (error.indexOf(F("no_free_ch")) != -1) { mustSetDelay = true; } if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = F("C018 : Sent: "); log += element.packed; log += F(" length: "); log += String(element.packed.length()); if (success) { log += F(" (success) "); } log += error; addLogMove(LOG_LEVEL_INFO, log); } if (mustSetDelay) { // Module is still sending, delay for 10x expected air time, which is equivalent of 10% air time duty cycle. // This can be retried a few times, so at most 10 retries like these are needed to get below 1% air time again. // Very likely only 2 - 3 of these delays are needed, as we have 8 channels to send from and messages are likely sent in bursts. C018_DelayHandler->setAdditionalDelay(10 * airtime_ms); if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = F("LoRaWAN : Unable to send. Delay for "); log += 10 * airtime_ms; log += F(" ms"); addLogMove(LOG_LEVEL_INFO, log); } } return success; } String c018_add_joinChanged_script_element_line(const String& id, bool forOTAA) { String result = F("document.getElementById('tr_"); result += id; result += F("').style.display = style"); result += forOTAA ? F("OTAA") : F("ABP"); result += ';'; return result; } #endif // ifdef USES_C018