Quickstart¶
The easiest way to get started with Python-LabThings is via the labthings.create_app()
function, and the labthings.LabThing
builder methods.
We will assume that for basic usage you already have some basic instrument control code. In our example, this is in the form of a PretendSpectrometer
class, which will generate some data like your instrument control code might. Our PretendSpectrometer
class has a data
attribute which quickly returns a spectrum, an x_range
attribute which determines the range of data we’ll return, an integration_time
attribute for cleaning up our signal, and a slow average_data(n)
method to average n
individual data measurements.
Building an API from this class requires a few extra considerations. In order to tell our API what data to expect from users, we need to construct a schema for each of our interactions. This schema simply maps variable names to JSON-compatible types, and is made simple via the labthings.fields
module.
For properties, the input and output MUST be formatted the same, and so a single schema
argument handles both. For actions, the input parameters and output response may be different. In this case, we can pass a schema
argument to format the output, and an args
argument to specify the input parameters,
An example Lab Thing built from our PretendSpectrometer
class, complete with schemas, might look like:
import time
from labthings import ActionView, PropertyView, create_app, fields, find_component, op
from labthings.example_components import PretendSpectrometer
from labthings.json import encode_json
"""
Class for our lab component functionality. This could include serial communication,
equipment API calls, network requests, or a "virtual" device as seen here.
"""
"""
Create a view to view and change our integration_time value,
and register is as a Thing property
"""
# Wrap in a semantic annotation to autmatically set schema and args
class DenoiseProperty(PropertyView):
"""Value of integration_time"""
schema = fields.Int(required=True, minimum=100, maximum=500)
semtype = "LevelProperty"
@op.readproperty
def get(self):
# When a GET request is made, we'll find our attached component
my_component = find_component("org.labthings.example.mycomponent")
return my_component.integration_time
@op.writeproperty
def put(self, new_property_value):
# Find our attached component
my_component = find_component("org.labthings.example.mycomponent")
# Apply the new value
my_component.integration_time = new_property_value
return my_component.integration_time
"""
Create a view to quickly get some noisy data, and register is as a Thing property
"""
class QuickDataProperty(PropertyView):
"""Show the current data value"""
# Marshal the response as a list of floats
schema = fields.List(fields.Float())
@op.readproperty
def get(self):
# Find our attached component
my_component = find_component("org.labthings.example.mycomponent")
return my_component.data
"""
Create a view to start an averaged measurement, and register is as a Thing action
"""
class MeasurementAction(ActionView):
# Expect JSON parameters in the request body.
# Pass to post function as dictionary argument.
args = {
"averages": fields.Integer(
missing=20, example=20, description="Number of data sets to average over",
)
}
# Marshal the response as a list of numbers
schema = fields.List(fields.Number)
# Main function to handle POST requests
@op.invokeaction
def post(self, args):
"""Start an averaged measurement"""
# Find our attached component
my_component = find_component("org.labthings.example.mycomponent")
# Get arguments and start a background task
n_averages = args.get("averages")
# Return the task information
return my_component.average_data(n_averages)
# Create LabThings Flask app
app, labthing = create_app(
__name__,
title="My Lab Device API",
description="Test LabThing-based API",
version="0.1.0",
)
# Attach an instance of our component
# Usually a Python object controlling some piece of hardware
my_spectrometer = PretendSpectrometer()
labthing.add_component(my_spectrometer, "org.labthings.example.mycomponent")
# Add routes for the API views we created
labthing.add_view(DenoiseProperty, "/integration_time")
labthing.add_view(QuickDataProperty, "/quick-data")
labthing.add_view(MeasurementAction, "/actions/measure")
# Start the app
if __name__ == "__main__":
from labthings import Server
Server(app).run()
Once started, the app will build and serve a full web API, and generate the following Thing Description:
{
"@context": [
"https://www.w3.org/2019/wot/td/v1",
"https://iot.mozilla.org/schemas/"
],
"id": "http://127.0.0.1:7486/",
"base": "http://127.0.0.1:7486/",
"title": "My PretendSpectrometer API",
"description": "LabThing API for PretendSpectrometer",
"properties": {
"pretendSpectrometerData": {
"title": "PretendSpectrometer_data",
"description": "A single-shot measurement",
"readOnly": true,
"links": [{
"href": "/properties/PretendSpectrometer/data"
}],
"forms": [{
"op": "readproperty",
"htv:methodName": "GET",
"href": "/properties/PretendSpectrometer/data",
"contentType": "application/json"
}],
"type": "array",
"items": {
"type": "number",
"format": "decimal"
}
},
"pretendSpectrometerMagicDenoise": {
"title": "PretendSpectrometer_magic_denoise",
"description": "Single-shot integration time",
"links": [{
"href": "/properties/PretendSpectrometer/magic_denoise"
}],
"forms": [{
"op": "readproperty",
"htv:methodName": "GET",
"href": "/properties/PretendSpectrometer/magic_denoise",
"contentType": "application/json"
},
{
"op": "writeproperty",
"htv:methodName": "PUT",
"href": "/properties/PretendSpectrometer/magic_denoise",
"contentType": "application/json"
}
],
"type": "number",
"format": "integer",
"min": 100,
"max": 500,
"example": 200
}
},
"actions": {
"averageDataAction": {
"title": "average_data_action",
"description": "Take an averaged measurement",
"links": [{
"href": "/actions/PretendSpectrometer/average_data"
}],
"forms": [{
"op": "invokeaction",
"htv:methodName": "POST",
"href": "/actions/PretendSpectrometer/average_data",
"contentType": "application/json"
}],
"input": {
"type": "object",
"properties": {
"n": {
"type": "number",
"format": "integer",
"default": 5,
"description": "Number of averages to take",
"example": 5
}
}
}
}
},
"links": [],
"securityDefinitions": {},
"security": "nosec_sc"
}
For completeness of the examples, our PretendSpectrometer
class code is:
import random
import math
import time
class PretendSpectrometer:
def __init__(self):
self.x_range = range(-100, 100)
self.integration_time = 200
def make_spectrum(self, x, mu=0.0, sigma=25.0):
"""
Generate a noisy gaussian function (to act as some pretend data)
Our noise is inversely proportional to self.integration_time
"""
x = float(x - mu) / sigma
return (
math.exp(-x * x / 2.0) / math.sqrt(2.0 * math.pi) / sigma
+ (1 / self.integration_time) * random.random()
)
@property
def data(self):
"""Return a 1D data trace."""
time.sleep(self.integration_time / 1000)
return [self.make_spectrum(x) for x in self.x_range]
def average_data(self, n: int):
"""Average n-sets of data. Emulates a measurement that may take a while."""
summed_data = self.data
for _ in range(n):
summed_data = [summed_data[i] + el for i, el in enumerate(self.data)]
time.sleep(0.25)
summed_data = [i / n for i in summed_data]
return summed_data