mirror of
https://github.com/letscontrolit/ESPEasy.git
synced 2026-09-14 18:46:59 +00:00
1091 lines
34 KiB
C++
1091 lines
34 KiB
C++
#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"
|
|
# define C018_BAUDRATE_LABEL "baudrate"
|
|
|
|
|
|
# include <rn2xx3.h>
|
|
# include <ESPeasySerial.h>
|
|
|
|
# include "src/ControllerQueue/C018_queue_element.h"
|
|
# include "src/DataTypes/ESPEasy_plugin_functions.h"
|
|
# include "src/Globals/CPlugins.h"
|
|
# include "src/Globals/Protocol.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
|
|
|
|
struct C018_data_struct {
|
|
private:
|
|
void C018_logError(const __FlashStringHelper* command) const;
|
|
void updateCacheOnInit();
|
|
|
|
public:
|
|
|
|
C018_data_struct() : C018_easySerial(nullptr), myLora(nullptr) {}
|
|
|
|
~C018_data_struct() {
|
|
reset();
|
|
}
|
|
|
|
void reset() {
|
|
if (myLora != nullptr) {
|
|
delete myLora;
|
|
myLora = nullptr;
|
|
}
|
|
|
|
if (C018_easySerial != nullptr) {
|
|
delete C018_easySerial;
|
|
C018_easySerial = nullptr;
|
|
}
|
|
cacheDevAddr = String();
|
|
cacheHWEUI = String();
|
|
cacheSysVer = String();
|
|
autobaud_success = false;
|
|
}
|
|
|
|
bool init(const uint8_t port, const int8_t serial_rx, const int8_t serial_tx, unsigned long baudrate,
|
|
bool joinIsOTAA, taskIndex_t sampleSet_Initiator, int8_t reset_pin) {
|
|
if ((serial_rx < 0) || (serial_tx < 0)) {
|
|
// Both pins are needed, or else no serial possible
|
|
return false;
|
|
}
|
|
|
|
// FIXME TD-er: Prevent unneeded OTAA joins.
|
|
// See: https://www.thethingsnetwork.org/forum/t/how-often-should-a-node-do-an-otaa-join-and-is-otaa-better-than-abp/11192/47?u=td-er
|
|
|
|
|
|
sampleSetInitiator = sampleSet_Initiator;
|
|
|
|
if (isInitialized()) {
|
|
// Check to see if serial parameters have changed.
|
|
bool notChanged = true;
|
|
notChanged &= C018_easySerial->getRxPin() == serial_rx;
|
|
notChanged &= C018_easySerial->getTxPin() == serial_tx;
|
|
notChanged &= C018_easySerial->getBaudRate() == baudrate;
|
|
notChanged &= myLora->useOTAA() == joinIsOTAA;
|
|
|
|
if (notChanged) { return true; }
|
|
}
|
|
reset();
|
|
_resetPin = reset_pin;
|
|
_baudrate = baudrate;
|
|
|
|
// FIXME TD-er: Make force SW serial a proper setting.
|
|
if (C018_easySerial != nullptr) {
|
|
delete C018_easySerial;
|
|
}
|
|
|
|
C018_easySerial = new (std::nothrow) ESPeasySerial(static_cast<ESPEasySerialPort>(port), serial_rx, serial_tx, false, 64);
|
|
|
|
if (C018_easySerial != nullptr) {
|
|
if (myLora != nullptr) {
|
|
delete myLora;
|
|
}
|
|
myLora = new (std::nothrow) rn2xx3(*C018_easySerial);
|
|
if (myLora == nullptr) {
|
|
delete C018_easySerial;
|
|
C018_easySerial = nullptr;
|
|
} else {
|
|
myLora->setAsyncMode(true);
|
|
myLora->setLastUsedJoinMode(joinIsOTAA);
|
|
triggerAutobaud();
|
|
}
|
|
}
|
|
return isInitialized();
|
|
}
|
|
|
|
bool isInitialized() const;
|
|
|
|
bool hasJoined() const {
|
|
if (!isInitialized()) { return false; }
|
|
return myLora->hasJoined();
|
|
}
|
|
|
|
bool useOTAA() const {
|
|
if (!isInitialized()) { return true; }
|
|
bool res = myLora->useOTAA();
|
|
|
|
C018_logError(F("useOTA()"));
|
|
return res;
|
|
}
|
|
|
|
bool command_finished() const {
|
|
return myLora->command_finished();
|
|
}
|
|
|
|
bool txUncnfBytes(const uint8_t *data, uint8_t size, uint8_t port) {
|
|
bool res = myLora->txBytes(data, size, port) != RN2xx3_datatypes::TX_return_type::TX_FAIL;
|
|
|
|
C018_logError(F("txUncnfBytes()"));
|
|
return res;
|
|
}
|
|
|
|
bool txHexBytes(const String& data, uint8_t port) {
|
|
bool res = myLora->txHexBytes(data, port) != RN2xx3_datatypes::TX_return_type::TX_FAIL;
|
|
|
|
C018_logError(F("txHexBytes()"));
|
|
return res;
|
|
}
|
|
|
|
bool txUncnf(const String& data, uint8_t port) {
|
|
bool res = myLora->tx(data, port) != RN2xx3_datatypes::TX_return_type::TX_FAIL;
|
|
|
|
C018_logError(F("txUncnf()"));
|
|
return res;
|
|
}
|
|
|
|
bool setTTNstack(RN2xx3_datatypes::TTN_stack_version version) {
|
|
if (!isInitialized()) { return false; }
|
|
bool res = myLora->setTTNstack(version);
|
|
|
|
C018_logError(F("setTTNstack"));
|
|
return res;
|
|
}
|
|
|
|
bool setFrequencyPlan(RN2xx3_datatypes::Freq_plan plan, uint32_t rx2_freq) {
|
|
if (!isInitialized()) { return false; }
|
|
bool res = myLora->setFrequencyPlan(plan, rx2_freq);
|
|
|
|
C018_logError(F("setFrequencyPlan()"));
|
|
return res;
|
|
}
|
|
|
|
bool setSF(uint8_t sf) {
|
|
if (!isInitialized()) { return false; }
|
|
bool res = myLora->setSF(sf);
|
|
|
|
C018_logError(F("setSF()"));
|
|
return res;
|
|
}
|
|
|
|
bool setAdaptiveDataRate(bool enabled) {
|
|
if (!isInitialized()) { return false; }
|
|
bool res = myLora->setAdaptiveDataRate(enabled);
|
|
|
|
C018_logError(F("setAdaptiveDataRate()"));
|
|
return res;
|
|
}
|
|
|
|
|
|
bool initOTAA(const String& AppEUI, const String& AppKey, const String& DevEUI) {
|
|
if (myLora == nullptr) { return false; }
|
|
bool success = myLora->initOTAA(AppEUI, AppKey, DevEUI);
|
|
cacheDevAddr = String();
|
|
|
|
C018_logError(F("initOTAA()"));
|
|
updateCacheOnInit();
|
|
return success;
|
|
}
|
|
|
|
bool initABP(const String& addr, const String& AppSKey, const String& NwkSKey) {
|
|
if (myLora == nullptr) { return false; }
|
|
bool success = myLora->initABP(addr, AppSKey, NwkSKey);
|
|
cacheDevAddr = addr;
|
|
|
|
C018_logError(F("initABP()"));
|
|
updateCacheOnInit();
|
|
return success;
|
|
}
|
|
|
|
String sendRawCommand(const String& command) {
|
|
if (!isInitialized()) { return EMPTY_STRING; }
|
|
|
|
if (loglevelActiveFor(LOG_LEVEL_INFO)) {
|
|
String log = F("sendRawCommand: ");
|
|
log += command;
|
|
addLogMove(LOG_LEVEL_INFO, log);
|
|
}
|
|
String res = myLora->sendRawCommand(command);
|
|
|
|
C018_logError(F("sendRawCommand()"));
|
|
return res;
|
|
}
|
|
|
|
int getVbat() {
|
|
if (!isInitialized()) { return -1; }
|
|
return myLora->getVbat();
|
|
}
|
|
|
|
String peekLastError() {
|
|
if (!isInitialized()) { return EMPTY_STRING; }
|
|
return myLora->peekLastError();
|
|
}
|
|
|
|
String getLastError() {
|
|
if (!isInitialized()) { return EMPTY_STRING; }
|
|
return myLora->getLastError();
|
|
}
|
|
|
|
String getDataRate() {
|
|
if (!isInitialized()) { return EMPTY_STRING; }
|
|
String res = myLora->getDataRate();
|
|
|
|
C018_logError(F("getDataRate()"));
|
|
return res;
|
|
}
|
|
|
|
int getRSSI() {
|
|
if (!isInitialized()) { return 0; }
|
|
return myLora->getRSSI();
|
|
}
|
|
|
|
uint32_t getRawStatus() {
|
|
if (!isInitialized()) { return 0; }
|
|
return myLora->getStatus().getRawStatus();
|
|
}
|
|
|
|
RN2xx3_status getStatus() const {
|
|
if (!isInitialized()) { return RN2xx3_status(); }
|
|
return myLora->getStatus();
|
|
}
|
|
|
|
bool getFrameCounters(uint32_t& dnctr, uint32_t& upctr) {
|
|
if (!isInitialized()) { return false; }
|
|
bool res = myLora->getFrameCounters(dnctr, upctr);
|
|
|
|
C018_logError(F("getFrameCounters()"));
|
|
return res;
|
|
}
|
|
|
|
bool setFrameCounters(uint32_t dnctr, uint32_t upctr) {
|
|
if (!isInitialized()) { return false; }
|
|
bool res = myLora->setFrameCounters(dnctr, upctr);
|
|
|
|
C018_logError(F("setFrameCounters()"));
|
|
return res;
|
|
}
|
|
|
|
// Cached data, only changing occasionally.
|
|
|
|
String getDevaddr() {
|
|
if (cacheDevAddr.isEmpty())
|
|
{
|
|
updateCacheOnInit();
|
|
}
|
|
return cacheDevAddr;
|
|
}
|
|
|
|
String hweui() {
|
|
if (cacheHWEUI.isEmpty()) {
|
|
if (isInitialized()) {
|
|
cacheHWEUI = myLora->hweui();
|
|
}
|
|
}
|
|
return cacheHWEUI;
|
|
}
|
|
|
|
String sysver() {
|
|
if (cacheSysVer.isEmpty()) {
|
|
if (isInitialized()) {
|
|
cacheSysVer = myLora->sysver();
|
|
}
|
|
}
|
|
return cacheSysVer;
|
|
}
|
|
|
|
uint8_t getSampleSetCount() const {
|
|
return sampleSetCounter;
|
|
}
|
|
|
|
uint8_t getSampleSetCount(taskIndex_t taskIndex) {
|
|
if (sampleSetInitiator == taskIndex)
|
|
{
|
|
++sampleSetCounter;
|
|
}
|
|
return sampleSetCounter;
|
|
}
|
|
|
|
float getLoRaAirTime(uint8_t pl) const {
|
|
if (isInitialized()) {
|
|
return myLora->getLoRaAirTime(pl + 13); // We have a LoRaWAN header of 13 bytes.
|
|
}
|
|
return -1.0;
|
|
}
|
|
|
|
void async_loop() {
|
|
if (isInitialized()) {
|
|
rn2xx3_handler::RN_state state = myLora->async_loop();
|
|
|
|
if (rn2xx3_handler::RN_state::must_perform_init == state) {
|
|
if (myLora->get_busy_count() > 10) {
|
|
if (validGpio(_resetPin)) {
|
|
pinMode(_resetPin, OUTPUT);
|
|
digitalWrite(_resetPin, LOW);
|
|
delay(50);
|
|
digitalWrite(_resetPin, HIGH);
|
|
delay(200);
|
|
}
|
|
autobaud_success = false;
|
|
|
|
// triggerAutobaud();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private:
|
|
|
|
void triggerAutobaud() {
|
|
if ((C018_easySerial == nullptr) || (myLora == nullptr)) {
|
|
return;
|
|
}
|
|
int retries = 2;
|
|
|
|
while (retries > 0 && !autobaud_success) {
|
|
if (retries == 1) {
|
|
if (validGpio(_resetPin)) {
|
|
pinMode(_resetPin, OUTPUT);
|
|
digitalWrite(_resetPin, LOW);
|
|
delay(50);
|
|
digitalWrite(_resetPin, HIGH);
|
|
delay(200);
|
|
}
|
|
}
|
|
|
|
// wakeUP_RN2483 and set data rate
|
|
// Delay must be longer than specified in the datasheet for firmware 1.0.3
|
|
// See: https://www.thethingsnetwork.org/forum/t/rn2483a-problems-no-serial-communication/7866/36?u=td-er
|
|
|
|
// First set the baud rate low enough to even trigger autobaud when 9600 baud is active
|
|
C018_easySerial->begin(600);
|
|
C018_easySerial->write(static_cast<uint8_t>(0x00));
|
|
|
|
// Set to desired baud rate.
|
|
C018_easySerial->begin(_baudrate);
|
|
C018_easySerial->write(static_cast<uint8_t>(0x55));
|
|
C018_easySerial->println();
|
|
delay(100);
|
|
|
|
String response = myLora->sysver();
|
|
|
|
// we could use sendRawCommand(F("sys get ver")); here
|
|
// C018_easySerial->println(F("sys get ver"));
|
|
// String response = C018_easySerial->readStringUntil('\n');
|
|
autobaud_success = response.length() > 10;
|
|
|
|
if (loglevelActiveFor(LOG_LEVEL_INFO)) {
|
|
String log = F("C018 AutoBaud: ");
|
|
log += response;
|
|
log += F(" status: ");
|
|
log += myLora->sendRawCommand(F("mac get status"));
|
|
addLogMove(LOG_LEVEL_INFO, log);
|
|
C018_logError(F("autobaud check"));
|
|
}
|
|
--retries;
|
|
}
|
|
}
|
|
|
|
ESPeasySerial *C018_easySerial = nullptr;
|
|
rn2xx3 *myLora = nullptr;
|
|
String cacheDevAddr;
|
|
String cacheHWEUI;
|
|
String cacheSysVer;
|
|
unsigned long _baudrate = 57600;
|
|
uint8_t sampleSetCounter = 0;
|
|
taskIndex_t sampleSetInitiator = INVALID_TASK_INDEX;
|
|
int8_t _resetPin = -1;
|
|
bool autobaud_success = false;
|
|
};
|
|
|
|
|
|
bool C018_data_struct::isInitialized() const {
|
|
if ((C018_easySerial != nullptr) && (myLora != nullptr)) {
|
|
if (autobaud_success) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void C018_data_struct::C018_logError(const __FlashStringHelper* command) const {
|
|
if (loglevelActiveFor(LOG_LEVEL_INFO)) {
|
|
String error = myLora->peekLastError();
|
|
|
|
// String error = myLora->getLastError();
|
|
|
|
if (error.length() > 0) {
|
|
String log = F("RN2483: ");
|
|
log += command;
|
|
log += F(": ");
|
|
log += error;
|
|
addLogMove(LOG_LEVEL_INFO, log);
|
|
}
|
|
}
|
|
}
|
|
|
|
void C018_data_struct::updateCacheOnInit() {
|
|
if (isInitialized()) {
|
|
if (cacheDevAddr.isEmpty() && myLora->getStatus().Joined)
|
|
{
|
|
cacheDevAddr = myLora->sendRawCommand(F("mac get devaddr"));
|
|
|
|
if (cacheDevAddr == F("00000000")) {
|
|
cacheDevAddr = String();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
C018_data_struct *C018_data = nullptr;
|
|
|
|
# define C018_DEVICE_EUI_LEN 17
|
|
# define C018_DEVICE_ADDR_LEN 33
|
|
# define C018_NETWORK_SESSION_KEY_LEN 33
|
|
# define C018_APP_SESSION_KEY_LEN 33
|
|
# define C018_USE_OTAA 0
|
|
# define C018_USE_ABP 1
|
|
struct C018_ConfigStruct
|
|
{
|
|
C018_ConfigStruct() {
|
|
reset();
|
|
}
|
|
|
|
void validate() {
|
|
ZERO_TERMINATE(DeviceEUI);
|
|
ZERO_TERMINATE(DeviceAddr);
|
|
ZERO_TERMINATE(NetworkSessionKey);
|
|
ZERO_TERMINATE(AppSessionKey);
|
|
|
|
if ((baudrate < 2400) || (baudrate > 115200)) {
|
|
reset();
|
|
}
|
|
if (stackVersion >= RN2xx3_datatypes::TTN_stack_version::TTN_NOT_SET) {
|
|
stackVersion = RN2xx3_datatypes::TTN_stack_version::TTN_v3;
|
|
}
|
|
switch (frequencyplan) {
|
|
case RN2xx3_datatypes::Freq_plan::SINGLE_CHANNEL_EU:
|
|
case RN2xx3_datatypes::Freq_plan::TTN_EU:
|
|
case RN2xx3_datatypes::Freq_plan::DEFAULT_EU:
|
|
if (rx2_freq < 867000000 || rx2_freq > 870000000) {
|
|
rx2_freq = 0;
|
|
}
|
|
break;
|
|
case RN2xx3_datatypes::Freq_plan::TTN_US:
|
|
// FIXME TD-er: Need to find the ranges for US (and other regions)
|
|
break;
|
|
default:
|
|
rx2_freq = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
void reset() {
|
|
ZERO_FILL(DeviceEUI);
|
|
ZERO_FILL(DeviceAddr);
|
|
ZERO_FILL(NetworkSessionKey);
|
|
ZERO_FILL(AppSessionKey);
|
|
baudrate = 57600;
|
|
rxpin = -1;
|
|
txpin = -1;
|
|
resetpin = -1;
|
|
sf = 7;
|
|
frequencyplan = RN2xx3_datatypes::Freq_plan::TTN_EU;
|
|
rx2_freq = 0;
|
|
stackVersion = RN2xx3_datatypes::TTN_stack_version::TTN_v3;
|
|
joinmethod = C018_USE_OTAA;
|
|
}
|
|
|
|
char DeviceEUI[C018_DEVICE_EUI_LEN] = { 0 };
|
|
char DeviceAddr[C018_DEVICE_ADDR_LEN] = { 0 };
|
|
char NetworkSessionKey[C018_NETWORK_SESSION_KEY_LEN] = { 0 };
|
|
char AppSessionKey[C018_APP_SESSION_KEY_LEN] = { 0 };
|
|
unsigned long baudrate = 57600;
|
|
int8_t rxpin = 12;
|
|
int8_t txpin = 14;
|
|
int8_t resetpin = -1;
|
|
uint8_t sf = 7;
|
|
uint8_t frequencyplan = RN2xx3_datatypes::Freq_plan::TTN_EU;
|
|
uint8_t joinmethod = C018_USE_OTAA;
|
|
uint8_t serialPort = 0;
|
|
uint8_t stackVersion = RN2xx3_datatypes::TTN_stack_version::TTN_v2;
|
|
uint8_t adr = 0;
|
|
uint32_t rx2_freq = 0;
|
|
};
|
|
|
|
|
|
// 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:
|
|
{
|
|
Protocol[++protocolCount].Number = CPLUGIN_ID_018;
|
|
Protocol[protocolCount].usesMQTT = false;
|
|
Protocol[protocolCount].usesAccount = true;
|
|
Protocol[protocolCount].usesPassword = true;
|
|
Protocol[protocolCount].defaultPort = 1;
|
|
Protocol[protocolCount].usesID = true;
|
|
Protocol[protocolCount].usesHost = false;
|
|
Protocol[protocolCount].usesCheckReply = false;
|
|
Protocol[protocolCount].usesTimeout = false;
|
|
Protocol[protocolCount].usesSampleSets = true;
|
|
Protocol[protocolCount].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();
|
|
}
|
|
|
|
unsigned long baudrate;
|
|
uint32_t rx2_frequency;
|
|
int8_t rxpin;
|
|
int8_t txpin;
|
|
int8_t resetpin;
|
|
uint8_t sf;
|
|
uint8_t frequencyplan;
|
|
uint8_t joinmethod;
|
|
uint8_t stackVersion;
|
|
uint8_t adr;
|
|
|
|
ESPEasySerialPort port = ESPEasySerialPort::not_set;
|
|
|
|
{
|
|
// Keep this object in a small scope so we can destruct it as soon as possible again.
|
|
std::shared_ptr<C018_ConfigStruct> customConfig;
|
|
{
|
|
// Try to allocate on 2nd heap
|
|
#ifdef USE_SECOND_HEAP
|
|
// HeapSelectIram ephemeral;
|
|
#endif
|
|
std::shared_ptr<C018_ConfigStruct> tmp_shared(new (std::nothrow) C018_ConfigStruct);
|
|
customConfig = std::move(tmp_shared);
|
|
}
|
|
|
|
if (!customConfig) {
|
|
break;
|
|
}
|
|
LoadCustomControllerSettings(event->ControllerIndex, reinterpret_cast<uint8_t *>(customConfig.get()), sizeof(C018_ConfigStruct));
|
|
customConfig->validate();
|
|
baudrate = customConfig->baudrate;
|
|
rxpin = customConfig->rxpin;
|
|
txpin = customConfig->txpin;
|
|
resetpin = customConfig->resetpin;
|
|
sf = customConfig->sf;
|
|
frequencyplan = customConfig->frequencyplan;
|
|
rx2_frequency = customConfig->rx2_freq;
|
|
joinmethod = customConfig->joinmethod;
|
|
stackVersion = customConfig->stackVersion;
|
|
adr = customConfig->adr;
|
|
port = static_cast<ESPEasySerialPort>(customConfig->serialPort);
|
|
|
|
{
|
|
addFormTextBox(F("Device EUI"), F("deveui"), customConfig->DeviceEUI, C018_DEVICE_EUI_LEN - 1);
|
|
String deveui_note = F("Leave empty to use HW DevEUI: ");
|
|
|
|
if (C018_data != nullptr) {
|
|
deveui_note += C018_data->hweui();
|
|
}
|
|
addFormNote(deveui_note, F("deveui_note"));
|
|
}
|
|
|
|
addFormTextBox(F("Device Addr"), F("devaddr"), customConfig->DeviceAddr, C018_DEVICE_ADDR_LEN - 1);
|
|
addFormTextBox(F("Network Session Key"), F("nskey"), customConfig->NetworkSessionKey, C018_NETWORK_SESSION_KEY_LEN - 1);
|
|
addFormTextBox(F("App Session Key"), F("appskey"), customConfig->AppSessionKey, C018_APP_SESSION_KEY_LEN - 1);
|
|
}
|
|
|
|
{
|
|
const __FlashStringHelper * options[2] = { F("OTAA"), F("ABP") };
|
|
const int values[2] = { C018_USE_OTAA, C018_USE_ABP };
|
|
addFormSelector_script(F("Activation Method"), F("joinmethod"), 2,
|
|
options, values, nullptr, joinmethod,
|
|
F("joinChanged(this)")); // Script to toggle OTAA/ABP fields visibility when changing selection.
|
|
}
|
|
html_add_script(F("document.getElementById('joinmethod').onchange();"), false);
|
|
|
|
addTableSeparator(F("Connection Configuration"), 2, 3);
|
|
{
|
|
const __FlashStringHelper * options[4] = { F("SINGLE_CHANNEL_EU"), F("TTN_EU"), F("TTN_US"), F("DEFAULT_EU") };
|
|
int values[4] =
|
|
{
|
|
RN2xx3_datatypes::Freq_plan::SINGLE_CHANNEL_EU,
|
|
RN2xx3_datatypes::Freq_plan::TTN_EU,
|
|
RN2xx3_datatypes::Freq_plan::TTN_US,
|
|
RN2xx3_datatypes::Freq_plan::DEFAULT_EU
|
|
};
|
|
|
|
addFormSelector(F("Frequency Plan"), F("frequencyplan"), 4, options, values, nullptr, frequencyplan, false);
|
|
addFormNumericBox(F("RX2 Frequency"), F("rx2freq"), rx2_frequency, 0);
|
|
addUnit(F("Hz"));
|
|
addFormNote(F("0 = default, or else override default"));
|
|
}
|
|
{
|
|
const __FlashStringHelper * options[2] = { F("TTN v2"), F("TTN v3") };
|
|
int values[2] = {
|
|
RN2xx3_datatypes::TTN_stack_version::TTN_v2,
|
|
RN2xx3_datatypes::TTN_stack_version::TTN_v3
|
|
};
|
|
|
|
addFormSelector(F("TTN Stack"), F("ttnstack"), 2, options, values, nullptr, stackVersion, false);
|
|
}
|
|
|
|
addFormNumericBox(F("Spread Factor"), F("sf"), sf, 7, 12);
|
|
addFormCheckBox(F("Adaptive Data Rate (ADR)"), F("adr"), adr);
|
|
|
|
|
|
addTableSeparator(F("Serial Port Configuration"), 2, 3);
|
|
|
|
serialHelper_webformLoad(port, rxpin, txpin, true);
|
|
|
|
// Show serial port selection
|
|
addFormPinSelect(PinSelectPurpose::Generic_input, formatGpioName_RX(false), F("taskdevicepin1"), rxpin);
|
|
addFormPinSelect(PinSelectPurpose::Generic_output, formatGpioName_TX(false), F("taskdevicepin2"), txpin);
|
|
|
|
html_add_script(F("document.getElementById('serPort').onchange();"), false);
|
|
|
|
addFormNumericBox(F("Baudrate"), F(C018_BAUDRATE_LABEL), baudrate, 2400, 115200);
|
|
addUnit(F("baud"));
|
|
addFormNote(F("Module default baudrate: 57600 bps"));
|
|
|
|
// Optional reset pin RN2xx3
|
|
addFormPinSelect(PinSelectPurpose::Generic_output, formatGpioName_output_optional(F("Reset")), F("taskdevicepin3"), resetpin);
|
|
|
|
addTableSeparator(F("Device Status"), 2, 3);
|
|
|
|
if (C018_data != nullptr) {
|
|
// Some information on detected device
|
|
addRowLabel(F("Hardware DevEUI"));
|
|
addHtml(C018_data->hweui());
|
|
addRowLabel(F("Version Number"));
|
|
addHtml(C018_data->sysver());
|
|
|
|
addRowLabel(F("Voltage"));
|
|
addHtmlFloat(static_cast<float>(C018_data->getVbat()) / 1000.0f, 3);
|
|
|
|
addRowLabel(F("Device Addr"));
|
|
addHtml(C018_data->getDevaddr());
|
|
|
|
uint32_t dnctr, upctr;
|
|
|
|
if (C018_data->getFrameCounters(dnctr, upctr)) {
|
|
addRowLabel(F("Frame Counters (down/up)"));
|
|
String values = String(dnctr);
|
|
values += '/';
|
|
values += upctr;
|
|
addHtml(values);
|
|
}
|
|
|
|
addRowLabel(F("Last Command Error"));
|
|
addHtml(C018_data->getLastError());
|
|
|
|
addRowLabel(F("Sample Set Counter"));
|
|
addHtmlInt(C018_data->getSampleSetCount());
|
|
|
|
addRowLabel(F("Data Rate"));
|
|
addHtml(C018_data->getDataRate());
|
|
|
|
{
|
|
RN2xx3_status status = C018_data->getStatus();
|
|
|
|
addRowLabel(F("Status RAW value"));
|
|
addHtmlInt(status.getRawStatus());
|
|
|
|
addRowLabel(F("Activation Status"));
|
|
addEnabled(status.Joined);
|
|
|
|
addRowLabel(F("Silent Immediately"));
|
|
addHtmlInt(status.SilentImmediately ? 1 : 0);
|
|
}
|
|
}
|
|
|
|
|
|
break;
|
|
}
|
|
case CPlugin::Function::CPLUGIN_WEBFORM_SAVE:
|
|
{
|
|
std::shared_ptr<C018_ConfigStruct> customConfig;
|
|
{
|
|
// Try to allocate on 2nd heap
|
|
#ifdef USE_SECOND_HEAP
|
|
// HeapSelectIram ephemeral;
|
|
#endif
|
|
std::shared_ptr<C018_ConfigStruct> tmp_shared(new (std::nothrow) C018_ConfigStruct);
|
|
customConfig = std::move(tmp_shared);
|
|
}
|
|
|
|
if (customConfig) {
|
|
customConfig->reset();
|
|
String deveui = webArg(F("deveui"));
|
|
String devaddr = webArg(F("devaddr"));
|
|
String nskey = webArg(F("nskey"));
|
|
String appskey = webArg(F("appskey"));
|
|
|
|
strlcpy(customConfig->DeviceEUI, deveui.c_str(), sizeof(customConfig->DeviceEUI));
|
|
strlcpy(customConfig->DeviceAddr, devaddr.c_str(), sizeof(customConfig->DeviceAddr));
|
|
strlcpy(customConfig->NetworkSessionKey, nskey.c_str(), sizeof(customConfig->NetworkSessionKey));
|
|
strlcpy(customConfig->AppSessionKey, appskey.c_str(), sizeof(customConfig->AppSessionKey));
|
|
customConfig->baudrate = getFormItemInt(F(C018_BAUDRATE_LABEL), customConfig->baudrate);
|
|
customConfig->rxpin = getFormItemInt(F("taskdevicepin1"), customConfig->rxpin);
|
|
customConfig->txpin = getFormItemInt(F("taskdevicepin2"), customConfig->txpin);
|
|
customConfig->resetpin = getFormItemInt(F("taskdevicepin3"), customConfig->resetpin);
|
|
customConfig->sf = getFormItemInt(F("sf"), customConfig->sf);
|
|
customConfig->frequencyplan = getFormItemInt(F("frequencyplan"), customConfig->frequencyplan);
|
|
customConfig->rx2_freq = getFormItemInt(F("rx2freq"), customConfig->rx2_freq);
|
|
customConfig->joinmethod = getFormItemInt(F("joinmethod"), customConfig->joinmethod);
|
|
customConfig->stackVersion = getFormItemInt(F("ttnstack"), customConfig->stackVersion);
|
|
customConfig->adr = isFormItemChecked(F("adr"));
|
|
serialHelper_webformSave(customConfig->serialPort, customConfig->rxpin, customConfig->txpin);
|
|
SaveCustomControllerSettings(event->ControllerIndex, reinterpret_cast<const uint8_t *>(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_data != nullptr) {
|
|
std::unique_ptr<C018_queue_element> element(new C018_queue_element(event, C018_data->getSampleSetCount(event->TaskIndex)));
|
|
success = C018_DelayHandler->addToQueue(std::move(element));
|
|
Scheduler.scheduleNextDelayQueue(ESPEasy_Scheduler::IntervalTimer_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 (command.equals(F("lorawan"))) {
|
|
const String subcommand = parseString(string, 2);
|
|
if (subcommand.equals(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->configureControllerSettings(ControllerSettings);
|
|
AppEUI = getControllerUser(event->ControllerIndex, ControllerSettings);
|
|
AppKey = getControllerPass(event->ControllerIndex, ControllerSettings);
|
|
SampleSetInitiator = ControllerSettings.SampleSetInitiator;
|
|
Port = ControllerSettings.Port;
|
|
}
|
|
|
|
std::shared_ptr<C018_ConfigStruct> customConfig;
|
|
{
|
|
// Try to allocate on 2nd heap
|
|
#ifdef USE_SECOND_HEAP
|
|
// HeapSelectIram ephemeral;
|
|
#endif
|
|
std::shared_ptr<C018_ConfigStruct> tmp_shared(new (std::nothrow) C018_ConfigStruct);
|
|
customConfig = std::move(tmp_shared);
|
|
}
|
|
|
|
if (!customConfig) {
|
|
return false;
|
|
}
|
|
LoadCustomControllerSettings(event->ControllerIndex, reinterpret_cast<uint8_t *>(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<RN2xx3_datatypes::Freq_plan>(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<RN2xx3_datatypes::TTN_stack_version>(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<const C018_queue_element&>(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<unsigned long>(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
|