# mqtt.py MQTT library for the micropython board using an ESP8266. # asyncio version # Author: Peter Hinch # Copyright Peter Hinch 2017 Released under the MIT license # Accessed via pbmqtt.py on Pyboard import gc import ubinascii from mqtt_as import MQTTClient, config from machine import Pin, unique_id, freq import uasyncio as asyncio gc.collect() from network import WLAN, STA_IF, AP_IF import usocket as socket gc.collect() from syncom import SynCom gc.collect() import ustruct as struct from status_values import * # Numeric status values shared with user code. _WIFI_DELAY = 15 # Time (s) to wait for default network blue = Pin(2, Pin.OUT, value = 1) def loads(s): d = {} exec("v=" + s, d) return d["v"] # Format an arbitrary list of positional args as a status_values.SEP separated string def argformat(*a): return SEP.join(['{}' for x in range(len(a))]).format(*a) async def heartbeat(): led = Pin(0, Pin.OUT) while True: await asyncio.sleep_ms(500) led(not led()) class Client(MQTTClient): def __init__(self, channel, config): self.channel = channel self.subscriptions = {} # Config defaults: # 4 repubs, delay of 10 secs between (response_time). # Initially clean session. config['subs_cb'] = self.subs_cb config['wifi_coro'] = self.wifi_han config['connect_coro'] = self.conn_han config['client_id'] = ubinascii.hexlify(unique_id()) super().__init__(config) # Get NTP time or 0 on any error. async def get_time(self): if not self.isconnected(): return 0 res = await self.wan_ok() if not res: return 0 # No internet connectivity. # connectivity check is not ideal. Could fail now... FIXME # (date(2000, 1, 1) - date(1900, 1, 1)).days * 24*60*60 NTP_DELTA = 3155673600 host = "pool.ntp.org" NTP_QUERY = bytearray(48) NTP_QUERY[0] = 0x1b t = 0 async with self.lock: addr = socket.getaddrinfo(host, 123)[0][-1] # Blocks 15s if no internet s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.setblocking(False) try: s.connect(addr) await self._as_write(NTP_QUERY, 48, s) await asyncio.sleep(2) msg = await self._as_read(48, s) val = struct.unpack("!I", msg[40:44])[0] t = val - NTP_DELTA except OSError: pass s.close() if t < 16 * 365 * 24 * 3600: t = 0 self.dprint('Time received: ', t) return t async def wifi_han(self, state): if state: self.channel.send(argformat(STATUS, WIFI_UP)) else: self.channel.send(argformat(STATUS, WIFI_DOWN)) blue(not state) await asyncio.sleep(1) async def conn_han(self, _): for topic, qos in self.subscriptions.items(): await self.subscribe(topic, qos) def subs_cb(self, topic, msg): self.channel.send(argformat(SUBSCRIPTION, topic.decode('UTF8'), msg.decode('UTF8'))) class Channel(SynCom): def __init__(self): mtx = Pin(14, Pin.OUT) # Define pins mckout = Pin(15, Pin.OUT, value=0) # clocks must be initialised to 0 mrx = Pin(13, Pin.IN) mckin = Pin(12, Pin.IN) super().__init__(True, mckin, mckout, mrx, mtx, string_mode = True) self.cstatus = False # Connection status self.client = None # Task runs continuously. Process incoming Pyboard messages. # Started by main_task() after client instantiated. async def from_pyboard(self): client = self.client while True: istr = await self.await_obj(20) # wait for string (poll interval 20ms) s = istr.split(SEP) command = s[0] if command == PUBLISH: await client.publish(s[1], s[2], bool(s[3]), int(s[4])) # If qos == 1 only returns once PUBACK received. self.send(argformat(STATUS, PUBOK)) elif command == SUBSCRIBE: await client.subscribe(s[1], int(s[2])) client.subscriptions[s[1]] = int(s[2]) # re-subscribe after outage elif command == MEM: gc.collect() gc.threshold(gc.mem_free() // 4 + gc.mem_alloc()) self.send(argformat(MEM, gc.mem_free(), gc.mem_alloc())) elif command == TIME: t = await client.get_time() self.send(argformat(TIME, t)) else: self.send(argformat(STATUS, UNKNOWN, 'Unknown command:', istr)) # Runs when channel has synchronised. No return: Pyboard resets ESP on fail. # Get parameters from Pyboard. Process them. Connect. Instantiate client. Start # from_pyboard() task. Wait forever, updating connected status. async def main_task(self, _): got_params = False # Await connection parameters (init record) while not got_params: istr = await self.await_obj(100) ilst = istr.split(SEP) command = ilst[0] if command == 'init': got_params = True ssid, pw, broker, mqtt_user, mqtt_pw, ssl_params = ilst[1:7] use_default = bool(int(ilst[7])) fast = bool(int(ilst[10])) debug = bool(int(ilst[12])) config['server'] = broker config['port'] = int(ilst[8]) config['user'] = mqtt_user config['password'] = mqtt_pw config['keepalive'] = int(ilst[11]) config['ping_interval'] = int(ilst[16]) config['ssl'] = bool(int(ilst[9])) config['ssl_params'] = eval(ssl_params) config['response_time'] = int(ilst[15]) config['clean'] = bool(int(ilst[13])) config['max_repubs'] = int(ilst[14]) elif command == WILL: config['will'] = (ilst[1:3] + [bool(ilst[3])] + [int(ilst[4])]) self.send(argformat(STATUS, WILLOK)) else: self.send(argformat(STATUS, UNKNOWN, 'Expected init, got: ', istr)) # Got parameters if debug: Client.DEBUG = True # verbose output on UART if fast: freq(160000000) # try default LAN if required sta_if = WLAN(STA_IF) if use_default: self.send(argformat(STATUS, DEFNET)) secs = _WIFI_DELAY while secs >= 0 and not sta_if.isconnected(): await asyncio.sleep(1) secs -= 1 # If can't use default, use specified LAN if not sta_if.isconnected(): self.send(argformat(STATUS, SPECNET)) # Pause for confirmation. User may opt to reboot instead. istr = await self.await_obj(100) ap = WLAN(AP_IF) # create access-point interface ap.active(False) # deactivate the interface sta_if.active(True) sta_if.connect(ssid, pw) while not sta_if.isconnected(): await asyncio.sleep(1) # WiFi is up: connect to the broker await asyncio.sleep(5) # Let WiFi stabilise before connecting self.client = Client(self, config) self.send(argformat(STATUS, BROKER_CHECK)) try: await self.client.connect() # Clean session. Throws OSError if broker down. # Sends BROKER_OK and RUNNING except OSError: # Cause Pyboard to reboot us when application requires it. self.send(argformat(STATUS, BROKER_FAIL)) while True: await asyncio.sleep(60) # Twiddle my thumbs. PB will reset me. self.send(argformat(STATUS, BROKER_OK)) self.send(argformat(STATUS, RUNNING)) # Set channel running loop = asyncio.get_event_loop() loop.create_task(self.from_pyboard()) while True: gc.collect() gc.threshold(gc.mem_free() // 4 + gc.mem_alloc()) await asyncio.sleep(1) loop = asyncio.get_event_loop() loop.create_task(heartbeat()) # Comms channel to Pyboard channel = Channel() loop.create_task(channel.start(channel.main_task)) loop.run_forever()