Initial version of app loader
This commit is contained in:
		
						commit
						a03a46eddb
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | *.mpy | ||||||
							
								
								
									
										235
									
								
								src/app_loader.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								src/app_loader.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,235 @@ | |||||||
|  | import uasyncio | ||||||
|  | import settings | ||||||
|  | from umqtt.robust import MQTTClient | ||||||
|  | import network | ||||||
|  | import machine | ||||||
|  | import gc | ||||||
|  | import os | ||||||
|  | import utime | ||||||
|  | 
 | ||||||
|  | def String(x): | ||||||
|  |     if isinstance(x, bytes): | ||||||
|  |         return x.decode() | ||||||
|  |     return str(x) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def raw(x): | ||||||
|  |     return x | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def replace_file(path): | ||||||
|  |     def do_replace(content): | ||||||
|  |         with open(path, 'wb') as f: | ||||||
|  |             length = f.write(content) | ||||||
|  |         return length | ||||||
|  |     return do_replace | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class MqttHandler(object): | ||||||
|  | 
 | ||||||
|  |     class Subscription(object): | ||||||
|  |         def __init__(self, handler, topic, cls, cb=None): | ||||||
|  |             self.topic = topic | ||||||
|  |             self.value = None | ||||||
|  |             self.cls = cls | ||||||
|  |             self.cb = cb | ||||||
|  |             self.handler = handler | ||||||
|  | 
 | ||||||
|  |         async def set_value(self, msg): | ||||||
|  |             val = self.cls(msg) | ||||||
|  |             if self.cb is None: | ||||||
|  |                 self.value = val | ||||||
|  |             else: | ||||||
|  |                 self.cb(val) | ||||||
|  | 
 | ||||||
|  |         async def wait_for_value(self, interval_ms=10): | ||||||
|  |             while self.value is None: | ||||||
|  |                 await uasyncio.sleep_ms(interval_ms) | ||||||
|  |             return self.value | ||||||
|  | 
 | ||||||
|  |         def unsubscribe(self): | ||||||
|  |             self.handler.subscriptions.pop(self.topic) | ||||||
|  | 
 | ||||||
|  |     def __init__(self, loop, host): | ||||||
|  |         ip = network.WLAN(network.STA_IF).ifconfig()[0] | ||||||
|  |         self.client = MQTTClient(ip, host) | ||||||
|  |         self.client.connect() | ||||||
|  |         self.client.set_callback(self.cb) | ||||||
|  |         self.subscriptions = {} | ||||||
|  |         self.ev_loop = loop | ||||||
|  |         self.ev_loop.create_task(self.loop()) | ||||||
|  |         self.publish = self.client.publish | ||||||
|  | 
 | ||||||
|  |     def cb(self, topic, msg): | ||||||
|  |         if topic in self.subscriptions: | ||||||
|  |             self.ev_loop.create_task(self.subscriptions[topic].set_value(msg)) | ||||||
|  | 
 | ||||||
|  |     def subscribe(self, topic, cls=String, cb=None, local=False): | ||||||
|  |         if local: | ||||||
|  |             topic = self.local_topic(topic) | ||||||
|  | 
 | ||||||
|  |         if not isinstance(topic, bytes): | ||||||
|  |             topic = topic.encode() | ||||||
|  | 
 | ||||||
|  |         if topic in self.subscriptions: | ||||||
|  |             raise Exception('topic {} already subscribed'.format(topic)) | ||||||
|  | 
 | ||||||
|  |         sub = MqttHandler.Subscription(self, topic, cls=cls, cb=cb) | ||||||
|  |         self.subscriptions[topic] = sub | ||||||
|  |         self.client.subscribe(topic) | ||||||
|  |         return sub | ||||||
|  | 
 | ||||||
|  |     def local_topic(self, topic): | ||||||
|  |         return '{}/{}'.format(settings.device_topic, String(topic)) | ||||||
|  | 
 | ||||||
|  |     def register_command(self, cmd, cb, cls=String, local=True): | ||||||
|  |         self.subscribe(cmd, cls=cls, cb=cb, local=local) | ||||||
|  | 
 | ||||||
