/* * 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-2015 Sensnology AB * Full contributor list: https://github.com/mysensors/Arduino/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" static uint32_t _transport_lastUplinkCheck; //!< last uplink check, to prevent GW flooding // repeaters (this includes GW) should perform regular sanity checks for network reliability #if defined(MY_TRANSPORT_SANITY_CHECK) || defined(MY_REPEATER_FEATURE) uint32_t _transport_lastSanityCheck; //!< last sanity check #endif // SM: transitions and update states static State stInit = { stInitTransition, NULL }; static State stParent = { stParentTransition, stParentUpdate }; static State stID = { stIDTransition, stIDUpdate }; static State stUplink = { stUplinkTransition, NULL }; static State stOK = { stOKTransition, stOKUpdate }; static State stFailure = { stFailureTransition, stFailureUpdate }; static transportSM _transportSM; // stInit: initialize transport HW void stInitTransition() { debug(PSTR("TSM:INIT\n")); // initialize status variables _transportSM.failedUplinkTransmissions = 0; _transportSM.pingActive = false; _transportSM.transportActive = false; #if defined(MY_TRANSPORT_SANITY_CHECK) || defined(MY_REPEATER_FEATURE) _transport_lastSanityCheck = hwMillis(); #endif _transport_lastUplinkCheck = 0; // Read node settings (ID, parentId, GW distance) from EEPROM hwReadConfigBlock((void*)&_nc, (void*)EEPROM_NODE_ID_ADDRESS, sizeof(NodeConfig)); // initialize radio if (!transportInit()) { debug(PSTR("!TSM:RADIO:FAIL\n")); setIndication(INDICATION_ERR_INIT_TRANSPORT); transportSwitchSM(stFailure); } else { debug(PSTR("TSM:RADIO:OK\n")); _transportSM.transportActive = true; #if defined(MY_GATEWAY_FEATURE) // Set configuration for gateway debug(PSTR("TSM:GW MODE\n")); _nc.parentNodeId = GATEWAY_ADDRESS; _nc.distance = 0; _nc.nodeId = GATEWAY_ADDRESS; transportSetAddress(GATEWAY_ADDRESS); transportSwitchSM(stOK); #else if (MY_NODE_ID != AUTO) { // Set static id _nc.nodeId = MY_NODE_ID; // Save static id in eeprom hwWriteConfig(EEPROM_NODE_ID_ADDRESS, MY_NODE_ID); } // set ID if static or set in EEPROM if(_nc.nodeId!=AUTO) transportAssignNodeID(_nc.nodeId); transportSwitchSM(stParent); #endif } } // stParent: find parent void stParentTransition() { debug(PSTR("TSM:FPAR\n")); // find parent _transportSM.preferredParentFound = false; _transportSM.findingParentNode = true; _transportSM.failedUplinkTransmissions = 0; _transportSM.uplinkOk = false; // Set distance to max and invalidate parent node id _nc.distance = DISTANCE_INVALID; _nc.parentNodeId = AUTO; // Broadcast find parent request setIndication(INDICATION_FIND_PARENT); transportRouteMessage(build(_msgTmp, _nc.nodeId, BROADCAST_ADDRESS, NODE_SENSOR_ID, C_INTERNAL, I_FIND_PARENT, false).set("")); } // stParentUpdate void stParentUpdate() { if (transportTimeInState() > STATE_TIMEOUT || _transportSM.preferredParentFound) { // timeout or preferred parent found if (_nc.parentNodeId != AUTO) { debug(PSTR("TSM:FPAR:OK\n")); // find parent ok _transportSM.findingParentNode = false; setIndication(INDICATION_GOT_PARENT); transportSwitchSM(stID); } else if (transportTimeInState() > STATE_TIMEOUT) { if (_transportSM.retries < STATE_RETRIES) { // reenter if timeout and retries left transportSwitchSM(stParent); } else { debug(PSTR("!TSM:FPAR:FAIL\n")); // find parent fail setIndication(INDICATION_ERR_FIND_PARENT); transportSwitchSM(stFailure); } } } } // stID: verify and request ID if necessary void stIDTransition() { debug(PSTR("TSM:ID\n")); if (_nc.nodeId == AUTO) { // send ID request setIndication(INDICATION_REQ_NODEID); transportRouteMessage(build(_msgTmp, _nc.nodeId, GATEWAY_ADDRESS, NODE_SENSOR_ID, C_INTERNAL, I_ID_REQUEST, false).set("")); } } void stIDUpdate() { if (_nc.nodeId != AUTO) { // current node ID is valid, proceed to uplink check debug(PSTR("TSM:CHKID:OK (ID=%d)\n"), _nc.nodeId); setIndication(INDICATION_GOT_NODEID); // check uplink transportSwitchSM(stUplink); } else if (transportTimeInState() > STATE_TIMEOUT) { if (_transportSM.retries < STATE_RETRIES) { // re-enter if retries left transportSwitchSM(stID); } else { // fail debug(PSTR("!TSM:CHKID:FAIL (ID=%d)\n"), _nc.nodeId); setIndication(INDICATION_ERR_GET_NODEID); transportSwitchSM(stFailure); } } } void stUplinkTransition() { debug(PSTR("TSM:UPL\n")); if(transportCheckUplink(true)) { debug(PSTR("TSM:UPL:OK\n")); transportSwitchSM(stOK); } else { debug(PSTR("!TSM:UPL:FAIL\n")); transportSwitchSM(stParent); } } void stOKTransition() { debug(PSTR("TSM:READY\n")); // transport is ready _transportSM.uplinkOk = true; } // stOK update: monitors uplink failures void stOKUpdate() { #if !defined(MY_GATEWAY_FEATURE) if (_transportSM.failedUplinkTransmissions > TRANSMISSION_FAILURES) { // too many uplink transmissions failed, find new parent #if !defined(MY_PARENT_NODE_IS_STATIC) debug(PSTR("!TSM:UPL FAIL, SNP\n")); transportSwitchSM(stParent); #else debug(PSTR("!TSM:UPL FAIL, STATP\n")); _transportSM.failedUplinkTransmissions = 0; #endif } #endif } // stFailure: entered upon HW init failure or max retries exceeded void stFailureTransition() { debug(PSTR("!TSM:FAILURE\n")); _transportSM.uplinkOk = false; _transportSM.transportActive = false; setIndication(INDICATION_ERR_INIT_TRANSPORT); // power down transport, no need until re-init debug(PSTR("TSM:PDT\n")); transportPowerDown(); } void stFailureUpdate() { if (transportTimeInState()> TIMEOUT_FAILURE_STATE) { transportSwitchSM(stInit); } } void transportSwitchSM(State& newState) { if (_transportSM.currentState != &newState) { // state change, reset retry counter _transportSM.retries = 0; // change state _transportSM.currentState = &newState; } else { _transportSM.retries++; // increment retries } // Transition event if (_transportSM.currentState->Transition) _transportSM.currentState->Transition(); // save time _transportSM.stateEnter = hwMillis(); } uint32_t transportTimeInState() { return hwMillis() - _transportSM.stateEnter; } void transportUpdateSM(){ if (_transportSM.currentState->Update) _transportSM.currentState->Update(); } bool isTransportOK() { return (_transportSM.uplinkOk); } void transportInitialize() { // intial state _transportSM.currentState = &stFailure; transportSwitchSM(stInit); } // update TSM and process incoming messages void transportProcess() { // update state machine transportUpdateSM(); // process transport FIFO transportProcessFIFO(); } bool transportCheckUplink(bool force) { if (!force && (hwMillis() - _transport_lastUplinkCheck) < CHKUPL_INTERVAL) { debug(PSTR("TSP:CHKUPL:OK (FLDCTRL)\n")); // flood control return true; } // ping GW uint8_t hopsCount = transportPingNode(GATEWAY_ADDRESS); // verify hops if (hopsCount != INVALID_HOPS) { // update _transport_lastUplinkCheck = hwMillis(); debug(PSTR("TSP:CHKUPL:OK\n")); // did distance to GW change upstream, i.e. re-routing of uplink nodes? if (hopsCount != _nc.distance) { debug(PSTR("TSP:CHKUPL:DGWC (old=%d,new=%d)"), _nc.distance, hopsCount); // distance to GW changed _nc.distance = hopsCount; } return true; } else { debug(PSTR("TSP:CHKUPL:FAIL (hops=%d)\n"), hopsCount); return false; } } void transportAssignNodeID(uint8_t newNodeId) { // verify if ID valid if (newNodeId != GATEWAY_ADDRESS && newNodeId != AUTO) { _nc.nodeId = newNodeId; transportSetAddress(newNodeId); // Write ID to EEPROM hwWriteConfig(EEPROM_NODE_ID_ADDRESS, newNodeId); debug(PSTR("TSP:ASSIGNID:OK (ID=%d)\n"),newNodeId); } else { debug(PSTR("!TSP:ASSIGNID:FAIL (ID=%d)\n"),newNodeId); setIndication(INDICATION_ERR_NET_FULL); // Nothing else we can do... transportSwitchSM(stFailure); } } bool transportRouteMessage(MyMessage &message) { uint8_t destination = message.destination; uint8_t route; if (_transportSM.findingParentNode && destination != BROADCAST_ADDRESS) { debug(PSTR("!TSP:FPAR:ACTIVE (msg not send)\n")); // request to send a non-BC message while finding parent active, abort return false; } if (destination == GATEWAY_ADDRESS) { route = _nc.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 = hwReadConfig(EEPROM_ROUTES_ADDRESS + destination); if (route == AUTO) { // route unknown if (message.last != _nc.parentNodeId) { // message not from parent, i.e. child node - route it to parent debug(PSTR("!TSP:ROUTING:DEST UNKNOWN (dest=%d, STP=%d)\n"), destination, _nc.parentNodeId); route = _nc.parentNodeId; } else { // route unknown and msg received from parent, send it to destination assuming in rx radius route = destination; } } #else route = _nc.parentNodeId; // not a repeater, all traffic routed via parent #endif } // send message bool ok = transportSendWrite(route, message); #if !defined(MY_GATEWAY_FEATURE) // update counter if (route == _nc.parentNodeId) { if (!ok) { setIndication(INDICATION_ERR_TX); _transportSM.failedUplinkTransmissions++; } else _transportSM.failedUplinkTransmissions = 0; } #else if(!ok) setIndication(INDICATION_ERR_TX); #endif return ok; } bool transportSendRoute(MyMessage &message) { if (isTransportOK()) { return transportRouteMessage(message); } else { // TNR: transport not ready debug(PSTR("!TSP:SEND:TNR\n")); return false; } } // only be used inside transport bool transportWait(uint32_t ms, uint8_t cmd, uint8_t msgtype){ uint32_t enter = hwMillis(); // invalidate msg type _msg.type = !msgtype; bool expectedResponse = false; while ((hwMillis() - enter < ms) && !expectedResponse) { // process incoming messages transportProcessFIFO(); #if defined(ARDUINO_ARCH_ESP8266) yield(); #endif expectedResponse = (mGetCommand(_msg) == cmd && _msg.type == msgtype); } return expectedResponse; } uint8_t transportPingNode(uint8_t targetId) { if(!_transportSM.pingActive){ if(targetId == _nc.nodeId) { // ping to ourself, pingActive remains false return 0; } _transportSM.pingActive = true; _transportSM.pingResponse = INVALID_HOPS; debug(PSTR("TSP:PING:SEND (dest=%d)\n"), targetId); transportRouteMessage(build(_msgTmp, _nc.nodeId, targetId, NODE_SENSOR_ID, C_INTERNAL, I_PING, false).set((uint8_t)0x01)); // Wait for ping reply or timeout transportWait(2000, C_INTERNAL, I_PONG); // make sure missing I_PONG msg does not block pinging function by leaving pignActive=true _transportSM.pingActive = false; return _transportSM.pingResponse; } else { return INVALID_HOPS; } } void transportClearRoutingTable() { for (uint8_t i = 0; i != 255; i++) { hwWriteConfig(EEPROM_ROUTES_ADDRESS + i, BROADCAST_ADDRESS); } debug(PSTR("TSP:CRT:OK\n")); // clear routing table } uint32_t transportGetHeartbeat() { return transportTimeInState(); } void transportProcessMessage() { (void)signerCheckTimer(); // Manage signing timeout uint8_t payloadLength = transportReceive((uint8_t *)&_msg); (void)payloadLength; // currently not used, but good to test for CRC-ok but corrupt msgs setIndication(INDICATION_RX); uint8_t command = mGetCommand(_msg); uint8_t type = _msg.type; uint8_t sender = _msg.sender; uint8_t last = _msg.last; uint8_t destination = _msg.destination; debug(PSTR("TSP:MSG:READ %d-%d-%d s=%d,c=%d,t=%d,pt=%d,l=%d,sg=%d:%s\n"), sender, last, destination, _msg.sensor, mGetCommand(_msg), type, mGetPayloadType(_msg), mGetLength(_msg), mGetSigned(_msg), _msg.getString(_convBuf)); // verify protocol version if(mGetVersion(_msg) != PROTOCOL_VERSION) { setIndication(INDICATION_ERR_VERSION); debug(PSTR("!TSP:MSG:PVER mismatch\n")); // protocol version return; } // Reject massages that do not pass verification if (!signerVerifyMsg(_msg)) { setIndication(INDICATION_ERR_SIGN); debug(PSTR("!TSP:MSG:SIGN verify fail\n")); return; } if (destination == _nc.nodeId) { // This message is addressed to this node // prevent buffer overflow by limiting max. possible message length (5 bits=31 bytes max) to MAX_PAYLOAD (25 bytes) mSetLength(_msg, min(mGetLength(_msg),MAX_PAYLOAD)); // null terminate data _msg.data[mGetLength(_msg)] = 0x00; // update routing table if msg not from parent #if defined(MY_REPEATER_FEATURE) if (last != _nc.parentNodeId) { // Message is from one of the child nodes. Add it to routing table. hwWriteConfig(EEPROM_ROUTES_ADDRESS+sender, last); } #endif // Check if sender requests an ack back. if (mGetRequestAck(_msg)) { _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 = _nc.nodeId; _msgTmp.destination = sender; // send ACK debug(PSTR("TSP:MSG:ACK msg\n")); // use transportSendRoute since ACK reply is not internal, i.e. if !transportOK do not reply 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 transportAssignNodeID(_msg.getByte()); #endif return; // no further processing required } if (type == I_FIND_PARENT_RESPONSE) { #if !defined(MY_GATEWAY_FEATURE) // Reply to a I_FIND_PARENT message. Check if the distance is shorter than we already have. uint8_t distance = _msg.getByte(); debug(PSTR("TSP:MSG:FPAR RES (ID=%d, dist=%d)\n"), sender, distance); if (isValidDistance(distance)) { // Distance to gateway is one more for us w.r.t. parent distance++; // update settings if distance shorter or preferred parent found if (((isValidDistance(distance) && distance < _nc.distance) || (!_autoFindParent && sender == MY_PARENT_NODE_ID)) && !_transportSM.preferredParentFound) { // Found a neighbor closer to GW than previously found if (!_autoFindParent && sender == MY_PARENT_NODE_ID) { _transportSM.preferredParentFound = true; debug(PSTR("TSP:MSG:FPAR (PPAR FOUND)\n")); // preferred parent found } _nc.distance = distance; _nc.parentNodeId = sender; debug(PSTR("TSP:MSG:PAR OK (ID=%d, dist=%d)\n"), _nc.parentNodeId, _nc.distance); } } return; #endif } #endif // general if (type == I_PING) { debug(PSTR("TSP:MSG:PINGED (ID=%d, hops=%d)\n"), sender, _msg.getByte()); transportRouteMessage(build(_msgTmp, _nc.nodeId, sender, NODE_SENSOR_ID, C_INTERNAL, I_PONG, false).set((uint8_t)0x01)); return; // no further processing required } if (type == I_PONG) { if (_transportSM.pingActive) { _transportSM.pingActive = false; _transportSM.pingResponse = _msg.getByte(); debug(PSTR("TSP:MSG:PONG RECV (hops=%d)\n"), _transportSM.pingResponse); } return; // no further processing required } if (_processInternalMessages()) { 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 } } #if defined(MY_GATEWAY_FEATURE) // Hand over message to controller gatewayTransportSend(_msg); #else // Call incoming message callback if available if (receive) { receive(_msg); } #endif return; } else if (destination == BROADCAST_ADDRESS) { // broadcast debug(PSTR("TSP:MSG:BC\n")); if (command == C_INTERNAL) { if (isTransportOK()) { // only reply if node is fully operational if (type == I_FIND_PARENT) { #if defined(MY_REPEATER_FEATURE) if (sender != _nc.parentNodeId) { // no circular reference debug(PSTR("TSP:MSG:FPAR REQ (sender=%d)\n"), sender); // FPR: find parent request // node is in our range, update routing table - important if node has new repeater as parent hwWriteConfig(EEPROM_ROUTES_ADDRESS + sender, sender); // 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(false)){ #if defined(MY_REPEATER_FEATURE) _transport_lastUplinkCheck = hwMillis(); #endif debug(PSTR("TSP:MSG:GWL OK\n")); // GW uplink ok // delay minimizes collisions delay(hwMillis() & 0x3ff); transportRouteMessage(build(_msgTmp, _nc.nodeId, sender, NODE_SENSOR_ID, C_INTERNAL, I_FIND_PARENT_RESPONSE, false).set(_nc.distance)); } } #endif return; // no further processing required } } if (type == I_DISCOVER) { if (last == _nc.parentNodeId) { // random wait to minimize collisions delay(hwMillis() & 0x3ff); transportRouteMessage(build(_msgTmp, _nc.nodeId, sender, NODE_SENSOR_ID, C_INTERNAL, I_DISCOVER_RESPONSE, false).set(_nc.parentNodeId)); // no return here (for fwd if repeater) } } } // 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 == _nc.parentNodeId && sender != _nc.nodeId && isTransportOK()){ debug(PSTR("TSP:MSG:FWD BC MSG\n")); // forward BC msg transportRouteMessage(_msg); } #endif // Call incoming message callback if available, but only if message received from parent if (command != C_INTERNAL && last == _nc.parentNodeId && receive) { receive(_msg); } } else { // msg not no us and not BC, relay msg #if defined(MY_REPEATER_FEATURE) if (isTransportOK()) { debug(PSTR("TSP:MSG:REL MSG\n")); // relay msg // update routing table if message not received from parent if (last != _nc.parentNodeId) { hwWriteConfig(EEPROM_ROUTES_ADDRESS + sender, last); } if (command == C_INTERNAL) { if (type == I_PING || type == I_PONG) { uint8_t hopsCnt = _msg.getByte(); if (hopsCnt != MAX_HOPS) { debug(PSTR("TSP:MSG:REL PxNG (hops=%d)\n"), hopsCnt); _msg.set((uint8_t)(hopsCnt + 1)); } } } // Relay this message to another node transportRouteMessage(_msg); } #else debug(PSTR("!TSM:MSG:REL MSG, but not a repeater\n")); #endif } } inline void transportProcessFIFO() { if (_transportSM.transportActive) { #if defined(MY_TRANSPORT_SANITY_CHECK) || defined(MY_REPEATER_FEATURE) if (hwMillis() > (_transport_lastSanityCheck + MY_TRANSPORT_SANITY_CHECK_INTERVAL)) { _transport_lastSanityCheck = hwMillis(); if (!transportSanityCheck()) { debug(PSTR("!TSP:SANCHK:FAIL\n")); // sanity check fail transportSwitchSM(stFailure); return; } else { debug(PSTR("TSP:SANCHK:OK\n")); // sanity check ok } } #endif } else { // transport not active, nothing to be done return; } 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 (isTransportOK()) { // only process if transport ok firmwareOTAUpdateRequest(); } #endif } bool transportSendWrite(uint8_t to, MyMessage &message) { // set protocol version and update last mSetVersion(message, PROTOCOL_VERSION); message.last = _nc.nodeId; // sign message if required if (!signerSignMsg(message)) { debug(PSTR("!TSP:MSG:SIGN fail\n")); setIndication(INDICATION_ERR_SIGN); return false; } // msg length changes if signed uint8_t length = mGetSigned(message) ? MAX_MESSAGE_LENGTH : mGetLength(message); // send setIndication(INDICATION_TX); bool ok = transportSend(to, &message, min(MAX_MESSAGE_LENGTH, HEADER_SIZE + length)); debug(PSTR("%sTSP:MSG:SEND %d-%d-%d-%d s=%d,c=%d,t=%d,pt=%d,l=%d,sg=%d,ft=%d,st=%s:%s\n"), (ok || to == BROADCAST_ADDRESS ? "" : "!"),message.sender,message.last, to, message.destination, message.sensor, mGetCommand(message), message.type, mGetPayloadType(message), mGetLength(message), mGetSigned(message), _transportSM.failedUplinkTransmissions, to==BROADCAST_ADDRESS ? "bc" : (ok ? "ok":"fail"), message.getString(_convBuf)); return (ok || to==BROADCAST_ADDRESS); }