/* * The MySensors Arduino library handles the wireless radio link and protocol * between your home built sensors/actuators and HA controller of choice. * The sensors forms a self healing radio network with optional repeaters. Each * repeater and gateway builds a routing tables in EEPROM which keeps track of the * network topology allowing messages to be routed to nodes. * * Created by Henrik Ekblad * Copyright (C) 2013-2018 Sensnology AB * Full contributor list: https://github.com/mysensors/MySensors/graphs/contributors * * Documentation: http://www.mysensors.org * Support Forum: http://forum.mysensors.org * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * version 2 as published by the Free Software Foundation. */ #include "MyTransport.h" // debug #if defined(MY_DEBUG_VERBOSE_TRANSPORT) #define TRANSPORT_DEBUG(x,...) DEBUG_OUTPUT(x, ##__VA_ARGS__) //!< debug extern char _convBuf[MAX_PAYLOAD * 2 + 1]; #else #define TRANSPORT_DEBUG(x,...) //!< debug NULL #endif // SM: transitions and update states static transportState_t stInit = { stInitTransition, stInitUpdate }; static transportState_t stParent = { stParentTransition, stParentUpdate }; static transportState_t stID = { stIDTransition, stIDUpdate }; static transportState_t stUplink = { stUplinkTransition, stUplinkUpdate }; static transportState_t stReady = { stReadyTransition, stReadyUpdate }; static transportState_t stFailure = { stFailureTransition, stFailureUpdate }; // transport SM variables static transportSM_t _transportSM; // transport configuration static transportConfig_t _transportConfig; // callback transportOk transportCallback_t _transportReady_cb = NULL; // enhanced ID assignment #if !defined(MY_GATEWAY_FEATURE) && (MY_NODE_ID == AUTO) static uint8_t _transportToken = AUTO; #endif // global variables extern MyMessage _msg; // incoming message extern MyMessage _msgTmp; // outgoing message #if defined(MY_RAM_ROUTING_TABLE_ENABLED) static routingTable_t _transportRoutingTable; //!< routing table static uint32_t _lastRoutingTableSave; //!< last routing table dump #endif // regular sanity check, activated by default on GW and repeater nodes #if defined(MY_TRANSPORT_SANITY_CHECK) static uint32_t _lastSanityCheck; //!< last sanity check #endif // regular network discovery, sends I_DISCOVER_REQUESTS to update routing table // sufficient to have GW triggering requests to also update repeater nodes #if defined(MY_GATEWAY_FEATURE) static uint32_t _lastNetworkDiscovery; //!< last network discovery #endif // stInit: initialise transport HW void stInitTransition(void) { TRANSPORT_DEBUG(PSTR("TSM:INIT\n")); // initialise status variables _transportSM.pingActive = false; _transportSM.transportActive = false; _transportSM.lastUplinkCheck = 0; #if defined(MY_TRANSPORT_SANITY_CHECK) _lastSanityCheck = hwMillis(); #endif #if defined(MY_GATEWAY_FEATURE) _lastNetworkDiscovery = 0; #endif #if defined(MY_RAM_ROUTING_TABLE_ENABLED) _lastRoutingTableSave = hwMillis(); #endif // Read node settings (ID, parent ID, GW distance) from EEPROM hwReadConfigBlock((void *)&_transportConfig, (void *)EEPROM_NODE_ID_ADDRESS, sizeof(transportConfig_t)); } void stInitUpdate(void) { // initialise radio if (!transportInit()) { TRANSPORT_DEBUG(PSTR("!TSM:INIT:TSP FAIL\n")); setIndication(INDICATION_ERR_INIT_TRANSPORT); transportSwitchSM(stFailure); } else { TRANSPORT_DEBUG(PSTR("TSM:INIT:TSP OK\n")); _transportSM.transportActive = true; #if defined (MY_PASSIVE_NODE) _transportConfig.passiveMode = true; TRANSPORT_DEBUG(PSTR("TSM:INIT:TSP PSM\n")); // transport passive mode #else _transportConfig.passiveMode = false; #endif #if defined(MY_GATEWAY_FEATURE) // Set configuration for gateway TRANSPORT_DEBUG(PSTR("TSM:INIT:GW MODE\n")); _transportConfig.parentNodeId = GATEWAY_ADDRESS; _transportConfig.distanceGW = 0u; _transportConfig.nodeId = GATEWAY_ADDRESS; transportSetAddress(GATEWAY_ADDRESS); // GW mode: skip FPAR,ID,UPL states transportSwitchSM(stReady); #else if (MY_NODE_ID != AUTO) { TRANSPORT_DEBUG(PSTR("TSM:INIT:STATID=%" PRIu8 "\n"),(uint8_t)MY_NODE_ID); // Set static ID _transportConfig.nodeId = (uint8_t)MY_NODE_ID; // Save static ID to eeprom (for bootloader) hwWriteConfig(EEPROM_NODE_ID_ADDRESS, (uint8_t)MY_NODE_ID); } // assign ID if set if (_transportConfig.nodeId == AUTO || transportAssignNodeID(_transportConfig.nodeId)) { // if node ID valid (>0 and <255), proceed to next state transportSwitchSM(stParent); } else { // ID invalid (0 or 255) transportSwitchSM(stFailure); } #endif } } // stParent: find parent void stParentTransition(void) { TRANSPORT_DEBUG(PSTR("TSM:FPAR\n")); // find parent setIndication(INDICATION_FIND_PARENT); _transportSM.uplinkOk = false; _transportSM.preferredParentFound = false; #if defined(MY_PARENT_NODE_IS_STATIC) || defined(MY_PASSIVE_NODE) TRANSPORT_DEBUG(PSTR("TSM:FPAR:STATP=%" PRIu8 "\n"), (uint8_t)MY_PARENT_NODE_ID); // static parent _transportSM.findingParentNode = false; _transportConfig.distanceGW = 1u; // assumption, CHKUPL:GWDC will update this variable _transportConfig.parentNodeId = (uint8_t)MY_PARENT_NODE_ID; // save parent ID to eeprom (for bootloader) hwWriteConfig(EEPROM_PARENT_NODE_ID_ADDRESS, (uint8_t)MY_PARENT_NODE_ID); #else _transportSM.findingParentNode = true; _transportConfig.distanceGW = DISTANCE_INVALID; // Set distance to max and invalidate parent node ID _transportConfig.parentNodeId = AUTO; // Broadcast find parent request (void)transportRouteMessage(build(_msgTmp, BROADCAST_ADDRESS, NODE_SENSOR_ID, C_INTERNAL, I_FIND_PARENT_REQUEST).set("")); #endif } // stParentUpdate void stParentUpdate(void) { #if defined(MY_PARENT_NODE_IS_STATIC) || defined(MY_PASSIVE_NODE) // skipping find parent setIndication(INDICATION_GOT_PARENT); transportSwitchSM(stID); #else if (transportTimeInState() > MY_TRANSPORT_STATE_TIMEOUT_MS || _transportSM.preferredParentFound) { // timeout or preferred parent found if (_transportConfig.parentNodeId != AUTO) { // parent assigned TRANSPORT_DEBUG(PSTR("TSM:FPAR:OK\n")); // find parent ok _transportSM.findingParentNode = false; setIndication(INDICATION_GOT_PARENT); // go to next state transportSwitchSM(stID); } else { // timeout w/o reply or valid parent if (_transportSM.stateRetries < MY_TRANSPORT_STATE_RETRIES) { // retries left TRANSPORT_DEBUG(PSTR("!TSM:FPAR:NO REPLY\n")); // find parent, no reply // reenter state transportSwitchSM(stParent); } else { // no retries left, finding parent failed TRANSPORT_DEBUG(PSTR("!TSM:FPAR:FAIL\n")); setIndication(INDICATION_ERR_FIND_PARENT); transportSwitchSM(stFailure); } } } #endif } // stID: verify and request ID if necessary void stIDTransition(void) { TRANSPORT_DEBUG(PSTR("TSM:ID\n")); // verify/request node ID if (_transportConfig.nodeId == AUTO) { // send ID request setIndication(INDICATION_REQ_NODEID); #if !defined(MY_GATEWAY_FEATURE) && (MY_NODE_ID == AUTO) _transportToken = (uint8_t)(hwMillis() & 0xFF); if (_transportToken == AUTO) { _transportToken++; // AUTO as token not allowed } const uint8_t sensorID = _transportToken; #else const uint8_t sensorID = NODE_SENSOR_ID; #endif TRANSPORT_DEBUG(PSTR("TSM:ID:REQ\n")); // request node ID (void)transportRouteMessage(build(_msgTmp, GATEWAY_ADDRESS, sensorID, C_INTERNAL, I_ID_REQUEST).set("")); } } void stIDUpdate(void) { if (_transportConfig.nodeId != AUTO) { // current node ID is valid TRANSPORT_DEBUG(PSTR("TSM:ID:OK\n")); setIndication(INDICATION_GOT_NODEID); // proceed to next state transportSwitchSM(stUplink); } else if (transportTimeInState() > MY_TRANSPORT_STATE_TIMEOUT_MS) { // timeout if (_transportSM.stateRetries < MY_TRANSPORT_STATE_RETRIES) { // retries left: reenter state transportSwitchSM(stID); } else { // no retries left TRANSPORT_DEBUG(PSTR("!TSM:ID:FAIL\n")); setIndication(INDICATION_ERR_GET_NODEID); transportSwitchSM(stFailure); } } } void stUplinkTransition(void) { #if !defined(MY_TRANSPORT_UPLINK_CHECK_DISABLED) TRANSPORT_DEBUG(PSTR("TSM:UPL\n")); setIndication(INDICATION_CHECK_UPLINK); _transportSM.pingResponse = INVALID_HOPS; _transportSM.pingActive = true; (void)transportRouteMessage(build(_msgTmp,GATEWAY_ADDRESS, NODE_SENSOR_ID, C_INTERNAL, I_PING).set((uint8_t)0x01)); #endif } void stUplinkUpdate(void) { #if !defined(MY_TRANSPORT_UPLINK_CHECK_DISABLED) if (_transportSM.pingResponse != INVALID_HOPS) { _transportSM.lastUplinkCheck = hwMillis(); // uplink ok, i.e. GW replied TRANSPORT_DEBUG(PSTR("TSM:UPL:OK\n")); // uplink ok if (_transportSM.pingResponse != _transportConfig.distanceGW) { TRANSPORT_DEBUG(PSTR("TSM:UPL:DGWC,O=%" PRIu8 ",N=%" PRIu8 "\n"), _transportConfig.distanceGW, _transportSM.pingResponse); // distance to GW changed _transportConfig.distanceGW = _transportSM.pingResponse; } transportSwitchSM(stReady); // proceed to next state } else if (transportTimeInState() > MY_TRANSPORT_STATE_TIMEOUT_MS) { // timeout if (_transportSM.stateRetries < MY_TRANSPORT_STATE_RETRIES) { // retries left: reenter state transportSwitchSM(stUplink); } else { // no retries left TRANSPORT_DEBUG(PSTR("!TSM:UPL:FAIL\n")); // uplink check failed _transportSM.pingActive = false; setIndication(INDICATION_ERR_CHECK_UPLINK); transportSwitchSM(stParent); // go back to stParent } } #else TRANSPORT_DEBUG(PSTR("TSM:UPL:DISABLED\n")); // uplink check disabled transportSwitchSM(stReady); #endif } void stReadyTransition(void) { // transport is ready and fully operational TRANSPORT_DEBUG(PSTR("TSM:READY:ID=%" PRIu8 ",PAR=%" PRIu8 ",DIS=%" PRIu8 "\n"), _transportConfig.nodeId, _transportConfig.parentNodeId, _transportConfig.distanceGW); _transportSM.uplinkOk = true; _transportSM.failureCounter = 0u; // reset failure counter _transportSM.failedUplinkTransmissions = 0u; // reset failed uplink TX counter // callback if (_transportReady_cb) { _transportReady_cb(); } } // stReadyUpdate: monitors link void stReadyUpdate(void) { #if defined(MY_GATEWAY_FEATURE) if (!_lastNetworkDiscovery || (hwMillis() - _lastNetworkDiscovery > MY_TRANSPORT_DISCOVERY_INTERVAL_MS)) { _lastNetworkDiscovery = hwMillis(); TRANSPORT_DEBUG(PSTR("TSM:READY:NWD REQ\n")); // send transport network discovery (void)transportRouteMessage(build(_msgTmp, BROADCAST_ADDRESS, NODE_SENSOR_ID, C_INTERNAL, I_DISCOVER_REQUEST).set("")); } #else if (_transportSM.failedUplinkTransmissions > MY_TRANSPORT_MAX_TX_FAILURES) { // too many uplink transmissions failed, find new parent (if non-static) #if !defined(MY_PARENT_NODE_IS_STATIC) TRANSPORT_DEBUG(PSTR("!TSM:READY:UPL FAIL,SNP\n")); // uplink failed, search new parent transportSwitchSM(stParent); #else TRANSPORT_DEBUG(PSTR("!TSM:READY:UPL FAIL,STATP\n")); // uplink failed, static parent // reset counter _transportSM.failedUplinkTransmissions = 0u; #endif } #endif #if defined(MY_RAM_ROUTING_TABLE_ENABLED) if (hwMillis() - _lastRoutingTableSave > MY_ROUTING_TABLE_SAVE_INTERVAL_MS) { _lastRoutingTableSave = hwMillis(); transportSaveRoutingTable(); } #endif } // stFailure: entered upon HW init failure or max retries exceeded void stFailureTransition(void) { if (_transportSM.failureCounter < MY_TRANSPORT_MAX_TSM_FAILURES) { _transportSM.failureCounter++; // increment consecutive TSM failure counter } TRANSPORT_DEBUG(PSTR("TSM:FAIL:CNT=%" PRIu8 "\n"),_transportSM.failureCounter); _transportSM.uplinkOk = false; // uplink nok _transportSM.transportActive = false; // transport inactive setIndication(INDICATION_ERR_INIT_TRANSPORT); #if defined(MY_SENSOR_NETWORK) TRANSPORT_DEBUG(PSTR("TSM:FAIL:DIS\n")); // disable transport, no need until re-init transportDisable(); // sleep #endif } void stFailureUpdate(void) { if (transportTimeInState() > ( isTransportExtendedFailure()? MY_TRANSPORT_TIMEOUT_EXT_FAILURE_STATE_MS: MY_TRANSPORT_TIMEOUT_FAILURE_STATE_MS) ) { TRANSPORT_DEBUG(PSTR("TSM:FAIL:RE-INIT\n")); // attempt to re-initialise transport transportSwitchSM(stInit); } } void transportSwitchSM(transportState_t &newState) { if (_transportSM.currentState != &newState) { _transportSM.stateRetries = 0u; // state change, reset retry counter _transportSM.currentState = &newState; // change state } else { _transportSM.stateRetries++; // increment retries } if (_transportSM.currentState) { _transportSM.currentState->Transition(); // State transition } _transportSM.stateEnter = hwMillis(); // save time } uint32_t transportTimeInState(void) { return hwMillis() - _transportSM.stateEnter; } void transportUpdateSM(void) { if (_transportSM.currentState) { _transportSM.currentState->Update(); } } bool isTransportReady(void) { return _transportSM.uplinkOk; } bool isTransportExtendedFailure(void) { return _transportSM.failureCounter == MY_TRANSPORT_MAX_TSM_FAILURES; } bool isTransportSearchingParent(void) { return _transportSM.findingParentNode; } bool isMessageReceived(void) { return _transportSM.msgReceived; } void resetMessageReceived(void) { _transportSM.msgReceived = false; } void transportInitialise(void) { _transportSM.failureCounter = 0u; // reset failure counter transportLoadRoutingTable(); // load routing table to RAM (if feature enabled) // initial state _transportSM.currentState = NULL; transportSwitchSM(stInit); } void transportDisable(void) { if (RADIO_CAN_POWER_OFF == true) { TRANSPORT_DEBUG(PSTR("TSF:TDI:TPD\n")); // power down transport transportPowerDown(); } else { TRANSPORT_DEBUG(PSTR("TSF:TDI:TSL\n")); // send transport to sleep transportSleep(); } } void transportReInitialise(void) { if (RADIO_CAN_POWER_OFF == true) { TRANSPORT_DEBUG(PSTR("TSF:TRI:TPU\n")); // transport power up transportPowerUp(); transportSetAddress(_transportConfig.nodeId); } else { TRANSPORT_DEBUG(PSTR("TSF:TRI:TSB\n")); // transport standby transportStandBy(); } } bool transportWaitUntilReady(const uint32_t waitingMS) { // check if transport ready TRANSPORT_DEBUG(PSTR("TSF:WUR:MS=%" PRIu32 "\n"), waitingMS); // timeout uint32_t enterMS = hwMillis(); bool result = false; while (!result && ( hwMillis() - enterMS < waitingMS || !waitingMS)) { transportProcess(); result = isTransportReady(); doYield(); } return result; } // update TSM and process incoming messages void transportProcess(void) { // update state machine transportUpdateSM(); // process transport FIFO transportProcessFIFO(); } bool transportCheckUplink(const bool force) { if (!force && (hwMillis() - _transportSM.lastUplinkCheck) < MY_TRANSPORT_CHKUPL_INTERVAL_MS) { TRANSPORT_DEBUG(PSTR("TSF:CKU:OK,FCTRL\n")); // flood control return true; } // ping GW const uint8_t hopsCount = transportPingNode(GATEWAY_ADDRESS); // verify hops if (hopsCount != INVALID_HOPS) { // update _transportSM.lastUplinkCheck = hwMillis(); TRANSPORT_DEBUG(PSTR("TSF:CKU:OK\n")); // did distance to GW change upstream, eg. re-routing of uplink nodes if (hopsCount != _transportConfig.distanceGW) { TRANSPORT_DEBUG(PSTR("TSF:CKU:DGWC,O=%" PRIu8 ",N=%" PRIu8 "\n"), _transportConfig.distanceGW, hopsCount); // distance to GW changed _transportConfig.distanceGW = hopsCount; } return true; } else { TRANSPORT_DEBUG(PSTR("TSF:CKU:FAIL\n")); return false; } } bool transportAssignNodeID(const uint8_t newNodeId) { // verify if ID valid if (newNodeId != GATEWAY_ADDRESS && newNodeId != AUTO) { _transportConfig.nodeId = newNodeId; transportSetAddress(newNodeId); // Write ID to EEPROM hwWriteConfig(EEPROM_NODE_ID_ADDRESS, newNodeId); TRANSPORT_DEBUG(PSTR("TSF:SID:OK,ID=%" PRIu8 "\n"),newNodeId); // Node ID assigned return true; } else { TRANSPORT_DEBUG(PSTR("!TSF:SID:FAIL,ID=%" PRIu8 "\n"),newNodeId); // ID is invalid, cannot assign ID setIndication(INDICATION_ERR_NET_FULL); _transportConfig.nodeId = AUTO; return false; } } bool transportRouteMessage(MyMessage &message) { const uint8_t destination = message.destination; uint8_t route = _transportConfig.parentNodeId; // by default, all traffic is routed via parent node if (_transportSM.findingParentNode && destination != BROADCAST_ADDRESS) { TRANSPORT_DEBUG(PSTR("!TSF:RTE:FPAR ACTIVE\n")); // find parent active, message not sent // request to send a non-BC message while finding parent active, abort return false; } if (destination == GATEWAY_ADDRESS) { route = _transportConfig.parentNodeId; // message to GW always routes via parent } else if (destination == BROADCAST_ADDRESS) { route = BROADCAST_ADDRESS; // message to BC does not require routing } else { #if defined(MY_REPEATER_FEATURE) // destination not GW & not BC, get route route = transportGetRoute(destination); if (route == AUTO) { TRANSPORT_DEBUG(PSTR("!TSF:RTE:%" PRIu8 " UNKNOWN\n"), destination); // route unknown #if !defined(MY_GATEWAY_FEATURE) if (message.last != _transportConfig.parentNodeId) { // message not from parent, i.e. child node - route it to parent route = _transportConfig.parentNodeId; } else { // route unknown and msg received from parent, send it to destination assuming in rx radius route = destination; } #else // if GW, all unknown destinations are directly addressed route = destination; #endif } #else if (destination > GATEWAY_ADDRESS && destination < BROADCAST_ADDRESS) { // node2node traffic: assume node is in vincinity. If transmission fails, hand over to parent if (transportSendWrite(destination, message)) { TRANSPORT_DEBUG(PSTR("TSF:RTE:N2N OK\n")); return true; } TRANSPORT_DEBUG(PSTR("!TSF:RTE:N2N FAIL\n")); } route = _transportConfig.parentNodeId; // not a repeater, all traffic routed via parent #endif } // send message const bool result = transportSendWrite(route, message); #if !defined(MY_GATEWAY_FEATURE) // update counter if (route == _transportConfig.parentNodeId) { if (!result) { setIndication(INDICATION_ERR_TX); _transportSM.failedUplinkTransmissions++; } else { _transportSM.failedUplinkTransmissions = 0u; #if defined(MY_SIGNAL_REPORT_ENABLED) // update uplink quality monitor const int16_t signalStrengthRSSI = transportGetSignalReport(SR_TX_RSSI); _transportSM.uplinkQualityRSSI = static_cast((1 - UPLINK_QUALITY_WEIGHT) * _transportSM.uplinkQualityRSSI + (UPLINK_QUALITY_WEIGHT * transportRSSItoInternal(signalStrengthRSSI))); #endif } } #else if(!result) { setIndication(INDICATION_ERR_TX); } #endif return result; } bool transportSendRoute(MyMessage &message) { bool result = false; if (isTransportReady()) { result = transportRouteMessage(message); } else { // TNR: transport not ready TRANSPORT_DEBUG(PSTR("!TSF:SND:TNR\n")); } return result; } // only be used inside transport bool transportWait(const uint32_t waitingMS, const uint8_t cmd, const uint8_t msgType) { const uint32_t enterMS = hwMillis(); // invalidate msg type _msg.type = !msgType; bool expectedResponse = false; while ((hwMillis() - enterMS < waitingMS) && !expectedResponse) { // process incoming messages transportProcessFIFO(); doYield(); expectedResponse = (mGetCommand(_msg) == cmd && _msg.type == msgType); } return expectedResponse; } uint8_t transportPingNode(const uint8_t targetId) { if(!_transportSM.pingActive) { TRANSPORT_DEBUG(PSTR("TSF:PNG:SEND,TO=%" PRIu8 "\n"), targetId); if(targetId == _transportConfig.nodeId) { // pinging self _transportSM.pingResponse = 0u; } else { _transportSM.pingActive = true; _transportSM.pingResponse = INVALID_HOPS; (void)transportRouteMessage(build(_msgTmp, targetId, NODE_SENSOR_ID, C_INTERNAL, I_PING).set((uint8_t)0x01)); // Wait for ping reply or timeout (void)transportWait(2000, C_INTERNAL, I_PONG); } // make sure missing I_PONG msg does not block pinging function by leaving pingActive=true _transportSM.pingActive = false; return _transportSM.pingResponse; } else { TRANSPORT_DEBUG(PSTR("!TSF:PNG:ACTIVE\n")); // ping active, cannot start new ping return INVALID_HOPS; } } uint32_t transportGetHeartbeat(void) { return transportTimeInState(); } void transportProcessMessage(void) { // Manage signing timeout (void)signerCheckTimer(); // receive message setIndication(INDICATION_RX); uint8_t payloadLength = transportReceive((uint8_t *) &_msg.last); // last is the first byte of the payload buffer // get message length and limit size const uint8_t msgLength = min(mGetLength(_msg), (uint8_t)MAX_PAYLOAD); // calculate expected length const uint8_t expectedMessageLength = HEADER_SIZE + (mGetSigned(_msg) ? MAX_PAYLOAD : msgLength); #if defined(MY_RF24_ENABLE_ENCRYPTION) || defined(MY_NRF5_ESB_ENABLE_ENCRYPTION) || defined(MY_RFM95_ENABLE_ENCRYPTION) // payload length = a multiple of blocksize length for decrypted messages, i.e. cannot be used for payload length check payloadLength = expectedMessageLength; #endif const uint8_t command = mGetCommand(_msg); const uint8_t type = _msg.type; const uint8_t sender = _msg.sender; const uint8_t last = _msg.last; const uint8_t destination = _msg.destination; TRANSPORT_DEBUG(PSTR("TSF:MSG:READ,%" PRIu8 "-%" PRIu8 "-%" PRIu8 ",s=%" PRIu8 ",c=%" PRIu8 ",t=%" PRIu8 ",pt=%" PRIu8 ",l=%" PRIu8 ",sg=%" PRIu8 ":%s\n"), sender, last, destination, _msg.sensor, command, type, mGetPayloadType(_msg), msgLength, mGetSigned(_msg), ((command == C_INTERNAL && type == I_NONCE_RESPONSE) ? "" : _msg.getString(_convBuf))); // Reject payloads with incorrect length if (payloadLength != expectedMessageLength) { setIndication(INDICATION_ERR_LENGTH); TRANSPORT_DEBUG(PSTR("!TSF:MSG:LEN=%" PRIu8 ",EXP=%" PRIu8 "\n"), payloadLength, expectedMessageLength); // invalid payload length return; } // Reject messages with incorrect protocol version if (mGetVersion(_msg) != PROTOCOL_VERSION) { setIndication(INDICATION_ERR_VERSION); TRANSPORT_DEBUG(PSTR("!TSF:MSG:PVER,%" PRIu8 "!=%" PRIu8 "\n"), mGetVersion(_msg), PROTOCOL_VERSION); // protocol version mismatch return; } // Reject messages that do not pass verification if (!signerVerifyMsg(_msg)) { setIndication(INDICATION_ERR_SIGN); TRANSPORT_DEBUG(PSTR("!TSF:MSG:SIGN VERIFY FAIL\n")); return; } // update routing table if msg not from parent #if defined(MY_REPEATER_FEATURE) #if !defined(MY_GATEWAY_FEATURE) if (last != _transportConfig.parentNodeId) { #else // GW doesn't have parent { #endif // Message is from one of the child nodes and not sent from this node. Add it to routing table. if (sender != _transportConfig.nodeId) { transportSetRoute(sender, last); } } #endif // MY_REPEATER_FEATURE // set message received flag _transportSM.msgReceived = true; // Is message addressed to this node? if (destination == _transportConfig.nodeId) { // prevent buffer overflow by limiting max. possible message length (5 bits=31 bytes max) to MAX_PAYLOAD (25 bytes) mSetLength(_msg, min(mGetLength(_msg), (uint8_t)MAX_PAYLOAD)); // null terminate data _msg.data[msgLength] = 0u; // Check if sender requests an ack back. if (mGetRequestAck(_msg)) { TRANSPORT_DEBUG(PSTR("TSF:MSG:ACK REQ\n")); // ACK requested _msgTmp = _msg; // Copy message mSetRequestAck(_msgTmp, false); // Reply without ack flag (otherwise we would end up in an eternal loop) mSetAck(_msgTmp, true); // set ACK flag _msgTmp.sender = _transportConfig.nodeId; _msgTmp.destination = sender; // send ACK, use transportSendRoute since ACK reply is not internal, i.e. if !transportOK do not reply (void)transportSendRoute(_msgTmp); } if(!mGetAck(_msg)) { // only process if not ACK if (command == C_INTERNAL) { // Process signing related internal messages if (signerProcessInternal(_msg)) { return; // Signer processing indicated no further action needed } #if !defined(MY_GATEWAY_FEATURE) if (type == I_ID_RESPONSE) { #if (MY_NODE_ID == AUTO) // only active if node ID dynamic if ((_msg.sensor == _transportToken) || (_msg.sensor == AUTO)) { (void)transportAssignNodeID(_msg.getByte()); } else { TRANSPORT_DEBUG(PSTR("!TSF:MSG:ID TK INVALID\n")); } #endif return; // no further processing required } if (type == I_FIND_PARENT_RESPONSE) { #if !defined(MY_GATEWAY_FEATURE) && !defined(MY_PARENT_NODE_IS_STATIC) if (_transportSM.findingParentNode) { // only process if find parent active // Reply to a I_FIND_PARENT_REQUEST message. Check if the distance is shorter than we already have. uint8_t distance = _msg.getByte(); if (isValidDistance(distance)) { distance++; // Distance to gateway is one more for us w.r.t. parent // update settings if distance shorter or preferred parent found if (((isValidDistance(distance) && distance < _transportConfig.distanceGW) || (!_autoFindParent && sender == (uint8_t)MY_PARENT_NODE_ID)) && !_transportSM.preferredParentFound) { // Found a neighbor closer to GW than previously found if (!_autoFindParent && sender == (uint8_t)MY_PARENT_NODE_ID) { _transportSM.preferredParentFound = true; TRANSPORT_DEBUG(PSTR("TSF:MSG:FPAR PREF\n")); // find parent, preferred parent found } _transportConfig.distanceGW = distance; _transportConfig.parentNodeId = sender; TRANSPORT_DEBUG(PSTR("TSF:MSG:FPAR OK,ID=%" PRIu8 ",D=%" PRIu8 "\n"), _transportConfig.parentNodeId, _transportConfig.distanceGW); } } } else { TRANSPORT_DEBUG(PSTR("!TSF:MSG:FPAR INACTIVE\n")); // find parent response received, but inactive } return; // no further processing required #endif } #endif // !defined(MY_GATEWAY_FEATURE) // general if (type == I_PING) { TRANSPORT_DEBUG(PSTR("TSF:MSG:PINGED,ID=%" PRIu8 ",HP=%" PRIu8 "\n"), sender, _msg.getByte()); // node pinged #if defined(MY_GATEWAY_FEATURE) && (F_CPU>16000000) // delay for fast GW and slow nodes delay(5); #endif (void)transportRouteMessage(build(_msgTmp, sender, NODE_SENSOR_ID, C_INTERNAL, I_PONG).set((uint8_t)1)); return; // no further processing required } if (type == I_PONG) { if (_transportSM.pingActive) { _transportSM.pingActive = false; _transportSM.pingResponse = _msg.getByte(); TRANSPORT_DEBUG(PSTR("TSF:MSG:PONG RECV,HP=%" PRIu8 "\n"), _transportSM.pingResponse); // pong received } else { TRANSPORT_DEBUG(PSTR("!TSF:MSG:PONG RECV,INACTIVE\n")); // pong received, but !pingActive } return; // no further processing required } if (type == I_SIGNAL_REPORT_REVERSE) { return; // no further processing required } if (type == I_SIGNAL_REPORT_REQUEST) { int16_t value = INVALID_RSSI; #if defined(MY_SIGNAL_REPORT_ENABLED) const char command = _msg.data[0]; if (_msg.data[1] != '!') { value = transportSignalReport(command); } else { // send request if (transportRouteMessage(build(_msgTmp, _msg.last, NODE_SENSOR_ID, C_INTERNAL, I_SIGNAL_REPORT_REVERSE).set((uint8_t)255))) { // S>s, R>r, ascii delta = 32 value = transportSignalReport(command + 32); // reverse }; } #endif (void)transportRouteMessage(build(_msgTmp, GATEWAY_ADDRESS, NODE_SENSOR_ID, C_INTERNAL, I_SIGNAL_REPORT_RESPONSE).set(value)); return; // no further processing required } if (_processInternalCoreMessage()) { return; // no further processing required } } else if (command == C_STREAM) { #if defined(MY_OTA_FIRMWARE_FEATURE) if(firmwareOTAUpdateProcess()) { return; // OTA FW update processing indicated no further action needed } #endif } } else { TRANSPORT_DEBUG( PSTR("TSF:MSG:ACK\n")); // received message is ACK, no internal processing, handover to msg callback } #if defined(MY_OTA_LOG_RECEIVER_FEATURE) if ((type == I_LOG_MESSAGE) && (command == C_INTERNAL)) { OTALogPrint(_msg); return; // no further processing required } #endif //defined(MY_OTA_LOG_RECEIVER_FEATURE) #if defined(MY_GATEWAY_FEATURE) // Hand over message to controller (void)gatewayTransportSend(_msg); #endif // Call incoming message callback if available if (receive) { receive(_msg); } } else if (destination == BROADCAST_ADDRESS) { TRANSPORT_DEBUG(PSTR("TSF:MSG:BC\n")); // broadcast msg if (command == C_INTERNAL) { if (isTransportReady()) { // only reply if node is fully operational if (type == I_FIND_PARENT_REQUEST) { #if defined(MY_REPEATER_FEATURE) if (sender != _transportConfig.parentNodeId) { // no circular reference TRANSPORT_DEBUG(PSTR("TSF:MSG:FPAR REQ,ID=%" PRIu8 "\n"), sender); // FPAR: find parent request // check if uplink functional - node can only be parent node if link to GW functional // this also prevents circular references in case GW ooo if (transportCheckUplink()) { _transportSM.lastUplinkCheck = hwMillis(); TRANSPORT_DEBUG(PSTR("TSF:MSG:GWL OK\n")); // GW uplink ok // random delay minimizes collisions delay(hwMillis() & 0x3ff); (void)transportRouteMessage(build(_msgTmp, sender, NODE_SENSOR_ID, C_INTERNAL, I_FIND_PARENT_RESPONSE).set(_transportConfig.distanceGW)); } else { TRANSPORT_DEBUG(PSTR("!TSF:MSG:GWL FAIL\n")); // GW uplink fail, do not respond to parent request } } #endif return; // no further processing required, do not forward } } // isTransportReady if (type == I_FIND_PARENT_RESPONSE) { return; // no further processing required, do not forward } #if !defined(MY_GATEWAY_FEATURE) if (type == I_DISCOVER_REQUEST) { if (last == _transportConfig.parentNodeId) { // random wait to minimize collisions delay(hwMillis() & 0x3ff); (void)transportRouteMessage(build(_msgTmp, sender, NODE_SENSOR_ID, C_INTERNAL, I_DISCOVER_RESPONSE).set(_transportConfig.parentNodeId)); // no return here (for fwd if repeater) } } #endif } // controlled BC relay #if defined(MY_REPEATER_FEATURE) // controlled BC repeating: forward only if message received from parent and sender not self to prevent circular fwds if(last == _transportConfig.parentNodeId && sender != _transportConfig.nodeId && isTransportReady()) { TRANSPORT_DEBUG(PSTR("TSF:MSG:FWD BC MSG\n")); // controlled broadcast msg forwarding (void)transportRouteMessage(_msg); } #endif // Callback for BC, only for non-internal messages if (command != C_INTERNAL) { #if !defined(MY_GATEWAY_FEATURE) // only proceed if message received from parent if (last != _transportConfig.parentNodeId) { return; } #endif #if defined(MY_GATEWAY_FEATURE) // Hand over message to controller (void)gatewayTransportSend(_msg); #endif if (receive) { TRANSPORT_DEBUG(PSTR("TSF:MSG:RCV CB\n")); // hand over message to receive callback function receive(_msg); } } } else { // msg not to us and not BC, relay msg #if defined(MY_REPEATER_FEATURE) if (isTransportReady()) { if (command == C_INTERNAL) { if (type == I_PING || type == I_PONG) { uint8_t hopsCnt = _msg.getByte(); TRANSPORT_DEBUG(PSTR("TSF:MSG:REL PxNG,HP=%" PRIu8 "\n"), hopsCnt); if (hopsCnt != MAX_HOPS) { hopsCnt++; _msg.set(hopsCnt); } } } // Relay this message to another node TRANSPORT_DEBUG(PSTR("TSF:MSG:REL MSG\n")); // relay msg (void)transportRouteMessage(_msg); } #else TRANSPORT_DEBUG(PSTR("!TSF:MSG:REL MSG,NREP\n")); // message relaying request, but not a repeater #endif } (void)last; //avoid cppcheck warning } void transportInvokeSanityCheck(void) { // Suppress this because the function may return a variable value in some configurations // cppcheck-suppress knownConditionTrueFalse if (!transportSanityCheck()) { TRANSPORT_DEBUG(PSTR("!TSF:SAN:FAIL\n")); // sanity check fail transportSwitchSM(stFailure); } else { TRANSPORT_DEBUG(PSTR("TSF:SAN:OK\n")); // sanity check ok } } void transportProcessFIFO(void) { if (!_transportSM.transportActive) { // transport not active, no further processing required return; } #if defined(MY_TRANSPORT_SANITY_CHECK) if (hwMillis() - _lastSanityCheck > MY_TRANSPORT_SANITY_CHECK_INTERVAL_MS) { _lastSanityCheck = hwMillis(); transportInvokeSanityCheck(); } #endif uint8_t _processedMessages = MAX_SUBSEQ_MSGS; // process all msgs in FIFO or counter exit while (transportAvailable() && _processedMessages--) { transportProcessMessage(); } #if defined(MY_OTA_FIRMWARE_FEATURE) if (isTransportReady()) { // only process if transport ok firmwareOTAUpdateRequest(); } #endif } bool transportSendWrite(const uint8_t to, MyMessage &message) { message.last = _transportConfig.nodeId; // Update last // sign message if required if (!signerSignMsg(message)) { TRANSPORT_DEBUG(PSTR("!TSF:MSG:SIGN FAIL\n")); setIndication(INDICATION_ERR_SIGN); return false; } // msg length changes if signed const uint8_t totalMsgLength = HEADER_SIZE + ( mGetSigned(message) ? MAX_PAYLOAD : mGetLength( message) ); // send setIndication(INDICATION_TX); bool result = transportSend(to, &message, min((uint8_t)MAX_MESSAGE_LENGTH, totalMsgLength), _transportConfig.passiveMode); // broadcasting (workaround counterfeits) result |= (to == BROADCAST_ADDRESS); TRANSPORT_DEBUG(PSTR("%sTSF:MSG:SEND,%" PRIu8 "-%" PRIu8 "-%" PRIu8 "-%" PRIu8 ",s=%" PRIu8 ",c=%" PRIu8 ",t=%" PRIu8 ",pt=%" PRIu8 ",l=%" PRIu8 ",sg=%" PRIu8 ",ft=%" PRIu8 ",st=%s:%s\n"), (_transportConfig.passiveMode ? "?" : result ? "" : "!"), message.sender, message.last, to, message.destination, message.sensor, mGetCommand(message), message.type, mGetPayloadType(message), mGetLength(message), mGetSigned(message), _transportSM.failedUplinkTransmissions, (result ? "OK" : "NACK"), ((mGetCommand(message) == C_INTERNAL && message.type == I_NONCE_RESPONSE) ? "" : message.getString(_convBuf))); return result; } void transportRegisterReadyCallback(transportCallback_t cb) { _transportReady_cb = cb; } uint8_t transportGetNodeId(void) { return _transportConfig.nodeId; } uint8_t transportGetParentNodeId(void) { return _transportConfig.parentNodeId; } uint8_t transportGetDistanceGW(void) { return _transportConfig.distanceGW; } void transportClearRoutingTable(void) { for (uint16_t i = 0; i < SIZE_ROUTES; i++) { transportSetRoute((uint8_t)i, BROADCAST_ADDRESS); } transportSaveRoutingTable(); // save cleared routing table to EEPROM (if feature enabled) TRANSPORT_DEBUG(PSTR("TSF:CRT:OK\n")); // clear routing table } void transportLoadRoutingTable(void) { #if defined(MY_RAM_ROUTING_TABLE_ENABLED) hwReadConfigBlock((void*)&_transportRoutingTable.route, (void*)EEPROM_ROUTES_ADDRESS, SIZE_ROUTES); TRANSPORT_DEBUG(PSTR("TSF:LRT:OK\n")); // load routing table #endif } void transportSaveRoutingTable(void) { #if defined(MY_RAM_ROUTING_TABLE_ENABLED) hwWriteConfigBlock((void*)&_transportRoutingTable.route, (void*)EEPROM_ROUTES_ADDRESS, SIZE_ROUTES); TRANSPORT_DEBUG(PSTR("TSF:SRT:OK\n")); // save routing table #endif } void transportSetRoute(const uint8_t node, const uint8_t route) { #if defined(MY_RAM_ROUTING_TABLE_ENABLED) _transportRoutingTable.route[node] = route; #else hwWriteConfig(EEPROM_ROUTES_ADDRESS + node, route); #endif } uint8_t transportGetRoute(const uint8_t node) { uint8_t result; #if defined(MY_RAM_ROUTING_TABLE_ENABLED) result = _transportRoutingTable.route[node]; #else result = hwReadConfig(EEPROM_ROUTES_ADDRESS + node); #endif return result; } void transportReportRoutingTable(void) { #if defined(MY_REPEATER_FEATURE) for (uint16_t cnt = 0; cnt < SIZE_ROUTES; cnt++) { const uint8_t route = transportGetRoute(cnt); if (route != BROADCAST_ADDRESS) { TRANSPORT_DEBUG(PSTR("TSF:RRT:ROUTE N=%" PRIu8 ",R=%" PRIu8 "\n"), cnt, route); uint8_t outBuf[2] = { (uint8_t)cnt,route }; (void)_sendRoute(build(_msgTmp, GATEWAY_ADDRESS, NODE_SENSOR_ID, C_INTERNAL, I_DEBUG).set(outBuf, 2)); wait(200); } } #endif } void transportTogglePassiveMode(const bool OnOff) { #if !defined (MY_PASSIVE_NODE) _transportConfig.passiveMode = OnOff; #else (void)OnOff; #endif } int16_t transportGetSignalReport(const signalReport_t signalReport) { int16_t result; switch (signalReport) { case SR_RX_RSSI: result = transportGetReceivingRSSI(); break; case SR_TX_RSSI: result = transportGetSendingRSSI(); break; case SR_RX_SNR: result = transportGetReceivingSNR(); break; case SR_TX_SNR: result = transportGetSendingSNR(); break; case SR_TX_POWER_LEVEL: result = transportGetTxPowerLevel(); break; case SR_TX_POWER_PERCENT: result = transportGetTxPowerPercent(); break; case SR_UPLINK_QUALITY: result = transportInternalToRSSI(_transportSM.uplinkQualityRSSI); break; default: result = 0; break; } return result; } int16_t transportSignalReport(const char command) { signalReport_t reportCommand; switch (command) { case 'S': // SNR (if available) of incoming message reportCommand = SR_RX_SNR; break; case 's': // SNR (if available) of incoming ACK message reportCommand = SR_TX_SNR; break; case 'R': // RSSI (if available) of incoming message reportCommand = SR_RX_RSSI; break; case 'r': // RSSI (if available) of incoming ACK message reportCommand = SR_TX_RSSI; break; case 'P': // TX powerlevel in % reportCommand = SR_TX_POWER_PERCENT; break; case 'T': // TX powerlevel in dBm reportCommand = SR_TX_POWER_LEVEL; break; case 'U': // Uplink quality reportCommand = SR_UPLINK_QUALITY; break; default: reportCommand = SR_NOT_DEFINED; break; } const uint16_t result = transportGetSignalReport(reportCommand); TRANSPORT_DEBUG(PSTR("TSF:SIR:CMD=%" PRIu8 ",VAL=%" PRIu16 "\n"), reportCommand, result); return result; }