/* xdrv_57_1_tasmesh_support.ino - Mesh via ESP-Now support for Tasmota Copyright (C) 2021 Christian Baars and Theo Arends This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifdef USE_TASMESH #include #include #ifdef ESP32 #include #include #else #include #endif //ESP32 /*********************************************************************************************\ * constants \*********************************************************************************************/ #define D_CMND_MESH "MESH" #define MESH_PAYLOAD_SIZE 160 // Default 160 - with header of 20 bytes and 16 bytes tag, stays below 200 bytes, which is reported to work with ESP8266 #define MESH_TOPICSZ 64 // Max supported topic size #define MESH_BUFFERS 26 // (6) Max buffers number for splitted messages #define MESH_MAX_PACKETS 3 // (3) Max number of packets #define MESH_REFRESH 50 // Number of ms // ------------------------------------------------------------------------------------------------------------ // | MAC Header | Category Code | Organization Identifier | Random Values | Vendor Specific Content | FCS | // ------------------------------------------------------------------------------------------------------------ // 24 bytes 1 byte 3 bytes 4 bytes 7~255 bytes 4 bytes // ------------------------------------------------------------------------------- // | Element ID | Length | Organization Identifier | Type | Version | Body | // ------------------------------------------------------------------------------- // 1 byte 1 byte 3 bytes 1 byte 1 byte 0~250 bytes struct mesh_packet_t { uint8_t sender[6]; // MAC uint8_t receiver[6]; // MAC uint32_t counter:4; // Rolling counter to identify a packet uint32_t type:6; // Command, Mqtt, ... uint32_t chunks:6; // Number of chunks uint32_t chunk:6; // Chunk number uint32_t chunkSize:8; // Chunk size uint32_t TTL:2; // Time to live, counting down union { uint32_t senderTime; // UTC-timestamp from every sender in the MESH uint32_t peerIndex; // Only for resending in the MESH }; uint8_t tag[16]; // Tag for de/encryption uint8_t payload[MESH_PAYLOAD_SIZE]; } __attribute__((packed)); struct mesh_packet_header_t { // ToDo: Maybe, we do not need this uint8_t sender[6]; // MAC uint8_t receiver[6]; // MAC uint32_t counter:4; // Rolling counter to identify a packet uint32_t type:6; // Command, Mqtt, ... uint32_t chunks:6; // Number of chunks uint32_t chunk:6; // Chunk number uint32_t chunkSize:8; // Chunk size uint32_t TTL:2; // Time to live, counting down union { uint32_t senderTime; // UTC-timestamp from every sender in the MESH uint32_t peerIndex; // Only for resending in the MESH }; uint8_t tag[16]; // Tag for de/encryption } __attribute__((packed)); struct mesh_peer_t { uint8_t MAC[6]; uint32_t lastMessageFromPeer; // Time of last message from peer #ifdef ESP32 char topic[MESH_TOPICSZ]; #endif //ESP32 }; struct mesh_flags_t { uint8_t brokerNeedsTopic:1; uint8_t nodeGotTime:1; uint8_t nodeWantsTime:1; uint8_t nodeWantsTimeASAP:1; }; struct mesh_packet_combined_t { mesh_packet_header_t header; uint32_t receivedChunks; // Bitmask for up to 32 chunks char raw[MESH_PAYLOAD_SIZE * MESH_BUFFERS]; }; struct mesh_first_header_bytes { // TODO: evaluate random 4-byte-value of pre-packet uint8_t raw[15]; } __attribute__((packed));; struct { uint32_t lastMessageFromBroker; // Time of last message from broker uint32_t lmfap; // Yime of last message from any peer uint8_t broker[6] = { 0 }; uint8_t key[32]; uint8_t role; uint8_t channel; // Wifi channel uint8_t interval; uint8_t currentTopicSize; mesh_flags_t flags; mesh_packet_t sendPacket; std::vector peers; std::queue packetToResend; std::queue packetToConsume; std::vector packetsAlreadySended; std::vector packetsAlreadyReceived; std::vector multiPackets; #ifdef ESP32 std::vector lastTeleMsgs; #endif //ESP32 } MESH; /*********************************************************************************************\ * Declarations for functions with custom types \*********************************************************************************************/ void MESHsendPacket(mesh_packet_t *_packet); bool MESHencryptPayload(mesh_packet_t *_packet, int _encrypt); // 1 encryption, 0 decryption /*********************************************************************************************\ * enumerations \*********************************************************************************************/ enum MESH_Commands { // commands useable in console or rules CMND_MESH_BROKER, // start broker on ESP32 CMND_MESH_NODE, // start node and connect to broker based on MAC address CMND_MESH_PEER, // add node to peer list of a broker or node CMND_MESH_CHANNEL}; // set wifi channel on node (the broker gets it automatically from the AP) enum MESH_Role { ROLE_NONE = 0, // not initialized ROLE_BROKER, // ESP32 will connect mesh to WLAN ROLE_NODE_FULL, // Node will listen and resend every message for MESH functionality ROLE_NODE_SMALL // Node will only talk to the broker }; enum MESH_Packet_Type { // Type of packet PACKET_TYPE_TIME = 0, // PACKET_TYPE_PEERLIST, // send all kown peers, broker is always 0 PACKET_TYPE_COMMAND, // not used yet PACKET_TYPE_REGISTER_NODE, // register a node with encrypted broker-MAC, announce mqtt topic to ESP32-proxy - broker will send time ASAP PACKET_TYPE_REFRESH_NODE, // refresh node infos with encrypted broker-MAC, announce mqtt topic to ESP32-proxy - broker will send time slightly delayed PACKET_TYPE_MQTT, // send regular mqtt messages, single or multipackets PACKET_TYPE_WANTTOPIC // the broker has no topic for this peer/node }; /*********************************************************************************************\ * \*********************************************************************************************/ #ifdef ESP32 void MESHsendTime(void) { // Only from broker to nodes MESH.sendPacket.counter++; MESH.sendPacket.type = PACKET_TYPE_TIME; MESH.sendPacket.TTL = 1; // memcpy(MESH.sendPacket.receiver,MESH.peers[_peerNumber].MAC,6); MESH.sendPacket.senderTime = Rtc.utc_time; MESH.sendPacket.payload[0] = 0; // mesh_flags_t *_flags = (mesh_flags_t *)MESH.sendPacket.payload; // if(MESH.peers[_peerNumber].topic[0]==0){ // AddLog(LOG_LEVEL_INFO, PSTR("MSH: Broker wants topic from peer: %u"), _peerNumber); // _flags->brokerNeedsTopic = 1; // } MESH.sendPacket.chunkSize = 0; MESH.sendPacket.chunks = 0; MESHsendPacket(&MESH.sendPacket); } void MESHdemandTopic(uint32_t _peerNumber) { MESH.sendPacket.counter++; MESH.sendPacket.type = PACKET_TYPE_WANTTOPIC; MESH.sendPacket.TTL = 2; MESH.sendPacket.payload[0] = 0; MESH.sendPacket.chunkSize = 0; MESH.sendPacket.chunks = 0; memcpy(MESH.sendPacket.receiver,MESH.peers[_peerNumber].MAC,6); MESHsendPacket(&MESH.sendPacket); } #endif //ESP32 void MESHsendPeerList(void) { // We send this list only to the peers, that can directly receive it MESH.sendPacket.counter++; MESH.sendPacket.type = PACKET_TYPE_PEERLIST; MESH.sendPacket.senderTime = Rtc.utc_time; uint32_t _idx = 0; for (auto &_peer : MESH.peers) { memcpy(MESH.sendPacket.payload + _idx, _peer.MAC, 6); _idx += 6; } if (0 == _idx) { return; } MESH.sendPacket.chunk = 0; MESH.sendPacket.chunks = 1; MESH.sendPacket.chunkSize = _idx; MESH.sendPacket.TTL = 1; // AddLogBuffer(LOG_LEVEL_INFO, MESH.sendPacket.payload, MESH.sendPacket.chunkSize); MESHsendPacket(&MESH.sendPacket); } bool MESHcheckPeerList(const uint8_t *MAC) { bool success = false; for (auto &_peer : MESH.peers) { if (memcmp(_peer.MAC, MAC, 6) == 0) { _peer.lastMessageFromPeer = millis(); return true; } } return false; } uint8_t MESHcountPeers(void) { #ifdef ESP32 esp_now_peer_num_t _peernum; esp_now_get_peer_num(&_peernum); uint8_t _num = _peernum.total_num; #else uint8_t _num; uint8_t _numEnc; // wo don't care esp_now_get_cnt_info(&_num, &_numEnc); #endif // AddLog(LOG_LEVEL_DEBUG, PSTR("MSH: Peers %u"), _num); return _num; } int MESHaddPeer(uint8_t *_MAC ) { mesh_peer_t _newPeer; memcpy(_newPeer.MAC, _MAC, 6); _newPeer.lastMessageFromPeer = millis(); #ifdef ESP32 _newPeer.topic[0] = 0; #endif MESH.peers.push_back(_newPeer); #ifdef ESP32 std::string _msg = "{\"Init\":1}"; // Init with a simple JSON only while developing MESH.lastTeleMsgs.push_back(_msg); // We must keep this vector in sync with the peers-struct on the broker regarding the indices #endif //ESP32 int err; #ifdef ESP32 esp_now_peer_info_t _peer; _peer.channel = MESH.channel; _peer.encrypt = false; _peer.ifidx = (wifi_interface_t)ESP_IF_WIFI_AP; memcpy(_peer.peer_addr, _MAC, 6); err = esp_now_add_peer(&_peer); #else err = esp_now_add_peer(_MAC, ESP_NOW_ROLE_COMBO, MESH.channel, NULL, 0); #endif if (0 == err) { char _peerMAC[18]; ToHex_P(_MAC, 6, _peerMAC, 18, ':'); AddLog(LOG_LEVEL_DEBUG, PSTR("MSH: Peer %s added successful"), _peerMAC); #ifdef ESP32 if (ROLE_BROKER == MESH.role) { MESHsendTime(); } #endif //ESP32 Response_P(PSTR("{\"%s\":{\"Peers\":%u}}"), D_CMND_MESH, MESH.peers.size()); XdrvRulesProcess(0); return err; } else { AddLog(LOG_LEVEL_DEBUG, PSTR("MSH: Failed to add peer %d"), err); } return err; } //helper functions void MESHstripColon(char* _string) { uint32_t _length = strlen(_string); uint32_t _index = 0; while (_index < _length) { char c = _string[_index]; if (c == ':') { memmove(_string + _index, _string + _index +1, _length - _index); } _index++; } _string[_index] = 0; } void MESHHexStringToBytes(char* _string, uint8_t _MAC[]) { //uppercase MESHstripColon(_string); UpperCase(_string, _string); uint32_t index = 0; uint32_t _end = strlen(_string); memset(_MAC, 0, _end / 2); while (index < _end) { char c = _string[index]; uint8_t value = 0; if ((c >= '0') && (c <= '9')) { value = (c - '0'); } else if ((c >= 'A') && (c <= 'F')) { value = (10 + (c - 'A')); } _MAC[(index / 2)] += value << (((index + 1) % 2) * 4); index++; } } void MESHsendPacket(mesh_packet_t *_packet) { MESHencryptPayload(_packet, 1); // esp_now_send(_packet->receiver, (uint8_t *)_packet, sizeof(MESH.sendPacket) - MESH_PAYLOAD_SIZE + _packet->chunkSize); esp_now_send(NULL, (uint8_t *)_packet, sizeof(MESH.sendPacket) - MESH_PAYLOAD_SIZE + _packet->chunkSize); //NULL -> broadcast } void MESHsetKey(uint8_t* _key) { // Must be 32 bytes!!! char* _pw = SettingsText(SET_STAPWD1 + Settings->sta_active); size_t _length = strlen(_pw); memset(_key, 0, 32); if (_length > 32) { _length = 32; } memcpy(_key, _pw, _length); AddLog(LOG_LEVEL_DEBUG, PSTR("MSH: set crypto key to PASSWORD1")); } bool MESHencryptPayload(mesh_packet_t *_packet, int _encrypt) { // AddLog(LOG_LEVEL_DEBUG, PSTR("MSH: will encrypt: %u"), _encrypt); size_t _size = _packet->chunkSize; char _tag[16]; // AddLog(LOG_LEVEL_DEBUG, PSTR("cc: %u, _size: %u"), _counter,_size); // AddLogBuffer(LOG_LEVEL_DEBUG,(uint8_t*)_tag,16); br_chacha20_run bc = br_chacha20_ct_run; br_poly1305_ctmul32_run((void*)MESH.key, (const void *)_packet, (void *)_packet->payload, _size, _packet->receiver+6, 2, _tag, bc, _encrypt); // AddLog(LOG_LEVEL_DEBUG, PSTR("MSH: encryption done ")); if (_encrypt==1) { memcpy(_packet->tag, _tag, 16); // AddLog(LOG_LEVEL_DEBUG, PSTR("MSH: payload encrypted")); return true; } if (memcmp(_packet->tag, _tag, 16) == 0) { // AddLog(LOG_LEVEL_DEBUG, PSTR("MSH: payload decrypted")); return true; } AddLog(LOG_LEVEL_DEBUG, PSTR("MSH: payload decryption error")); return false; } void MESHsetSleep(void) { if (MESH.role && (Settings->sleep > MESH.interval)) { Settings->sleep = MESH.interval; TasmotaGlobal.sleep = MESH.interval; } } void MESHsetWifi(bool state) { #ifdef ESP8266 // Only ESP8266 as ESP32 is a broker and needs Wifi if (state) { // Wifi On Settings->flag4.network_wifi = 1; // (Re-)enable wifi as long as Mesh is not enabled // TasmotaGlobal.global_state.wifi_down = 0; Settings->flag.global_state = 0; // (Wifi, MQTT) Control link led blinking (1) } else { // Wifi Off and use ESP-NOW Settings->flag4.network_wifi = 0; // The "old" wifi off command TasmotaGlobal.global_state.wifi_down = 1; Settings->flag.global_state = 1; // (Wifi, MQTT) Control link led blinking (1) } #endif // ESP8266 } uint32_t MESHmaxPayloadSize(void) { return MESH_PAYLOAD_SIZE - MESH.currentTopicSize -1; } #endif //USE_TASMESH