From 5e30d4ec707080d3776d2e3cc3d65acdd424f442 Mon Sep 17 00:00:00 2001 From: Garth Kidd Date: Wed, 27 Jan 2021 08:06:27 +1100 Subject: [PATCH 1/4] Add plugin system with auto-shake demo. Based on work by `@enderboi`. Main differences so far: * `plugins/README.md` * The plugin system always runs at boot * Individual plugins can be disabled * Plugins can be directory modules as well as single files I've kept the `plugins.*` namespace empty of anything except its `initialise` function and any imported plugins. I'm using `plugins.initialise` to match the rest of the framework. --- configuration/mqtt.py | 2 +- main.py | 3 +++ plugins/README.md | 47 +++++++++++++++++++++++++++++++++++++++ plugins/__init__.py | 23 +++++++++++++++++++ plugins/auto_shake.py | 28 +++++++++++++++++++++++ plugins/test2/__init__.py | 5 +++++ scripts/aiko.mpf | 7 ++++++ scripts/release.sh | 3 +++ 8 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 plugins/README.md create mode 100644 plugins/__init__.py create mode 100644 plugins/auto_shake.py create mode 100644 plugins/test2/__init__.py diff --git a/configuration/mqtt.py b/configuration/mqtt.py index fd3460c..db91af6 100644 --- a/configuration/mqtt.py +++ b/configuration/mqtt.py @@ -13,7 +13,7 @@ "topic_path": "$me", # "topic_subscribe": [ "$me/in", "$me/exec", upgrade_topic ], # "topic_subscribe": [ "$me/in", "$me/exec", upgrade_topic, "$all/log" ], - "topic_subscribe": [ "$me/in", "$me/exec", lca_schedule_topic, upgrade_topic ], + "topic_subscribe": [ "$me/in", "$me/exec", lca_schedule_topic, upgrade_topic, 'public/+/0/out' ], "lca_schedule_topic": lca_schedule_topic, "upgrade_topic": upgrade_topic, diff --git a/main.py b/main.py index db7c1c9..ae69cc3 100644 --- a/main.py +++ b/main.py @@ -45,4 +45,7 @@ application = __import__(application_name) application.initialise() +import plugins +plugins.initialise() + aiko.event.loop_thread() diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 0000000..69dd75c --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,47 @@ +# Plugins + +## What's a plugin? + +**Plugins aren't the same as applications.** In particular: we only want +to be running one application at a time. To get a little RFC2119, the +application MAY assume it has sole control of what's shown on the OLED +displays, and MAY assume any touch sensor or push buttons are for it to +handle. Eventually, their `initialise` function might get a matching +`uninitialise`, or we might get _very_ clever and remove their handlers +automatically. + +Plugins, on the other hand, MUST co-operate with each other, MUST NOT +assume they control the displays or inputs, and SHOULD work together. +We'll eventually run into conflicts we can't resolve without being +able to disable specific plugins, but let's see how long we can last. + +## How do I enable and disable plugins? + +Set the parameter `plugin_xxx_disabled` to stop your badge automatically +calling `plugins.xxx.initialise()` at startup. You can set that in +`configuration/main.py`, and check it with the code: + +```python +import configuration +configuration.main.parameter("plugins_enabled") +``` + +## How do I add plugins? + +Add Python modules to the `plugins` directory. They MUST contain an +`initialise` function. They MUST NOT be named `initialise`, as that +would conflict with the plugin system's initialisation code. + +You'll be able to see your plugins at the REPL: + +```plain +MicroPython v1.13 on 2020-09-02; ESP32 module with ESP32 +Type "help()" for more information. +>>> import plugins +>>> dir(plugins) +['__class__', '__name__', '__file__', '__path__', 'initialise', 'auto_shake', 'test2'] +>>> plugins.auto_shake.messages +[('public/esp32_10521c5de548/0/out', '(boot v05 swagbadge)')] +>>> plugins.test2.tested +True +``` diff --git a/plugins/__init__.py b/plugins/__init__.py new file mode 100644 index 0000000..aeebf5a --- /dev/null +++ b/plugins/__init__.py @@ -0,0 +1,23 @@ +def initialise(): + from aiko.common import log + from configuration.main import parameter + import os, sys + + for basename in os.listdir(__path__): + pathname = "/".join([__path__, basename]) + if pathname == __file__: + continue + s_ifmt = os.stat(pathname)[0] & 61440 + if basename[-3:] == ".py" and s_ifmt == 32768: + submodule = basename[:-3] + elif s_ifmt == 16384: + submodule = basename + assert submodule not in locals() + if parameter("plugin_{}_disabled".format(submodule)): + continue + try: + module = ".".join([__name__, submodule]) + __import__(module, globals(), locals(), [module]).initialise() + log("plugin: {}".format(submodule)) + except Exception as err: + sys.print_exception(err, sys.stderr) diff --git a/plugins/auto_shake.py b/plugins/auto_shake.py new file mode 100644 index 0000000..62c89a2 --- /dev/null +++ b/plugins/auto_shake.py @@ -0,0 +1,28 @@ +# Automatically shakes newly woken badges using @enderboi's protocol + +import aiko, binascii, machine, sys, time + +my_shake_source = binascii.hexlify(machine.unique_id()).decode('ascii') +my_reply_topic = aiko.mqtt.get_topic_path("public") + "/in" +messages = [] + +def on_message(topic, payload_in): + messages.append((topic, payload_in)) + if len(messages) > 5: + messages.pop(0) + if payload_in.startswith("(boot ") and payload_in.endswith(" swagbadge)"): + # TODO use aiko.event instead + time.sleep_ms(500) + hostname = topic.split('/')[1] + reply_topic = "/".join(["public", hostname, "0", "in"]) + if reply_topic != my_reply_topic: + aiko.mqtt.client.publish(reply_topic, "(shake 230 {})".format(my_shake_source)) + aiko.mqtt.client.publish(my_reply_topic, "(oled:log shook {})".format(hostname)) + return False + +def initialise(): + print("plugins.test.initialise()...") + # TODO broadcast message to indicate badge can be shook + # TODO shake only badges that broadcast that message + aiko.mqtt.add_message_handler(on_message, "$all/out") + print("plugins.test.initialise() done") diff --git a/plugins/test2/__init__.py b/plugins/test2/__init__.py new file mode 100644 index 0000000..44c7e80 --- /dev/null +++ b/plugins/test2/__init__.py @@ -0,0 +1,5 @@ +tested = False +def initialise(): + print("testing 2") + global tested + tested = True diff --git a/scripts/aiko.mpf b/scripts/aiko.mpf index d8dd4df..216e9a9 100644 --- a/scripts/aiko.mpf +++ b/scripts/aiko.mpf @@ -7,6 +7,8 @@ md examples md lib md lib/aiko # md lib/umqtt +md plugins +# md plugins/test2 exec print("### Copy applications/*.py ###") # put applications/nodebots.py @@ -17,6 +19,11 @@ put applications/default.py put applications/schedule/schedule.py put applications/swagbadge.py +exec print("### Copy plugins/*.py ###") +put plugins/__init__.py +# put plugins/auto_shake.py +# put plugins/test2/__init__.py + exec print("### Copy configuration/*.py ###") put configuration/keys.db put configuration/led.py diff --git a/scripts/release.sh b/scripts/release.sh index 46442c9..ed111bc 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -17,6 +17,7 @@ mkdir $RELEASE_PATHNAME/configuration mkdir $RELEASE_PATHNAME/examples mkdir $RELEASE_PATHNAME/lib mkdir $RELEASE_PATHNAME/lib/aiko +mkdir $RELEASE_PATHNAME/plugins cp applications/default.py $RELEASE_PATHNAME/applications cp applications/schedule/schedule.py $RELEASE_PATHNAME/applications/schedule @@ -51,6 +52,8 @@ cp lib/shutil.py $RELEASE_PATHNAME/lib cp lib/ssd1306.py $RELEASE_PATHNAME/lib cp lib/threading.py $RELEASE_PATHNAME/lib +cp plugins/__init__.py $RELEASE_PATHNAME/plugins + cp main.py $RELEASE_PATHNAME find $RELEASE_PATHNAME -type f \( -exec md5sum {} \; -exec wc -c {} \; \) | paste - - | column -t | tr -s "[:blank:]" | cut -d" " -f1,3,4 | sort -k 3 >$MANIFEST From deeeaabef7580b0d6dbf59bf130ac7b892e3533a Mon Sep 17 00:00:00 2001 From: Garth Kidd Date: Sat, 30 Jan 2021 11:08:41 +1100 Subject: [PATCH 2/4] Flash some colours across the LED strip on boot --- plugins/flash_leds_on_boot.py | 43 +++++++++++++++++++++++++++++++++++ scripts/aiko.mpf | 1 + 2 files changed, 44 insertions(+) create mode 100644 plugins/flash_leds_on_boot.py diff --git a/plugins/flash_leds_on_boot.py b/plugins/flash_leds_on_boot.py new file mode 100644 index 0000000..0085273 --- /dev/null +++ b/plugins/flash_leds_on_boot.py @@ -0,0 +1,43 @@ +# Flash configured LEDs on boot. + +import aiko.led +import time +import binascii + +DURATION_MS = 100 +COLOURS = ["e40303", "ff8c00", "ffed00", "008026", "004dff", "750787"] +GAMMAS = [v for v in binascii.a2b_base64(b"".join([ + # Overkill, but I wanted the orange to look orange. + # https://learn.adafruit.com/led-tricks-gamma-correction/the-quick-fix + b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQECAgICAgICAgMDA", + b"wMDAwMEBAQEBAUFBQUGBgYGBwcHBwgICAkJCQoKCgsLCwwMDQ0NDg4PDxAQERESEhMTFB", + b"QVFRYWFxgYGRkaGxscHR0eHyAgISIjIyQlJicnKCkqKywtLi8wMTIyMzQ2Nzg5Ojs8PT4", + b"/QEJDREVGSElKS01OT1FSU1VWV1laXF1fYGJjZWZoaWttbnByc3V3eHp8fn+Bg4WHiYqM", + b"jpCSlJaYmpyeoKKkp6mrra+xtLa4ur2/wcTGyMvN0NLV19rc3+Hk5+ns7/H09/n8/w==" +]))] + +def gammify(colour): + return tuple(GAMMAS[v] for v in colour) + +def h2c(hex): + return tuple(v for v in binascii.unhexlify(hex)) + +def initialise(): + colours = [aiko.led.black] + [gammify(h2c(hex)) for hex in COLOURS] + [aiko.led.black] + ncolours = len(colours) + npixels = aiko.led.np.n + delay = max(1, DURATION_MS // (ncolours + npixels)) + + aiko.led.fill(aiko.led.black) + aiko.led.np.write() + + for offset in range(0 - ncolours, npixels): + for index in range(ncolours): + pixel = offset + index + if 0 <= pixel < npixels: + aiko.led.np[pixel] = colours[index] + aiko.led.np.write() + time.sleep_ms(delay) + + aiko.led.fill(aiko.led.black) + aiko.led.np.write() diff --git a/scripts/aiko.mpf b/scripts/aiko.mpf index 216e9a9..7761273 100644 --- a/scripts/aiko.mpf +++ b/scripts/aiko.mpf @@ -22,6 +22,7 @@ put applications/swagbadge.py exec print("### Copy plugins/*.py ###") put plugins/__init__.py # put plugins/auto_shake.py +# put plugins/flash_leds_on_boot.py # put plugins/test2/__init__.py exec print("### Copy configuration/*.py ###") From c41065191232c73b0edf510bf02723788084ef41 Mon Sep 17 00:00:00 2001 From: Garth Kidd Date: Sat, 30 Jan 2021 10:30:28 +1100 Subject: [PATCH 3/4] Remove noisy debug log --- plugins/auto_shake.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/auto_shake.py b/plugins/auto_shake.py index 62c89a2..c167e0c 100644 --- a/plugins/auto_shake.py +++ b/plugins/auto_shake.py @@ -21,8 +21,6 @@ def on_message(topic, payload_in): return False def initialise(): - print("plugins.test.initialise()...") # TODO broadcast message to indicate badge can be shook # TODO shake only badges that broadcast that message aiko.mqtt.add_message_handler(on_message, "$all/out") - print("plugins.test.initialise() done") From a51f4c35da9637c03b8cc1422544e8f367ef8461 Mon Sep 17 00:00:00 2001 From: Garth Kidd Date: Sun, 31 Jan 2021 16:03:29 +1100 Subject: [PATCH 4/4] Optimise the LED flashing performance. TL;DR: assign bytes directly into `pixel.buf`, then call `pixel.write()`. --- plugins/flash_leds_on_boot.py | 90 +++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 36 deletions(-) diff --git a/plugins/flash_leds_on_boot.py b/plugins/flash_leds_on_boot.py index 0085273..72258f9 100644 --- a/plugins/flash_leds_on_boot.py +++ b/plugins/flash_leds_on_boot.py @@ -1,43 +1,61 @@ # Flash configured LEDs on boot. +# Tested with 235X pixels on SAO_1, data to IO19, and configuration.led.settings = +# {'zigzag': False, 'dimension': (235,), 'apa106': False, 'neopixel_pin': 19} import aiko.led -import time -import binascii - -DURATION_MS = 100 -COLOURS = ["e40303", "ff8c00", "ffed00", "008026", "004dff", "750787"] -GAMMAS = [v for v in binascii.a2b_base64(b"".join([ - # Overkill, but I wanted the orange to look orange. - # https://learn.adafruit.com/led-tricks-gamma-correction/the-quick-fix - b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQECAgICAgICAgMDA", - b"wMDAwMEBAQEBAUFBQUGBgYGBwcHBwgICAkJCQoKCgsLCwwMDQ0NDg4PDxAQERESEhMTFB", - b"QVFRYWFxgYGRkaGxscHR0eHyAgISIjIyQlJicnKCkqKywtLi8wMTIyMzQ2Nzg5Ojs8PT4", - b"/QEJDREVGSElKS01OT1FSU1VWV1laXF1fYGJjZWZoaWttbnByc3V3eHp8fn+Bg4WHiYqM", - b"jpCSlJaYmpyeoKKkp6mrra+xtLa4ur2/wcTGyMvN0NLV19rc3+Hk5+ns7/H09/n8/w==" -]))] - -def gammify(colour): - return tuple(GAMMAS[v] for v in colour) +from binascii import unhexlify, b2a_base64 +from gc import collect +from time import sleep_us, ticks_us, ticks_diff + +DURATION_MS = 250 +COLOURS = ["000000", "e40303", "ff8c00", "ffed00", "008026", "004dff", "750787", "000000"] +COMPENSATION_MEASUREMENTS = 5 def h2c(hex): - return tuple(v for v in binascii.unhexlify(hex)) + return tuple(v for v in unhexlify(hex)) + +def measure_write_time_us(write): + collect() + before_us = ticks_us() + for _ in range(0, COMPENSATION_MEASUREMENTS): write() + return ticks_diff(ticks_us(), before_us) // COMPENSATION_MEASUREMENTS + +def noop(): pass + +def swipe(buf, cbuf, step, callback=noop): + lcbuf = len(cbuf) + lbuf = len(buf) + for head in range(0 - lcbuf, lbuf, step): + lt = 0 - min(0, head) + rt = min(0, lbuf - (head + lcbuf)) + w = lcbuf - lt + rt + buf[head + lt : head + lt + w] = cbuf[lt : lcbuf + rt] + callback() + +def make_cbuf(hex_colours, colour_width=1): + ncolours = len(hex_colours) + colours = [aiko.led.apply_dim(h2c(hex)) for hex in hex_colours] + cbuf = bytearray(3 * ncolours * colour_width) + for c in range(ncolours): + colour = bytearray(colours[c]) + for s in range(colour_width): + offset = (c * colour_width + s) * 3 + cbuf[offset:offset+3] = colour + return memoryview(cbuf) def initialise(): - colours = [aiko.led.black] + [gammify(h2c(hex)) for hex in COLOURS] + [aiko.led.black] - ncolours = len(colours) - npixels = aiko.led.np.n - delay = max(1, DURATION_MS // (ncolours + npixels)) - - aiko.led.fill(aiko.led.black) - aiko.led.np.write() - - for offset in range(0 - ncolours, npixels): - for index in range(ncolours): - pixel = offset + index - if 0 <= pixel < npixels: - aiko.led.np[pixel] = colours[index] - aiko.led.np.write() - time.sleep_ms(delay) - - aiko.led.fill(aiko.led.black) - aiko.led.np.write() + ncolours = len(COLOURS) + pixel = aiko.led.np + write = aiko.led.np.write + write_time_us = measure_write_time_us(write) + colour_width = 1 + while write_time_us * (ncolours * colour_width + pixel.n) // colour_width > DURATION_MS * 1000: + colour_width += 1 + cbuf = make_cbuf(COLOURS, colour_width) + expected_steps = (len(cbuf) + len(pixel.buf) + 3) // (colour_width * 3) + delay_us = max(0, DURATION_MS * 1000 // expected_steps - write_time_us) + def callback(): + write() + sleep_us(delay_us) + collect() + swipe(pixel.buf, cbuf, colour_width * 3, callback)