Donkey Plugin#148
Open
iliketocode2 wants to merge 35 commits intoinvent-framework:mainfrom
Open
Conversation
Contributor
Author
|
Ok, this implementation feels much cleaner! 🧹 |
Member
|
Improvised API from our call 😉 : from invent.tools import make_helper
# We're starting the donkey in the background with a certain script (containing
# the function "do_stuff") with a certain interpreter and configuration.
#
# Calling the make_helper function kicks off the startup of the web-worker
# with the given configuration and source code. All further communication with
# the helper is via the given channel. The helper sends two types of message
# distinguished by subject:
#
# 1. "result" - the result of a call (see details below)
# 2. "status" - to indicate the helper is ready, broken, killed, whatever.
#
# Furthermore, the helper listens on the channel for messages with the subject
# "run", which define a function to call and args/kwargs to pass in. See below
# for details.
make_helper(
src="something.py",
interpreter="py",
config={...},
channel="my_helper"
)
def handle_click(msg):
invent.publish(
Message(
subject = "run",
function="do_stuff",
args = [
1, 2, 3
]
kwargs={
a=4,
b=5,
}
)
)
# At this point the helper swings into action. It will publish the return
# value to `my_helper` with the subject "result".
def handle_raw_result(msg):
"""
Message from the helper looks like this:
msg.function (the name of the function that was called)
msg.result (the raw returned value from the helper)
msg.error (if there was a problem, describe it here)
"""
if msg.function == "do_stuff":
# Do stuff
if msg.error:
# Something went wrong.
... # handle error condition.
else:
invent.datastore["results"] = msg.result # or perhaps post-process the result derived from msg.
invent.subscribe(handle_click, "make_stuff", "click")
invent.subscribe(handle_raw_result, "my_helper", "result")
app = invent.App(
...
children = [
Chart(
... # we're showing a bar chart
data=from_datastore("results"), # The data in the bar chart is always what's at "results" in the datastore.
),
Button(label="Click me", channel="make_stuff") # Pressing this kicks things off.
]
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Here, I've explored making the donkey more of a plugin which different components can easily use. It has been implemented for the chart, webcam and codeeditor widgets. I have created "test pages" for the codeeditor and the chart widgets. Claude and I spent a while discussing/organizing an architecture for the donkey plugin itself, so while that is more defined, the simple /tests folder with the aforementioned test pages can be moved around.
This PR also relates to the idea of interactive test pages for components such as BLE and webserial as I have built in some "assertions" that show whether or not the donkey has run as expected. Similar logic could be applied to features that require developers to allow BLE connections for example.
The following spec (thanks Claude!) describes the donkey plugin implementation:
Donkey Plugin: Webcam / OpenCV Adapter
Summary
This PR completes the migration of OpenCV/webcam support into the donkey plugin architecture.
OpenCVDonkeyandcreate_opencv_donkeyhave been removed. OpenCV now runs throughWebcamDonkeyAdapter, which is a first-classWidgetDonkeyAdaptersubclass alongside the existingChartDonkeyAdapterandCodeEditorDonkeyAdapter.A test page for the webcam adapter has been added under
tests/webcam/, following the same pattern as the chart and codeeditor test pages.File overview
device.py:
DonkeyConnection), datastore status/result wiring, and widget adapters (ChartDonkeyAdapter,CodeEditorDonkeyAdapter,WebcamDonkeyAdapter).donkey_plugin.py:
DonkeyPluginFlow,make_plugin_runner, andmake_assertion_callbacksreduce repetitive main.py code for start/run/error handling.test_helpers.py:
pass_html,fail_html,wait_html, andStatusProxyfor assertion/status display and channel publishing.tests/
make_plugin_runnerandmake_assertion_callbacksfor worker lifecycle and assertion displayRemoved
OpenCVDonkey— superseded byWebcamDonkeyAdapter.create_opencv_donkey— superseded byWebcamDonkeyAdapter.initialize().Architecture Notes
WebcamDonkeyAdapterfollows theWidgetDonkeyAdaptercontract with one intentional deviation: it overridesrun()rather than routing throughDonkeyConnection.run_code(). This is because the OpenCV worker callsworker_run_user_code(code, data_url)directly rather thaninvent_run_code(code, context_json). The deviation is contained inside the adapter and is invisible to callers._context()returns{}inWebcamDonkeyAdapter. It satisfies the base class interface but is not used, becauserun()builds the worker expression itself. This is a known stub; it would only become meaningful if the OpenCV worker were later refactored to accept context viainvent_run_code.Donkey Plugin Spec (v1)
Purpose
Define a standard way to attach Python worker logic ("donkey plugins") to widgets so Invent apps can compose features like LEGO bricks.
This spec captures both the target model and the current implementation.
Core Decisions
Strict output contract
Plugin task output is strict and must be an object:
{"ok": true, "result": <object>}{"ok": false, "error": "<message>"}Worker ownership
Each plugin instance owns its own donkey worker. Plugins do not share workers by default.
Trigger model
Execution is triggered by explicit button press only in v1.
Status visibility
Every plugin publishes worker status to datastore.
Interpreter and packages
Worker type is
pyonly. Packages are framework-managed, not user-managed.Runtime Archetype
Runtime implementation lives in
invent.tools.deviceviaDonkeyConnection.Required API:
initialize()execute(code)evaluate(expression)run_code(code, result_key, context=None)kill()readypropertyThe runtime is widget-agnostic and must not include widget-specific logic.
Datastore Contract
Each plugin defines two keys:
status_key: worker lifecycle and run-time statusresult_key: latest task result payloadStatus Values
Use framework constants:
_DEVICE_DONKEY_CREATING_DEVICE_DONKEY_BUSY_DEVICE_DONKEY_READY_DEVICE_DONKEY_KILLED_DEVICE_DONKEY_ERROR: <details>Result Values
result_keymust always receive an object:{"ok": true, "result": {...}}{"ok": false, "error": "..."}Adapter Archetype
Adapter base implementation lives in
invent.tools.deviceviaWidgetDonkeyAdapter.Subclass responsibilities:
_context()returns JSON-safe context dict_apply_result(payload)validates payload and applies widget updatesCurrent adapters:
ChartDonkeyAdapterCodeEditorDonkeyAdapterWebcamDonkeyAdapterTask Contract
Runtime task execution inputs:
code: Python code string to executecontext: JSON-safe object available to executed coderesult_key: datastore key for strict result objectExecuted code rules:
resultresultmust be JSON-safe and adapter-compatiblePage Workflow Helpers
To keep user-facing
main.pyfiles small, workflow helpers live ininvent.tools.donkey_plugin.Implemented helpers:
DonkeyPluginFlowmake_plugin_runner(...)ensure_workerandrun_codecallablesmake_assertion_callbacks(...)These helpers are optional but recommended for user-facing pages.
Example: Chart Plugin
Chart adapter context:
{ "chart_type": chart.chart_type, "data": chart.data, "options": chart.options, }Expected user code:
Apply rules:
result["data"]exists, updatechart.dataresult["options"]exists, updatechart.optionsExample: CodeEditor Plugin
CodeEditor adapter context:
{ "editor_code": source_editor.code, }Expected user code:
Apply rules:
result["output"]is written to output widget.textExample: Webcam Plugin
The webcam adapter does not use a context dict.
run()retrieves the latest photo capture from the widget directly and passes it to the OpenCV worker alongside the user code.User code has access to:
Expected user code:
Apply rules:
result_image(orresult) must be a numpy ndarraywebcam.show_image(data_url)Acceptance Criteria
A donkey plugin is compliant if it:
Since this PR is on top of the webcam one, here is a brief summary paragraph of the changes to the webcam:
The webcam widget now supports a
photo_outputproperty controlling whether captured photos are downloaded, shown in a preview panel, or both. Apreview_layoutproperty allows the live video feed and capture preview to be displayed stacked or side-by-side. Captures are stored internally via a capture history, which exposescaptures()andlatest_capture()for querying by media type. This is what allows the donkey adapter to retrieve the most recent frame for OpenCV processing. Ashow_image()method allows any external data URL to be displayed in the preview panel, replacing its current content. Thephoto_capturedevent now carries a capture argument containing the photo metadata, including its data URL, timestamp, and ID.