Actions
Only methods decorated with action() are exposed to clients.
Payload Validation
If arguments are loosely typed, the action will be invoked with given payload without any validation. One may validate them manually inside the method. However, one can also specify the expected argument schema using either JSON Schema or pydantic models:
Specify the expected type of the argument (with or without name)
JSON schema seen in Thing Description
Specify the argument names under the properties field with type as object.
Names not found in the properties field can be subsumed under python spread operator **kwargs if necessary (dont set additionalProperties to False in that case).
JSON schema seen in Thing Description
{
"description": "Set the parameter for a channel. https://www.picotech.com/download/manuals/picoscope-6000-series-a-api-programmers-guide.pdf",
"input": {
"type": "object",
"properties": {
"channel": {"type": "string", "enum": ["A", "B", "C", "D"]},
"enabled": {"type": "boolean"},
"voltage_range": {
"type": "string",
"enum": [
"10mV",
"20mV",
"50mV",
"100mV",
"200mV",
"500mV",
"1V",
"2V",
"5V",
"10V",
"20V",
"50V",
"MAX_RANGES"
]},
"offset": {"type": "number"},
"coupling": {"type": "string", "enum": ["AC", "DC"]},
"bw_limiter": {"type": "string", "enum": ["full", "20MHz"]}
}
},
"synchronous": True
}
Specify return type under output_schema field:
JSON schema seen in Thing Description
{
"description": "analogue offset for a voltage range and coupling",
"synchronous": True,
"input": {
"type": "object",
"properties": {
"voltage_range": {
"type": "string",
"enum": ["10mV",
"20mV",
"50mV",
"100mV",
"200mV",
"500mV",
"1V",
"2V",
"5V",
"10V",
"20V",
"50V",
"MAX_RANGES"
]
},
"coupling": {"type": "string", "enum": ["AC", "DC"]}
}
},
"output": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": {"type": "number"}
},
}
Type annotate the argument, either plainly or with Annotated. A pydantic model will be composed with the argument name as the field name and the type annotation as the field type:
JSON schema seen in Thing Description
{
"description": "Start acquisition of energy measurements. max_count: maximum number of measurements to acquire before stopping automatically.",
"input": {
"properties": {
"max_count": {
"exclusiveMinimum": 0,
"type": "integer"
}
},
"required": ["max_count"],
"title": "start_acquisition_input",
"type": "object"
},
"synchronous": True
}
Again, type annotate the arguments, either plainly or with Annotated:
from typing import Literal
class Picoscope6000(Thing):
@action()
def set_channel(
self,
channel: Literal["A", "B", "C", "D"],
enabled: bool = True,
v_range: Literal[
"10mV",
"20mV",
"50mV",
"100mV",
"200mV",
"500mV",
"1V",
"2V",
"5V",
"10V",
"20V",
"50V",
"MAX_RANGES",
] = "2V",
offset: float = 0,
coupling: Literal["AC", "DC"] = "DC_1M",
bw_limiter: Literal["full", "20MHz"] = "full",
) -> None:
JSON schema seen in Thing Description
{
"description": "Set the parameter for a channel. https://www.picotech.com/download/manuals/picoscope-6000-series-a-api-programmers-guide.pdf",
"input": {
"properties": {
"channel": {
"enum": ["A", "B", "C", "D"],
"type": "string"
},
"enabled": {"default": True, "type": "boolean"},
"v_range": {
"default": "2V",
"enum": [
"10mV",
"20mV",
"50mV",
"100mV",
"200mV",
"500mV",
"1V",
"2V",
"5V",
"10V",
"20V",
"50V",
"MAX_RANGES"
],
"type": "string"
},
"offset": {"default": 0, "type": "number"},
"coupling": {
"default": "DC_1M",
"enum": ["AC", "DC"],
"type": "string"
},
"bw_limiter": {
"default": "full",
"enum": ["full", "20MHz"],
"type": "string"
}
},
"required": ["channel"],
"type": "object"
},
"synchronous": True
}
type annotate the return type:
from typing import Annotated
from pydantic import Field
class SerialUtility(Thing):
@action()
def execute_instruction(
self, command: str, return_data_size: Annotated[int, Field(ge=0)] = 0
) -> str:
"""
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.
"""
JSON schema seen in Thing Description
{
"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.",
"input": {
"properties": {
"command": {"type": "string"},
"return_data_size": {"default": 0, "minimum": 0, "type": "integer"}
},
"required": ["command"],
"type": "object"
},
"output": {"type": "string"},
"synchronous": True
}
If the composed models from the type annotations are not sufficient or contain errors, one may directly supply the models:
Threaded & Async Actions
Actions can be made asynchronous or threaded by setting the synchronous flag to False. For methods that are not async,
they will run in a separate thread:
The return value is fetched and returned to the client. One could also start long running actions without fetching a return value, (although it would be better in many cases to manually thread out a long running action):
For async actions, they are scheduled in the running event loop as a task:
For long running actions that do not return, call them with oneway flag on the client, otherwise expect a TimeoutError:
Thing Description Metadata
| field | supported | description |
|---|---|---|
| input | ✓ | schema of the input payload (validation carried out) |
| output | ✓ | schema of the output payload (validation not carried out) |
| safe | ✓ | whether the action is safe to execute, only treated as a metadata |
| idempotent | ✓ | whether the action is idempotent, True when the action is executable in all states of a state machine, otherwise False |
| synchronous | ✓ | whether the action is synchronous, False for threaded actions and async actions which are scheduled in the running event loop |