├── CDVE2023 ├── README.md └── slides.pdf ├── LICENSE ├── README.md ├── examples ├── README.md ├── espnow_init.py └── mesh.py ├── firmwares ├── README.md ├── esp32-c3 │ ├── README.md │ └── firmware.bin ├── esp32-s2 │ ├── README.md │ └── firmware.bin ├── esp32-s3 │ ├── README.md │ └── firmware.bin └── esp32 │ ├── README.md │ └── firmware.bin └── media ├── Bridge.jpeg ├── esp32-mcus.jpeg └── mesh_dia1.png /CDVE2023/README.md: -------------------------------------------------------------------------------- 1 | This work is presented at the [CDVE2023](https://www.cdve.org/) conference. 2 | The mp4 video of the presentation is hosted on Google Drive and can be found [here](https://drive.google.com/file/d/1HzKyBpdNmdMhlXoGDcmiV637Yx-BL7lE/view?usp=sharing). 3 | -------------------------------------------------------------------------------- /CDVE2023/slides.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shariltumin/mesh-espnow-micropython/7a69705aeab10d4dc6c34c84f122e9f1415a16dc/CDVE2023/slides.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 shariltumin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dynamic Secure Mesh for Collaborative Nodes of IoT devices 2 | 3 | 1. esp32, esp32-c3, esp32-s2, esp32-s3 4 | 2. [Custom firmwares](https://github.com/shariltumin/mesh-espnow-micropython/tree/main/firmwares) MicroPython-V1.20.0-39 with ESP-IDF-V4.4.4 5 | 3. Exclude BLE, \_thread, and uasyncio 6 | 4. Include espnow and worker 7 | 8 | [Example scripts](https://github.com/shariltumin/mesh-espnow-micropython/tree/main/examples) 9 | 10 | 1. espnow_init.py 11 | 2. mesh.py 12 | 13 | Diagram1 14 | 15 | ![espnow-dynamic-mesh](https://github.com/shariltumin/mesh-espnow-micropython/blob/main/media/mesh_dia1.png) 16 | 17 | Diagram2 18 | 19 | ![mesh-cloud-bridge](https://github.com/shariltumin/mesh-espnow-micropython/blob/main/media/Bridge.jpeg) 20 | 21 | Links 22 | 23 | [workers](https://github.com/shariltumin/workers-framework-micropython) 24 | 25 | [CryptoXo](https://github.com/shariltumin/tscp/blob/main/CryptoXo.py) 26 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | The mesh.py script simulates a dynamic mesh network between ESP32 MCUs. The script is incompatible with the ESP8266. 2 | -------------------------------------------------------------------------------- /examples/espnow_init.py: -------------------------------------------------------------------------------- 1 | from network import WLAN, AP_IF, STA_IF 2 | from espnow import ESPNow 3 | from sys import platform 4 | from CryptoXo import Crypt 5 | 6 | # A WLAN AP interface disable 7 | ap = WLAN(AP_IF) 8 | ap.active(False) 9 | 10 | # A WLAN STA interface must be active to send()/recv() 11 | sta = WLAN(STA_IF) 12 | # sta.config(channel=3) # does not work with esp32-s3 13 | sta.active(True) 14 | sta.disconnect() # disconnect from previous connection 15 | 16 | # node mac address and node identifier 17 | MAC = sta.config('mac') 18 | NID = "%s-%d-%d" % (platform, hash(MAC)%254, hash(MAC)%42) 19 | 20 | # Now setup ESPNOW 21 | ew = ESPNow() 22 | ew.active(True) 23 | 24 | # Set the Primary Master Key (PMK) which is used to encrypt the 25 | # Local Master Keys (LMK) for encrypting ESPNow data traffic. 26 | # Must be 16Bytes long 27 | ew.set_pmk(b'AdsTuiPo10J10lmt') # not sure it is working 28 | 29 | BAS = b'\xff'*6 30 | ew.add_peer(BAS) 31 | 32 | # encryption functions 33 | crypt = Crypt() # only one object - not thread save! 34 | 35 | def encrypt(mac): 36 | crypt.key(mac, 'HSGT 17ysg#1nsv0= hst!!') 37 | return crypt.encrypt 38 | 39 | def decrypt(mac): 40 | crypt.key(mac, 'HSGT 17ysg#1nsv0= hst!!') 41 | return crypt.decrypt 42 | 43 | # usage: 44 | # from espnow_init import MAC, NID, BAS, ew, encrypt, decrypt 45 | # 46 | # where: 47 | # MAC - node mac address 48 | # NID - unique node identifier 49 | # BAS - broadcast mac address 50 | # ew - espnow object 51 | # encrypt - encryption function after new key 52 | # decrypt - decryption function after new key 53 | -------------------------------------------------------------------------------- /examples/mesh.py: -------------------------------------------------------------------------------- 1 | 2 | from machine import reset 3 | # from hashlib import sha1 as sha 4 | from sys import platform 5 | import time 6 | import urandom 7 | from espnow_init import MAC, NID, BAS, ew, encrypt, decrypt 8 | from worker import task, MT 9 | 10 | urandom.seed(4) 11 | 12 | # global structure 13 | MESH = [] # authenticated mac address of current mesh-network 14 | MID = [b'0'*11]*100 # # message id array for already processed messages 15 | midp = 0 # the index to insert new MID. MID[midp]=msg_id; midp=(midp+1)%100 16 | 17 | @task 18 | def process(pm): # process payload 19 | peer, payload = pm 20 | s=(yield) 21 | msg = decrypt(peer)(payload) # pl=encrypt(mac)(msg) 22 | print(MAC.hex(), peer.hex(), msg) 23 | yield 'OK' 24 | 25 | @task 26 | def forward(pm): # message forwarding 27 | sender, source, msg = pm 28 | s=(yield) 29 | for peer in MESH: 30 | if peer not in (sender, source): 31 | try: 32 | w=ew.get_peer(peer) # this will fail if peer not registered 33 | ok = ew.send(peer, msg) # forward the message 34 | if not ok: 35 | print('Fail to forward msg to', peer.hex()) 36 | MESH.remove(peer) # peer may be down or out of reach 37 | except: 38 | print('Peer', peer.hex(), 'not found (forward)') 39 | yield # one at a time 40 | yield 'OK' 41 | 42 | @task 43 | def get_bye(pm): 44 | peer = pm[0] 45 | s=(yield) 46 | try: 47 | MESH.remove(peer) 48 | except: 49 | pass 50 | try: 51 | ew.del_peer(peer) 52 | except: 53 | pass 54 | yield 'OK' 55 | 56 | @task 57 | def send_auq(pm): # send AUQ in response to HEY 58 | peer = pm[0] 59 | s=(yield) 60 | payload = encrypt(MAC)(peer) 61 | msg = b'AUQ'+payload 62 | try: 63 | w=ew.get_peer(peer) 64 | ok = ew.send(peer, msg) 65 | if not ok: 66 | print('Fail to send AUQ to', peer.hex()) 67 | except: 68 | print('Peer', peer.hex(), 'not found (send_auq)') 69 | yield 'OK' 70 | 71 | @task 72 | def send_aur(pm): # process AUQ and send back AUR 73 | sender, msg = pm 74 | s=(yield) 75 | if len(msg)<9: yield 'ER' 76 | node = decrypt(sender)(msg[3:]) 77 | if node != MAC: 78 | print('Fail to authenticate', sender.hex(), 'from AUQ message') 79 | else: 80 | if sender not in MESH: # we can choose to include node in MESH or not 81 | MESH.append(sender) # node is now authenticated 82 | payload = encrypt(MAC)(sender) 83 | msg = b'AUR'+payload 84 | try: 85 | w=ew.get_peer(sender) 86 | ok = ew.send(sender, msg) 87 | if not ok: 88 | print('Fail to send AUR to', sender.hex()) 89 | except: 90 | print('Peer', sender.hex(), 'not found (send_aur)') 91 | yield 'OK' 92 | 93 | @task 94 | def get_aur(pm): # process AUR 95 | sender, msg = pm 96 | s=(yield) 97 | if len(msg)<9: yield 'ER' 98 | node = decrypt(sender)(msg[3:]) 99 | if node != MAC: 100 | print('Fail to authenticate', sender.hex(), 'from AUR message') 101 | else: 102 | if sender not in MESH: # we can choose to include node in MESH or not 103 | MESH.append(sender) # node is now authenticated 104 | yield 'OK' 105 | 106 | @task 107 | def send_msg(pm): # send a message 'MSG' to all authenticated peers 108 | payload = pm[0] 109 | s=(yield) 110 | if type(payload)!=bytes: 111 | payload=b'%s'%payload 112 | # msg='MSGyyyyyyxxxxxxiiiiiddddd........' 113 | # target=msg[3:3+6] # yyyyyy 114 | # source=msg[3+6:3+6+6] # xxxxxx 115 | # mid=msg[3+6:3+6+6+5] # xxxxxxiiiii message id 116 | target=BAS # sent to all 117 | mid=b'%s%05d'%(MAC, hash('%d'%time.ticks_us())) # 11 bytes message ID 118 | msg=b'MSG'+target+mid+encrypt(MAC)(payload) 119 | for peer in MESH: # send to all authenticated peer 120 | try: 121 | w=ew.get_peer(peer) 122 | ok = ew.send(peer, msg) # forward the message 123 | if not ok: 124 | print('Fail to send MSG to', peer.hex()) 125 | MESH.remove(peer) # peer may be down or out of reach 126 | yield # one at a time 127 | except: 128 | print('Peer', peer.hex(), 'not found (send_msg)') 129 | print(ew.peers_table) 130 | yield 'OK' 131 | 132 | @task 133 | def get_any(pm): # get any package from espnow inbuffer 134 | global MID, midp 135 | s=(yield) 136 | while 1: 137 | if not ew.any(): 138 | # print('get_any got', ew.any()) 139 | wait=s.delay(1*1000) # 1 sec. 140 | while wait():yield 141 | continue # restart loop 142 | peer, msg = ew.recv() # this is blocking, therefore after ew.any() 143 | if len(msg) < 3: # msg MUST be 3 chars or more 144 | yield # yield to other tasks 145 | continue # before continue loop 146 | # add peer to espnow peers_table 147 | try: 148 | ew.add_peer(peer) 149 | except: 150 | pass 151 | msg_typ = msg[:3] # message type: HEY, AUQ, AUR, MSG, BYE 152 | if msg_typ == b'HEY': 153 | print('Get HEY from', peer.hex()) 154 | # send AUQ to peer regardless MESH membership 155 | mt.worker(send_auq, (peer,)) 156 | yield 157 | elif msg_typ == b'AUQ': 158 | print('Get AUQ from', peer.hex()) 159 | # process AUQ and send AUR to peer 160 | mt.worker(send_aur, (peer, msg)) 161 | yield 162 | elif msg_typ == b'AUR': 163 | print('Get AUR from', peer.hex()) 164 | # process AUR from peer 165 | mt.worker(get_aur, (peer, msg)) 166 | yield 167 | elif msg_typ == b'MSG': 168 | print('Get MSG from', peer.hex(), 'message', msg[:20], '...') 169 | # mid=b'%s%05d'%(MAC, hash('%d'%time.ticks_us())) 170 | # msg='MSGyyyyyyxxxxxxiiiiiddddd........' 171 | # target=msg[3:3+6] # yyyyyy 172 | # source=msg[3+6:3+6+6] # xxxxxx 173 | # mid=msg[3+6:3+6+6+5] # xxxxxxiiiii message id 174 | # payload=msg[3+6+6+5:] 175 | if peer in MESH: # authenticated peer? 176 | if len(msg)<20: continue 177 | msg_id = msg[9:20] # message id: 11 bytes 178 | if msg_id not in MID: 179 | MID[midp]=msg_id; midp=(midp+1)%100 180 | target=msg[3:9] 181 | source=msg[9:15] 182 | payload = msg[20:] # payload: rest of bytes at pos 20 onward 183 | if len(payload)==0: continue 184 | # process payload - task 185 | mt.worker(process, (source, payload)) 186 | if MAC==target: # distination reach 187 | print('Distination', target.hex(), 'reached') 188 | else: 189 | # forward msg to know peers but not to peer it came from 190 | mt.worker(forward, (peer, source, msg)) 191 | yield 192 | elif msg_typ == b'BYE': 193 | print('Get BYE from', peer.hex()) 194 | # remove peer from MESH membership and espnow peers list 195 | mt.worker(get_bye, (peer,)) 196 | yield 197 | else: 198 | print("Unknown message type", msg_typ, "from", peer.hex()) 199 | yield 200 | 201 | @task 202 | def mesh_in(pm): # try to join mesh network 203 | val = pm[0] # at regular interval in millisecond 204 | s=(yield) 205 | if len(MESH) == 0: # do it immedeatly if no peers 206 | print(MAC.hex(), 'sending HEY') 207 | ok = ew.send(BAS, b'HEY') # broadcast the 'HEY' message 208 | wait=s.delay(30*1000) # wait 30 secs. 209 | while wait():yield 210 | else: 211 | wait=s.delay(val) 212 | while wait():yield 213 | print(MAC.hex(), 'sending HEY') 214 | ok = ew.send(BAS, b'HEY') 215 | wait=s.delay(30*1000) # wait 30 secs. 216 | while wait():yield 217 | mt.worker(mesh_in, (val, )) # every val milliseconds 218 | yield 'OK' 219 | 220 | @task 221 | def debug(pm): # print error log if any 222 | val = pm[0] 223 | s=(yield) 224 | while 1: 225 | err = mt.log() 226 | if err: 227 | print('*DEBUG*', err) 228 | wait=s.delay(val) # wait val secs. 229 | while wait():yield 230 | 231 | @task 232 | def test(pm): # simulate send MSG at random interval 233 | s=(yield) 234 | wait=s.delay((urandom.getrandbits(3)+10)*1000) 235 | while wait():yield 236 | if len(MESH) > 0: # there are some peers to send MSG to 237 | msg = b'The time at ' + NID + ' is now ' + b'%d:%d:%d'%time.localtime()[3:6] 238 | print(MAC.hex(), 'sending MSG') 239 | print(MESH) 240 | mt.worker(send_msg, (msg, )) 241 | mt.worker(test, ()) # try again 242 | yield 'OK' 243 | 244 | 245 | print('MAC:', MAC) 246 | 247 | mt=MT(20) # max 20 tasks 248 | 249 | # register initial tasks 250 | mt.worker(debug, (30*1000, )) # print error (if any) every 30 sec. 251 | mt.worker(mesh_in, (5*60*1000, )) # every 5 mins 252 | mt.worker(get_any, ()) 253 | mt.worker(test, ()) 254 | 255 | print('Start MESH simulation') 256 | 257 | # start MESH simulation 258 | mt.start() 259 | 260 | # Will never reach here 261 | print('Program aborted due to error', mt.log()) 262 | 263 | -------------------------------------------------------------------------------- /firmwares/README.md: -------------------------------------------------------------------------------- 1 | ESP32 variants: (left to right) esp32, esp32-c3, esp32-s2, esp32-s3 2 | 3 | ![esp32-mcus](https://github.com/shariltumin/mesh-espnow-micropython/blob/main/media/esp32-mcus.jpeg) 4 | -------------------------------------------------------------------------------- /firmwares/esp32-c3/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /firmwares/esp32-c3/firmware.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shariltumin/mesh-espnow-micropython/7a69705aeab10d4dc6c34c84f122e9f1415a16dc/firmwares/esp32-c3/firmware.bin -------------------------------------------------------------------------------- /firmwares/esp32-s2/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /firmwares/esp32-s2/firmware.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shariltumin/mesh-espnow-micropython/7a69705aeab10d4dc6c34c84f122e9f1415a16dc/firmwares/esp32-s2/firmware.bin -------------------------------------------------------------------------------- /firmwares/esp32-s3/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /firmwares/esp32-s3/firmware.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shariltumin/mesh-espnow-micropython/7a69705aeab10d4dc6c34c84f122e9f1415a16dc/firmwares/esp32-s3/firmware.bin -------------------------------------------------------------------------------- /firmwares/esp32/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /firmwares/esp32/firmware.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shariltumin/mesh-espnow-micropython/7a69705aeab10d4dc6c34c84f122e9f1415a16dc/firmwares/esp32/firmware.bin -------------------------------------------------------------------------------- /media/Bridge.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shariltumin/mesh-espnow-micropython/7a69705aeab10d4dc6c34c84f122e9f1415a16dc/media/Bridge.jpeg -------------------------------------------------------------------------------- /media/esp32-mcus.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shariltumin/mesh-espnow-micropython/7a69705aeab10d4dc6c34c84f122e9f1415a16dc/media/esp32-mcus.jpeg -------------------------------------------------------------------------------- /media/mesh_dia1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shariltumin/mesh-espnow-micropython/7a69705aeab10d4dc6c34c84f122e9f1415a16dc/media/mesh_dia1.png --------------------------------------------------------------------------------