|  |     async def loop(self, interval_ms=10): | ||||||
|  |         while True: | ||||||
|  |             for i in range(1000/interval_ms): | ||||||
|  |                 self.client.check_msg() | ||||||
|  |                 await uasyncio.sleep_ms(interval_ms) | ||||||
|  |             gc.collect() | ||||||
|  | 
 | ||||||
|  |     def log(self, *args): | ||||||
|  |         if settings.LOG_TOPIC is not None: | ||||||
|  |             log_str = ' '.join([String(a) for a in args]) | ||||||
|  |             self.client.publish(self.local_topic(settings.LOG_TOPIC), log_str) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | async def update_app(mqtt, name): | ||||||
|  |     current_version = None | ||||||
|  |     current_app = None | ||||||
|  |     app_file = 'app.mpy' | ||||||
|  |     try: | ||||||
|  |         gc.collect() | ||||||
|  |         import app | ||||||
|  |         current_app = app.__app__ | ||||||
|  |         current_version = app.__version__ | ||||||
|  |     except: | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     try: | ||||||
|  |         app_ns = '{}/apps/{}/{{}}'.format(settings.MQTT_PREFIX, name) | ||||||
|  |         version_sub = mqtt.subscribe(app_ns.format('version'), | ||||||
|  |                                      cls=int) | ||||||
|  |         latest = await version_sub.wait_for_value() | ||||||
|  |         version_sub.unsubscribe() | ||||||
|  | 
 | ||||||
|  |         if current_app != name or current_version != latest: | ||||||
|  |             mqtt.log('installed: {} v{}, want {} v{}'.format( | ||||||
|  |                 current_app, current_version, name, latest)) | ||||||
|  |             try: | ||||||
|  |                 # if the wrong app is installed, we remove it and reboot to | ||||||
|  |                 # avoid running out of memory during the update | ||||||
|  |                 os.remove('app.mpy') | ||||||
|  |                 return True | ||||||
|  |             except Exception: | ||||||
|  |                 pass | ||||||
|  | 
 | ||||||
|  |             gc.collect() | ||||||
|  |             sub = mqtt.subscribe(app_ns.format('content'), | ||||||
|  |                                  cls=replace_file(app_file)) | ||||||
|  |             app_size = await sub.wait_for_value() | ||||||
|  |             sub.unsubscribe() | ||||||
|  |             mqtt.log('Installed {} v{}, {}b'.format(name, latest, app_size)) | ||||||
|  |             return True | ||||||
|  |     except Exception as e: | ||||||
|  |         mqtt.log('Update error:', e) | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     return False | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def deepsleep(duration_ms): | ||||||
|  |     utime.sleep_ms(200) | ||||||
|  |     # configure RTC.ALARM0 to be able to wake the device | ||||||
|  |     rtc = machine.RTC() | ||||||
|  |     rtc.irq(trigger=rtc.ALARM0, wake=machine.DEEPSLEEP) | ||||||
|  |     # set RTC.ALARM0 to fire after the specified time | ||||||
|  |     rtc.alarm(rtc.ALARM0, duration_ms) | ||||||
|  |     machine.deepsleep() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | async def wait_for_connection(timeout=5): | ||||||
|  |     sta = network.WLAN(network.STA_IF) | ||||||
|  | 
 | ||||||
|  |     while not sta.isconnected(): | ||||||
|  |         await uasyncio.sleep_ms(100) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_device_info(mqtt): | ||||||
|  |     sta = network.WLAN(network.STA_IF) | ||||||
|  |     mac = ':'.join(['{:02x}'.format(x) for x in sta.config('mac')]) | ||||||
|  |     ip = sta.ifconfig()[0] | ||||||
|  |     settings.device_topic = '{}/{}'.format(settings.MQTT_PREFIX, | ||||||
|  |                                            settings.DEVICE_TOPIC_FORMAT.format(ip=ip, mac=mac)) | ||||||
|  |     topic_fmt = '{}/{{}}'.format(settings.device_topic.strip('/')) | ||||||
|  | 
 | ||||||
|  |     dev_name = mqtt.subscribe(topic_fmt.format('config/name')) | ||||||
|  |     dev_app = mqtt.subscribe(topic_fmt.format('config/app')) | ||||||
|  | 
 | ||||||
