@@ -202,7 +202,7 @@ on the robustness the developer is expecting in their application:
202202 class Picoscope6000(Thing):
203203
204204 @action()
205- def set_channel_pydantic (
205+ def set_channel (
206206 self,
207207 channel: Literal["A", "B", "C", "D"],
208208 enabled: bool = True,
@@ -229,6 +229,10 @@ on the robustness the developer is expecting in their application:
229229
230230 ???+ note "JSON schema seen in Thing Description"
231231
232+ ```py
233+ Picoscope6000.set_channel.to_affordance().json()
234+ ```
235+
232236 ```json
233237 {
234238 "description": "Set the parameter for a channel. https://www.picotech.com/download/manuals/picoscope-6000-series-a-api-programmers-guide.pdf",
@@ -278,13 +282,54 @@ on the robustness the developer is expecting in their application:
278282 }
279283 ```
280284
285+ === "Return Type"
286+
287+ ```py
288+ from typing import Annotated
289+ from pydantic import Field
290+
291+ class SerialUtility(Thing):
292+
293+ @action()
294+ def execute_instruction(
295+ self, command: str, return_data_size: Annotated[int, Field(ge=0)] = 0
296+ ) -> str:
297+ """
298+ executes instruction given by the ASCII string parameter 'command'. If return data size is greater than 0, it reads the response and returns the response. Return Data Size - in bytes - 1 ASCII character = 1 Byte.
299+ """
300+ ```
301+
302+ ???+ note "JSON schema seen in Thing Description"
303+
304+ ```py
305+ SerialUtility.execute_instruction.to_affordance().json()
306+ ```
307+
308+ ```json
309+ {
310+ "description": "executes instruction given by the ASCII string parameter 'command'. If return data size is greater than 0, it reads the response and returns the response. Return Data Size - in bytes - 1 ASCII character = 1 Byte.",
311+ "input": {
312+ "properties": {
313+ "command": {"type": "string"},
314+ "return_data_size": {"default": 0, "minimum": 0, "type": "integer"}
315+ },
316+ "required": ["command"],
317+ "type": "object"
318+ },
319+ "output": {"type": "string"},
320+ "synchronous": True
321+ }
322+ ```
323+
281324However, a schema is optional and it only matters that
282- the method signature is matching when requested from a client. To enable this, set global attribute ` allow_relaxed_schema_actions=True ` . This setting is used especially when a schema is useful for validation of arguments but not available - not for methods with no arguments.
325+ the method signature is matching when requested from a client.
326+
327+ <!-- To enable this, set global attribute`allow_relaxed_schema_actions=True`. This setting is used especially when a schema is useful for validation of arguments but not available - not for methods with no arguments.
283328
284329```py title="Relaxed or Unavailable Schema for Actions" linenums="1"
285330--8<-- "docs/beginners-guide/code/thing_example_2.py:168:172"
286331--8<-- "docs/beginners-guide/code/thing_example_2.py:558:559"
287- ```
332+ ``` -->
288333
289334It is always possible to custom validate the arguments after invoking the action:
290335
@@ -293,7 +338,7 @@ It is always possible to custom validate the arguments after invoking the action
293338-- 8 < -- " docs/beginners-guide/code/actions/parameterized_function.py:9:27"
294339```
295340
296- The last and least preferred possibility is to use ` ParameterizedFunction ` :
341+ <!-- The last and least preferred possibility is to use `ParameterizedFunction`:
297342
298343```py title="Parameterized Function" linenums="1"
299344--8<-- "docs/beginners-guide/code/actions/parameterized_function.py:52:"
@@ -317,8 +362,89 @@ client side, there is no difference between invoking a normal action and an acti
317362
318363 ```py title="Custom Validation" linenums="1"
319364 --8<-- "docs/beginners-guide/code/actions/parameterized_function.py:30:36"
320- ```
365+ ``` -->
366+
367+ ## Threaded & Async Actions
368+
369+ Actions can be made asynchronous or threaded by setting the ` synchronous ` flag to ` False ` in the decorator. For methods
370+ that are ** not** ` async ` :
371+
372+ ``` py title="Threaded Actions" linenums="3"
373+ class ServoMotor (Thing ):
374+
375+ @action (synchronous = False )
376+ def poll_device_state (self ) -> str :
377+ """ check device state, especially when it got stuck up"""
378+ ...
379+
380+ @action (threaded = True ) # exactly the same effect for sync methods
381+ def poll_device_state (self ) -> str :
382+ """ check device state, especially when it got stuck up"""
383+ ...
384+ ```
385+
386+ The return value is fetched and returned to the client. One could also start long running actions without fetching a return value,
387+ (although it would be better in many cases to manually thread out a long running action):
388+
389+ ``` py title="Threaded Actions" linenums="3"
390+ class DCPowerSupply (Thing ):
391+ """ A DC Power Supply from 0-30V"""
392+
393+ @action (threaded = True )
394+ def monitor_over_voltage (self , period : float = 5 ):
395+ """ background voltage monitor loop"""
396+ while True :
397+ voltage = self .measure_voltage()
398+ if voltage > self .over_voltage_threshold:
399+ self .over_voltage_event(
400+ dict (
401+ timestamp = datetime.datetime.now().strftime(
402+ " %Y-%m-%d %H:%M:%S"
403+ ),
404+ voltage = voltage
405+ )
406+ )
407+ time.sleep(period)
408+ # The suitability of this example in a realistic use case is untested
409+ ```
410+
411+ Same applies for ` async ` :
412+
413+ ``` py title="Async Actions" linenums="3"
414+ class DCPowerSupply (Thing ):
415+
416+ @action (create_task = True )
417+ async def monitor_over_voltage (self , period : float = 5 ):
418+ """ background monitor loop"""
419+ while True :
420+ voltage = await asyncio.get_running_loop().run_in_executor(
421+ None , self .measure_voltage
422+ )
423+ if voltage > self .over_voltage_threshold:
424+ self .over_voltage_event(
425+ dict (
426+ timestamp = datetime.datetime.now().strftime(
427+ " %Y-%m-%d %H:%M:%S"
428+ ),
429+ voltage = voltage
430+ )
431+ )
432+ await asyncio.sleep(period)
433+ # The suitability of this example in a realistic use case is untested
434+ ```
435+
436+ For long running actions that do not return, call them with ` noblock ` flag on the client, otherwise except a ` TimeoutError ` :
437+
438+ ``` py
439+ client.invoke_action(" monitor_over_voltage" , period = 10 , noblock = True )
440+ ```
321441
322- ## Async & Threaded Actions
442+ ## Thing Description Metadata
323443
324- ## TD
444+ | field | supported | description |
445+ | ----------- | --------- | ------------------------------------------------------------------------------------------------------------------------------- |
446+ | input | ✓ | schema of the input payload (validation carried out) |
447+ | output | ✓ | schema of the output payload (validation not carried out) |
448+ | safe | ✓ | whether the action is safe to execute, only treated as a metadata |
449+ | idempotent | ✓ | whether the action is idempotent, ` True ` when the action is executable in all states of a state machine, otherwise ` False ` |
450+ | synchronous | ✓ | whether the action is synchronous, ` False ` for threaded actions and async actions which are scheduled in the running event loop |
0 commit comments