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..c167e0c --- /dev/null +++ b/plugins/auto_shake.py @@ -0,0 +1,26 @@ +# 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(): + # 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") diff --git a/plugins/flash_leds_on_boot.py b/plugins/flash_leds_on_boot.py new file mode 100644 index 0000000..72258f9 --- /dev/null +++ b/plugins/flash_leds_on_boot.py @@ -0,0 +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 +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 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(): + 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) 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..7761273 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,12 @@ 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/flash_leds_on_boot.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