|  |     return dev_name, dev_app | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | async def main(loop): | ||||||
|  |     try: | ||||||
|  |         await uasyncio.wait_for(wait_for_connection(), | ||||||
|  |                                 settings.NETWORK_TIMEOUT) | ||||||
|  |     except Exception: | ||||||
|  |         raise Exception('Network error') | ||||||
|  | 
 | ||||||
|  |     mqtt = MqttHandler(loop, settings.MQTT_BROKER) | ||||||
|  | 
 | ||||||
|  |     dev_name_sub, app_name_sub = get_device_info(mqtt) | ||||||
|  | 
 | ||||||
|  |     dev_name = await dev_name_sub.wait_for_value() | ||||||
|  |     app_name = await app_name_sub.wait_for_value() | ||||||
|  |     dev_name_sub.unsubscribe() | ||||||
|  |     app_name_sub.unsubscribe() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     mqtt.log('Dev. info:', dev_name, app_name) | ||||||
|  | 
 | ||||||
|  |     # commands must be registered after get_device_info() since the device's | ||||||
|  |     # base topic is not known before. | ||||||
|  |     mqtt.register_command('reboot', lambda x: deepsleep(1000)) | ||||||
|  | 
 | ||||||
|  |     try: | ||||||
|  |         reboot = await uasyncio.wait_for(update_app(mqtt, app_name), 5) | ||||||
|  |         if reboot: | ||||||
|  |             deepsleep(100) | ||||||
|  |     except uasyncio.TimeoutError: | ||||||
|  |         mqtt.log('Error updating app: timeout') | ||||||
|  |     except Exception as e: | ||||||
|  |         mqtt.log('Error updating app:', e) | ||||||
|  | 
 | ||||||
|  |     # if no app is found, wake up every ten seconds to retry the update: | ||||||
|  |     sleep_duration_ms = 10000 | ||||||
|  | 
 | ||||||
|  |     # run app | ||||||
|  |     try: | ||||||
|  |         gc.collect() | ||||||
|  |         import app | ||||||
|  |         gc.collect() | ||||||
|  |         mqtt.log('Heap after import:', gc.mem_free()) | ||||||
|  |         mqtt.publish(mqtt.local_topic('status/app'), app.__app__) | ||||||
|  |         mqtt.publish(mqtt.local_topic('status/version'), str(app.__version__)) | ||||||
|  | 
 | ||||||
|  |         sleep_duration_ms = await app.run(mqtt, name=dev_name) | ||||||
|  |     except Exception as e: | ||||||
|  |         mqtt.log('Error running app:', e) | ||||||
|  | 
 | ||||||
|  |     mqtt.log('Done. Sleeping for {}ms'.format(sleep_duration_ms)) | ||||||
|  | 
 | ||||||
|  |     deepsleep(sleep_duration_ms) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def run(): | ||||||
|  |     try: | ||||||
|  |         loop = uasyncio.get_event_loop() | ||||||
|  |         loop.run_until_complete(main(loop)) | ||||||
|  |     except Exception as e: | ||||||
|  |         print('Error:', e) | ||||||
|  |         deepsleep(1000) | ||||||
							
								
								
									
										4
									
								
								src/main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/main.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | |||||||
|  | #import app_loader | ||||||
|  | 
 | ||||||
|  | import app_loader | ||||||
|  | app_loader.run() | ||||||
							
								
								
									
										16
									
								
								src/settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/settings.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | |||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # timeout for connecting to the WiFi | ||||||
|  | NETWORK_TIMEOUT = 5 | ||||||
|  | 
 | ||||||
|  | # address of MQTT broker | ||||||
|  | MQTT_BROKER = '192.168.1.1' | ||||||
|  | 
 | ||||||
|  | # basic prefix used for all topics | ||||||
|  | MQTT_PREFIX = 'home' | ||||||
|  | 
 | ||||||
|  | # format for device topics | ||||||
|  | DEVICE_TOPIC_FORMAT = 'devices/{ip}' | ||||||
|  | 
 | ||||||
|  | # log topic (None to disable logging via MQTT). relative to device topic | ||||||
|  | LOG_TOPIC = 'log' | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user