/*
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
// The format of the vendor-specific action frame is as follows:
// ------------------------------------------------------------------------------------------------------------
// | 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
//
// The Vendor Specific Content contains vendor-specific fields as follows:
// -------------------------------------------------------------------------------
// | 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