def __init__(self, d): self.user_start = (d['user_start'], d['args']) self.init_str = buildinit(d) # Synchroniser idle until started. self.rtc_synchroniser = RTCsynchroniser(self, d['rtc_resync'], d['local_time_offset']) self.keepalive = d['keepalive'] self._running = False self.verbose = d['verbose'] # Watchdog timeout for ESP8266 (ms). wdog = d['timeout'] * 1000 # SynCom string mode self.channel = SynCom(False, d['sckin'], d['sckout'], d['srx'], d['stx'], d['reset'], wdog, True, self.verbose) loop = asyncio.get_event_loop() loop.create_task(heartbeat()) # Start the SynCom instance. This will run self.start(). If an error # occurs self.quit() is called which cancels all NamedTask instances # and returns to Syncom's start() method. This waits on Cancellable.cancel_all() # before forcing a failure on the ESP8266 and re-running self.start() # to perform a clean restart. loop.create_task(self.channel.start(self.start, Killer())) self.subs = {} # Callbacks indexed by topic self.pubs = [] self._pub_free = True # No publication in progress self.s_han = default_status_handler self.wifi_han = (lambda *_ : None, ()) self._wifi_up = False # Only specify network on first run self.first_run = True
def __init__(self, d): self.user_start = (d['user_start'], d['args']) self.init_str = buildinit(d) # Synchroniser idle until started. self.rtc_synchroniser = RTCsynchroniser(self, d['rtc_resync'], d['local_time_offset']) self.keepalive = d['keepalive'] self._running = False self.verbose = d['verbose'] # Watchdog timeout for ESP8266 (ms). wdog = d['timeout'] * 1000 # SynCom string mode self.channel = SynCom(False, d['sckin'], d['sckout'], d['srx'], d['stx'], d['reset'], wdog, True, self.verbose) loop = asyncio.get_event_loop() loop.create_task(heartbeat()) self.exit_gate = ExitGate() loop.create_task(self.channel.start(self.start, self.exit_gate)) self.subs = {} # Callbacks indexed by topic self.pubs = [] self._pub_free = True # No publication in progress self.s_han = default_status_handler self.wifi_han = (lambda *_: None, ()) self._wifi_up = False # Only specify network on first run self.first_run = True
def test(): freq(160000000) mtx = Pin(14, Pin.OUT) # Define pins mckout = Pin(15, Pin.OUT, value=0) # clocks must be initialised to zero. mrx = Pin(13, Pin.IN) mckin = Pin(12, Pin.IN) objsched = Sched(heartbeat=1) channel = SynCom(objsched, True, mckin, mckout, mrx, mtx) channel.start() objsched.add_thread(passive_thread(channel)) try: objsched.run() finally: mckout(0)
def test(): stx = Pin(Pin.board.Y5, Pin.OUT_PP) # Define pins sckout = Pin(Pin.board.Y6, Pin.OUT_PP) sckout.value(0) # Don't assert clock until data is set srx = Pin(Pin.board.Y7, Pin.IN) sckin = Pin(Pin.board.Y8, Pin.IN) reset = Pin(Pin.board.Y4, Pin.OPEN_DRAIN) objsched = Sched(heartbeat=1) channel = SynCom(objsched, False, sckin, sckout, srx, stx) channel.start(reset, 0) objsched.add_thread(initiator_thread(channel)) try: objsched.run() finally: sckout.value(0)
def test(): freq(160000000) dout = Pin(14, Pin.OUT, value=0) # Define pins ckout = Pin(15, Pin.OUT, value=0) # clocks must be initialised to zero. din = Pin(13, Pin.IN) ckin = Pin(12, Pin.IN) channel = SynCom(True, ckin, ckout, din, dout) loop = asyncio.get_event_loop() loop.create_task(heartbeat()) loop.create_task(channel.start(passive_task)) try: loop.run_forever() except KeyboardInterrupt: pass finally: ckout(0)
def test(): dout = Pin(Pin.board.Y5, Pin.OUT_PP, value=0) # Define pins ckout = Pin(Pin.board.Y6, Pin.OUT_PP, value=0) # Don't assert clock until data is set din = Pin(Pin.board.Y7, Pin.IN) ckin = Pin(Pin.board.Y8, Pin.IN) reset = Pin(Pin.board.Y4, Pin.OPEN_DRAIN) sig_reset = Signal(reset, invert=True) channel = SynCom(False, ckin, ckout, din, dout, sig_reset, 10000) loop = asyncio.get_event_loop() loop.create_task(heartbeat()) loop.create_task(channel.start(initiator_task)) try: loop.run_forever() except KeyboardInterrupt: pass finally: ckout.value(0)
def __init__(self, *args, **kwargs): d = defaults # d is the config dict: initially populate with default values for arg in args: d.update(arg) # Hardware and network configs d.update(kwargs) # d is now populated. self.user_start = d['user_start'] shan = d['status_handler'] self.s_han = (default_status_handler, ()) if shan is None else shan # coro self.crash_han = d['crash_handler'] self.wifi_han = d['wifi_handler'] self.init_str = buildinit(d) self.keepalive = d['keepalive'] self._evtrun = asyncio.Event() self.verbose = d['verbose'] # Watchdog timeout for ESP8266 (ms). wdog = d['timeout'] * 1000 # SynCom string mode self.channel = SynCom(False, d['sckin'], d['sckout'], d['srx'], d['stx'], d['reset'], wdog, True, self.verbose) if 'led' in d: asyncio.create_task(heartbeat(d['led'])) # Start the SynCom instance. This will run self.start(). If an error # occurs self.quit() is called which returns to Syncom's start() method. # This waits on ._die before forcing a failure on the ESP8266 and re-running # self.start() to perform a clean restart. asyncio.create_task(self.channel.start(self.start, self._die)) self.subs = {} # (callback, qos, args) indexed by topic self.publock = asyncio.Lock() self.puback = asyncio.Event() self.evttim = asyncio.Event() self.evtwifi = asyncio.Event() # Only specify network on first run self.first_run = True self._time = 0 # NTP time. Invalid. # ESP8266 returns seconds from 2000 because I believed the docs. # Maintainers change the epoch on a whim. # Calculate ofsets in CPython using datetime.date: # (date(2000, 1, 1) - date(1970, 1, 1)).days * 24*60*60 epoch = {2000 : 0, 1970 : 946684800} # Offset to add. self._epoch_fix = epoch[gmtime(0)[0]] # Find offset on current platform
class MQTTlink: lw_topic = None lw_msg = None lw_retain = False lw_qos = 0 @classmethod def will(cls, topic, msg, retain=False, qos=0): cls.lw_topic = topic cls.lw_msg = msg cls.lw_retain = retain cls.lw_qos = qos status_msgs = ('connected to broker', 'awaiting broker', 'awaiting default network', 'awaiting specified network', 'publish OK', 'running', 'unknown', 'Will registered', 'Fail to connect to broker', 'WiFi up', 'WiFi down') def __init__(self, *args, **kwargs): d = defaults # d is the config dict: initially populate with default values for arg in args: d.update(arg) # Hardware and network configs d.update(kwargs) # d is now populated. self.user_start = d['user_start'] shan = d['status_handler'] self.s_han = (default_status_handler, ()) if shan is None else shan # coro self.crash_han = d['crash_handler'] self.wifi_han = d['wifi_handler'] self.init_str = buildinit(d) self.keepalive = d['keepalive'] self._evtrun = asyncio.Event() self.verbose = d['verbose'] # Watchdog timeout for ESP8266 (ms). wdog = d['timeout'] * 1000 # SynCom string mode self.channel = SynCom(False, d['sckin'], d['sckout'], d['srx'], d['stx'], d['reset'], wdog, True, self.verbose) if 'led' in d: asyncio.create_task(heartbeat(d['led'])) # Start the SynCom instance. This will run self.start(). If an error # occurs self.quit() is called which returns to Syncom's start() method. # This waits on ._die before forcing a failure on the ESP8266 and re-running # self.start() to perform a clean restart. asyncio.create_task(self.channel.start(self.start, self._die)) self.subs = {} # (callback, qos, args) indexed by topic self.publock = asyncio.Lock() self.puback = asyncio.Event() self.evttim = asyncio.Event() self.evtwifi = asyncio.Event() # Only specify network on first run self.first_run = True self._time = 0 # NTP time. Invalid. # ESP8266 returns seconds from 2000 because I believed the docs. # Maintainers change the epoch on a whim. # Calculate ofsets in CPython using datetime.date: # (date(2000, 1, 1) - date(1970, 1, 1)).days * 24*60*60 epoch = {2000 : 0, 1970 : 946684800} # Offset to add. self._epoch_fix = epoch[gmtime(0)[0]] # Find offset on current platform # API async def publish(self, topic, msg, retain=False, qos=0): qos_check(qos) validate(topic, 'topic') # Raise ValueError if invalid. validate(msg, 'message') await self.ready() async with self.publock: self.channel.send(argformat(PUBLISH, topic, msg, int(retain), qos)) # Wait for ESP8266 to complete. Quick if qos==0. May be very slow # in an outage await self.puback.wait() async def subscribe(self, topic, qos, callback, *args): qos_check(qos) # Save subscription to resubscribe after outage self.subs[topic] = (callback, qos, args) # Subscribe await self.ready() self.channel.send(argformat(SUBSCRIBE, topic, qos)) # Command handled directly by mqtt.py on ESP8266 e.g. MEM async def command(self, *argsend): await self.ready() self.channel.send(argformat(*argsend)) def running(self): return self._evtrun.is_set() async def ready(self): await self._evtrun.wait() await self.evtwifi.wait() def wifi(self): return self.evtwifi.is_set() # Attempt to retrieve NTP time in secs since 2000 or device epoch async def get_time(self, pause=120, y2k=False): delta = 0 if y2k else self._epoch_fix self.evttim.clear() # Defensive self._time = 0 # Invalidate while True: await self.ready() self.channel.send(TIME) try: await asyncio.wait_for(self.evttim.wait(), pause // 2) except asyncio.TimeoutError: pass if self._time: return self._time + delta # Fix epoch await asyncio.sleep(pause) # API END def _do_time(self, action): # TIME received from ESP8266 try: self._time = int(action[0]) except ValueError: # Gibberish. self._time = 0 # May be 0 if ESP has signalled failure self.evttim.set() self.evttim.clear() async def _die(self): # ESP has crashed. Run user callback if provided. cb, args = self.crash_han cb(self, *args) await asyncio.sleep_ms(0) # Convenience method allows this error code: # return self.quit(message) # This method is called from self.start. Note that return is to SynCom's # .start method which will launch ._die def quit(self, *args): if args is not (): self.verbose and print(*args) self._evtrun.clear() def get_cmd(self, istr): ilst = istr.split(SEP) return ilst[0], ilst[1:] def do_status(self, action, last_status): try: iact = int(action[0]) except ValueError: # Gibberish from ESP8266: caller reboots return UNKNOWN if iact == PUBOK: # Occurs after all publications. If qos==0 immediately, otherwise # when PUBACK received from broker. May take a long time in outage. # If a crash occurs, puback is cleared by start() self.puback.set() self.puback.clear() elif iact == RUNNING: self._evtrun.set() if iact == WIFI_UP: self.evtwifi.set() elif iact == WIFI_DOWN: self.evtwifi.clear() # Detect WiFi status changes. Ignore initialisation and repeats if last_status != -1 and iact != last_status and iact in (WIFI_UP, WIFI_DOWN): cb, args = self.wifi_han cb(self.evtwifi.is_set(), self, *args) if self.verbose: if iact != UNKNOWN: if iact != last_status: # Ignore repeats printtime() print('Status: ', self.status_msgs[iact]) else: printtime() print('Unknown status: {} {}'.format(action[1], action[2])) return iact # start() is run each time the channel acquires sync i.e. on startup and also # after an ESP8266 crash and reset. # Behaviour after fatal error with ESP8266: # It clears ._evtrun to cause local tasks to terminate, then returns. # A return causes the local channel instance to launch .die then issues # a hardware reset to the ESP8266 (See SynCom.start() and .run() methods). # After acquiring sync, start() gets rescheduled. async def start(self, channel): self.verbose and print('Starting...') self.puback.set() # If a crash occurred while a pub was pending self.puback.clear() # let it terminate and release the lock self.evttim.set() # Likewise for get_time: let it return fail status. self.evttim.clear() await asyncio.sleep_ms(0) self._evtrun.clear() # Set by .do_status s_task, s_args = self.s_han if self.lw_topic is not None: channel.send(argformat(WILL, self.lw_topic, self.lw_msg, self.lw_retain, self.lw_qos)) res = await channel.await_obj() if res is None: # SynCom timeout await s_task(self, ESP_FAIL, *s_args) return self.quit('ESP8266 fail 1. Resetting.') command, action = self.get_cmd(res) if command == STATUS: iact = self.do_status(action, -1) await s_task(self, iact, *s_args) if iact in _BAD_STATUS: return self.quit('Bad status: {}. Resetting.'.format(iact)) else: self.verbose and print('Expected will OK got: ', command, action) channel.send(self.init_str) while not self._evtrun.is_set(): # Until RUNNING status received res = await channel.await_obj() if res is None: await s_task(self, ESP_FAIL, *s_args) return self.quit('ESP8266 fail 2. Resetting.') command, action = self.get_cmd(res) if command == STATUS: iact = self.do_status(action, -1) result = await s_task(self, iact, *s_args) if iact == SPECNET: if result == 1: channel.send('1') # Any string will do else: return self.quit() if iact in _BAD_STATUS: return self.quit('Bad status. Resetting.') else: self.verbose and print('Init got: ', command, action) # On power up this will do nothing because user awaits .ready # before subscribing, so self.subs will be empty. for topic in self.subs: qos = self.subs[topic][1] self.channel.send(argformat(SUBSCRIBE, topic, qos)) self.verbose and print('About to run user program.') if self.user_start[0] is not None: self.user_start[0](self, *self.user_start[1]) # User start function cb, args = self.wifi_han cb(True, self, *args) # Initialisation is complete. Process messages from ESP8266. iact = -1 # Invalidate last status for change detection while True: # print incoming messages, handle subscriptions chan_state = channel.any() if chan_state is None: # SynCom Timeout self._evtrun.clear() elif chan_state > 0: res = await channel.await_obj() command, action = self.get_cmd(res) if command == SUBSCRIPTION: if action[0] in self.subs: # topic found cb, qos, args = self.subs[action[0]] action += args # Callback gets topic, message, retained, plus any user args cb(*action) # Run the callback elif command == STATUS: # 1st arg of status is an integer iact = self.do_status(action, iact) # Update pub q and wifi status await s_task(self, iact, *s_args) if iact in _DIRE_STATUS: return self.quit('Fatal status. Resetting.') elif command == TIME: self._do_time(action) elif command == MEM: # Wouldn't ask for this if we didn't want a printout print('ESP8266 RAM free: {} allocated: {}'.format(action[0], action[1])) else: await s_task(self, UNKNOWN, *s_args) return self.quit('Got unhandled command, resetting ESP8266:', command, action) # ESP8266 has failed await asyncio.sleep_ms(20) if not self._evtrun.is_set(): # self.quit() has been called. await s_task(self, NO_NET, *s_args) return self.quit('Not running, resetting ESP8266')
class MQTTlink(object): lw_topic = None lw_msg = None lw_retain = False lw_qos = 0 @classmethod def will(cls, topic, msg, retain=False, qos=0): cls.lw_topic = topic cls.lw_msg = msg cls.lw_retain = retain cls.lw_qos = qos status_msgs = ('connected to broker', 'awaiting broker', 'awaiting default network', 'awaiting specified network', 'publish OK', 'running', 'unk', 'Will registered', 'Fail to connect to broker', 'WiFi up', 'WiFi down') def __init__(self, d): self.user_start = (d['user_start'], d['args']) self.init_str = buildinit(d) # Synchroniser idle until started. self.rtc_synchroniser = RTCsynchroniser(self, d['rtc_resync'], d['local_time_offset']) self.keepalive = d['keepalive'] self._running = False self.verbose = d['verbose'] # Watchdog timeout for ESP8266 (ms). wdog = d['timeout'] * 1000 # SynCom string mode self.channel = SynCom(False, d['sckin'], d['sckout'], d['srx'], d['stx'], d['reset'], wdog, True, self.verbose) loop = asyncio.get_event_loop() loop.create_task(heartbeat()) # Start the SynCom instance. This will run self.start(). If an error # occurs self.quit() is called which cancels all NamedTask instances # and returns to Syncom's start() method. This waits on Cancellable.cancel_all() # before forcing a failure on the ESP8266 and re-running self.start() # to perform a clean restart. loop.create_task(self.channel.start(self.start, Killer())) self.subs = {} # Callbacks indexed by topic self.pubs = [] self._pub_free = True # No publication in progress self.s_han = default_status_handler self.wifi_han = (lambda *_ : None, ()) self._wifi_up = False # Only specify network on first run self.first_run = True # API def publish(self, topic, msg, retain=False, qos=0): qos_check(qos) validate(topic, 'topic') # Raise ValueError if invalid. validate(msg, 'message') self.pubs.append((topic, msg, 1 if retain else 0, qos)) def pubq_len(self): return len(self.pubs) def subscribe(self, topic, qos, callback, *args): qos_check(qos) self.subs[topic] = (callback, args) self.channel.send(argformat(SUBSCRIBE, topic, qos)) # Command handled directly by mqtt.py on ESP8266 e.g. MEM def command(self, *argsend): self.channel.send(argformat(*argsend)) # Is Pyboard RTC synchronised? def rtc_syn(self): return self.rtc_synchroniser._rtc_syn() def status_handler(self, coro): self.s_han = coro def wifi_handler(self, cb, *args): self.wifi_han = (cb, args) def running(self): return self._running def wifi(self): return self._wifi_up # API END # Convenience method allows return self.quit() on error def quit(self, *args): if args is not (): self.vbprint(*args) self._running = False # Note that if this method is called from self.start return is to # SynCom's start method which will wait on the Killer instance to # cancel all Cancellable tasks # On publication of a qos 1 message this is called with False. This prevents # further pubs until the response is received, ensuring we wait for the PUBACK # that corresponds to the last publication. # This delay can be arbitrarily long if WiFi connectivity has been lost. def pub_free(self, val = None): if val is not None: self._pub_free = val return self._pub_free def vbprint(self, *args): if self.verbose: print(*args) def get_cmd(self, istr): ilst = istr.split(SEP) return ilst[0], ilst[1:] def do_status(self, action, last_status): try: iact = int(action[0]) except ValueError: # Gibberish from ESP8266: caller reboots return UNKNOWN if iact == PUBOK: self.pub_free(True) # Unlock so we can do another pub. elif iact == RUNNING: self._running = True # Detect WiFi status changes. Ignore initialisation and repeats if last_status != -1 and iact != last_status and iact in (WIFI_UP, WIFI_DOWN): self._wifi_up = iact == WIFI_UP cb, args = self.wifi_han cb(self._wifi_up, *args) if self.verbose: if iact != UNKNOWN: if iact != last_status: # Ignore repeats printtime() print('Status: ', self.status_msgs[iact]) else: printtime() print('Unknown status: {} {}'.format(action[1], action[2])) return iact # PUBLISH @asyn.cancellable async def _publish(self): while True: if len(self.pubs): args = self.pubs.pop(0) secs = 0 # pub free can be a long time coming if WiFi is down while not self._pub_free: # If pubs are queued, wait 1 sec to respect ESP8266 buffers await asyncio.sleep(1) if self._wifi_up: secs += 1 if secs > 60: self.quit('No response from ESP8266') break else: self.channel.send(argformat(PUBLISH, *args)) # All pubs send PUBOK. # No more pubs allowed until PUBOK received. self.pub_free(False) else: await asyncio.sleep_ms(20) # start() is run each time the channel acquires sync i.e. on startup and also # after an ESP8266 crash and reset. # Behaviour after fatal error with ESP8266: # It sets _running False to cause local tasks to terminate, then returns. # A return causes the local channel instance to wait on the exit_gate to cause # tasks in this program terminate, then issues a hardware reset to the ESP8266. # (See SynCom.start() method). After acquiring sync, start() gets rescheduled. async def start(self, channel): self.vbprint('Starting...') self.subs = {} # Callbacks indexed by topic self._pub_free = True # No publication in progress self._running = False if self.lw_topic is not None: channel.send(argformat(WILL, self.lw_topic, self.lw_msg, self.lw_retain, self.lw_qos)) res = await channel.await_obj() if res is None: # SynCom timeout await self.s_han(self, ESP_FAIL) return self.quit('ESP8266 fail 1. Resetting.') command, action = self.get_cmd(res) if command == STATUS: iact = self.do_status(action, -1) await self.s_han(self, iact) if iact in _BAD_STATUS: return self.quit('Bad status: {}. Resetting.'.format(iact)) else: self.vbprint('Expected will OK got: ', command, action) channel.send(self.init_str) while not self._running: # Until RUNNING status received res = await channel.await_obj() if res is None: await self.s_han(self, ESP_FAIL) return self.quit('ESP8266 fail 2. Resetting.') command, action = self.get_cmd(res) if command == STATUS: iact = self.do_status(action, -1) result = await self.s_han(self, iact) if iact == SPECNET: if result == 1: channel.send('1') # Any string will do else: return self.quit() if iact in _BAD_STATUS: return self.quit('Bad status. Resetting.') else: self.vbprint('Init got: ', command, action) self.vbprint('About to run user program.') if self.user_start is not None: self.user_start[0](self, *self.user_start[1]) # User start function loop = asyncio.get_event_loop() loop.create_task(asyn.Cancellable(self._publish)()) self.rtc_synchroniser._start() # Run coro if synchronisation is required. cb, args = self.wifi_han cb(True, *args) # Initialisation is complete. Process messages from ESP8266. iact = -1 # Invalidate last status for change detection while True: # print incoming messages, handle subscriptions chan_state = channel.any() if chan_state is None: # SynCom Timeout self._running = False elif chan_state > 0: res = await channel.await_obj() command, action = self.get_cmd(res) if command == SUBSCRIPTION: if action[0] in self.subs: # topic found cb, args = self.subs[action[0]] action += args cb(*action) # Run the callback elif command == STATUS: # 1st arg of status is an integer iact = self.do_status(action, iact) # Update pub q and wifi status await self.s_han(self, iact) if iact in _DIRE_STATUS: return self.quit('Fatal status. Resetting.') elif command == TIME: self.rtc_synchroniser._do_time(action) elif command == MEM: # Wouldn't ask for this if we didn't want a printout print('ESP8266 RAM free: {} allocated: {}'.format(action[0], action[1])) else: await self.s_han(self, UNKNOWN) return self.quit('Got unhandled command, resetting ESP8266:', command, action) # ESP8266 has failed await asyncio.sleep_ms(20) if not self._running: # self.quit() has been called. await self.s_han(self, NO_NET) return self.quit('Not running, resetting ESP8266')