Skip to content

Commit 940c735

Browse files
committed
state machine docs somewhat OK
1 parent de513d5 commit 940c735

File tree

4 files changed

+130
-86
lines changed

4 files changed

+130
-86
lines changed
Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
[API Reference](../../api-reference/thing/state-machine.md)
22

33
Often, certain operations are not allowed during certain conditions, for example,
4-
one cannot turn ON a motor twice in a row, or one does not wish to change the
5-
exposure of a camera during video capture (say).
4+
one cannot turn ON a motor twice in a row, or a measurement device cannot modify a setting change if a measurement is ongoing.
65

76
To implement these contraints, a state machine may be used to prevent property writes or
87
action invokations in certain states (events are not supported). A `StateMachine` is a class-level
@@ -11,20 +10,20 @@ in these states:
1110

1211
```py title="Definition" linenums="1"
1312
--8<-- "docs/beginners-guide/code/fsm/def.py:1:1"
14-
--8<-- "docs/beginners-guide/code/fsm/def.py:12:15"
15-
--8<-- "docs/beginners-guide/code/fsm/def.py:34:36"
16-
--8<-- "docs/beginners-guide/code/fsm/def.py:40:41"
13+
--8<-- "docs/beginners-guide/code/fsm/def.py:14:15"
14+
--8<-- "docs/beginners-guide/code/fsm/def.py:30:33"
15+
--8<-- "docs/beginners-guide/code/fsm/def.py:37:38"
1716
```
1817

1918
Specify the machine conditions as keyword arguments to the `state_machine` with properties and actions
2019
in a list:
2120

2221
```py title="Specify Properties and Actions" linenums="1"
2322
--8<-- "docs/beginners-guide/code/fsm/def.py:1:2"
24-
--8<-- "docs/beginners-guide/code/fsm/def.py:12:41"
23+
--8<-- "docs/beginners-guide/code/fsm/def.py:12:38"
2524
```
2625

27-
As expected, one needs to set the `StateMachine` state to indicate state changes:
26+
One needs to set the `StateMachine` state to indicate state changes:
2827

2928
```py title="set_state()" linenums="1"
3029
--8<-- "docs/beginners-guide/code/fsm/def.py:13:15"
@@ -34,22 +33,43 @@ As expected, one needs to set the `StateMachine` state to indicate state changes
3433
One can also sepcify the allowed state of a property or action directly
3534
on the corresponding objects:
3635

37-
```py title="Specify State Alternate" linenums="1"
36+
```py title="Specify State Directly on Object" linenums="1"
3837
--8<-- "docs/beginners-guide/code/fsm/def.py:13:15"
39-
--8<-- "docs/beginners-guide/code/fsm/def.py:64:"
38+
--8<-- "docs/beginners-guide/code/fsm/def.py:64:74"
4039
```
4140

41+
## State Change Events
42+
4243
State machines also push state change event when the state changes:
4344

44-
```py title="Definition" linenums="1"
45+
```py title="Definition" linenums="1" hl_lines="7"
46+
--8<-- "docs/beginners-guide/code/fsm/def.py:13:16"
47+
--8<-- "docs/beginners-guide/code/fsm/def.py:41:48"
48+
```
4549

50+
One can suppress state change events by setting:
51+
52+
```python title="suppress state change event"
53+
self.state_machine.set_state('STATE', push_event=False)
4654
```
4755

48-
One can suppress state change events by setting `push_state_change_event=False`.
56+
```python title="subscription" linenums="1"
57+
def state_change_cb(event):
58+
print(f"State changed to {event.data}")
4959

50-
Lastly, one can also supply callbacks which are executed when entering and exiting certain states,
51-
irrespective of where or when the state change occured:
60+
client.observe_property(name="state", callbacks=state_change_cb)
61+
```
5262

53-
```py title="Definition" linenums="1"
63+
> One can supply multiple callbacks which may called in series or concurrently (see [Events](events.md#subscription)).
64+
65+
## State Change Callbacks
66+
67+
One can also supply callbacks which are executed when entering and exiting certain states,
68+
irrespective of where or when the state change occured:
5469

70+
```py title="enter and exit callbacks" linenums="1" hl_lines="21"
71+
--8<-- "docs/beginners-guide/code/fsm/def.py:13:15"
72+
--8<-- "docs/beginners-guide/code/fsm/def.py:75:"
5573
```
74+
75+
These callbacks are executed after the state change is effected, and are mostly useful when there are state changes at multiple places in the code which need to trigger the same side-effects.

docs/beginners-guide/code/fsm/client.py

Whitespace-only changes.
Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from hololinked.server import Thing, StateMachine, action, Property
2-
from hololinked.server.properties import String
1+
from hololinked.core import Thing, StateMachine, action, Property
2+
from hololinked.core.properties import String
33
from enum import StrEnum
44

5+
56
class states(StrEnum):
67
DISCONNECTED = "DISCONNECTED"
78
ON = "ON"
@@ -12,28 +13,24 @@ class states(StrEnum):
1213

1314
class Picoscope(Thing):
1415
"""A PC Oscilloscope from Picotech"""
15-
16+
1617
@action()
17-
def connect(self):
18-
...
18+
def connect(self): ...
1919

2020
@action()
21-
def disconnect(self):
22-
...
21+
def disconnect(self): ...
2322

2423
@action()
25-
def start_acquisition(self):
26-
...
24+
def start_acquisition(self): ...
2725

2826
@action()
29-
def stop_acquisition(self):
30-
...
27+
def stop_acquisition(self): ...
3128

3229
serial_number = String()
3330

3431
state_machine = StateMachine(
35-
states=['DISCONNECTED', 'ON', 'FAULT', 'ALARM', 'MEASURING'],
36-
initial_state='DISCONNECTED',
32+
states=["DISCONNECTED", "ON", "FAULT", "ALARM", "MEASURING"],
33+
initial_state="DISCONNECTED",
3734
DISCONNECTED=[connect, serial_number],
3835
ON=[start_acquisition, disconnect],
3936
MEASURING=[stop_acquisition],
@@ -47,28 +44,51 @@ def stop_acquisition(self):
4744
push_state_change_event=True,
4845
DISCONNECTED=[connect, serial_number],
4946
ON=[start_acquisition, disconnect],
50-
MEASURING=[stop_acquisition]
47+
MEASURING=[stop_acquisition],
5148
)
5249
# v2
5350

5451
def connect(self):
55-
# add connect logic here
56-
self.state_machine.set_state('ON')
52+
"""add connect logic here"""
53+
self.state_machine.set_state("ON")
5754

5855
def disconnect(self):
59-
# add disconnect logic here
60-
self.state_machine.current_state = 'DISCONNECTED'
61-
# same as self.state_machine.set_state('DISCONNECTED',
62-
# push_event=True, skip_callbacks=False)
56+
"""add disconnect logic here"""
57+
self.state_machine.current_state = "DISCONNECTED"
58+
# self.state_machine.set_state('DISCONNECTED', push_event=True, skip_callbacks=False)
6359

6460
@action(state=[states.ON])
6561
def start_acquisition(self):
66-
# add start measurement logic
62+
"""add start measurement logic here"""
6763
self.state_machine.set_state(states.MEASURING)
6864

6965
@action(state=[states.MEASURING, states.FAULT, states.ALARM])
7066
def stop_acquisition(self):
71-
# add stop measurement logic
67+
"""add stop measurement logic here"""
7268
if self.state_machine.state == states.MEASURING:
7369
self.state_machine.set_state(states.ON)
74-
# else allow FAULT or ALARM state to persist
70+
# else allow FAULT or ALARM state to persist to inform the user that something is wrong
71+
72+
serial_number = String(
73+
state=[states.DISCONNECTED], doc="serial number of the device"
74+
) # type: str
75+
76+
def cancel_polling(self):
77+
"""add cancel polling logic here"""
78+
self._cancel_polling = True
79+
80+
def polling_loop(self):
81+
"""add polling logic here"""
82+
self._cancel_polling = False
83+
while not self._cancel_polling:
84+
...
85+
86+
state_machine = StateMachine(
87+
states=states,
88+
initial_state=states.DISCONNECTED,
89+
push_state_change_event=True,
90+
DISCONNECTED=[connect, serial_number],
91+
ON=[start_acquisition, disconnect],
92+
MEASURING=[stop_acquisition],
93+
on_enter=dict(DISCONNECTED=[cancel_polling]),
94+
)

docs/beginners-guide/code/properties/schema.py

Lines changed: 53 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,26 @@
55

66

77
class Rect(BaseModel):
8-
x : Annotated[int, Field(default=0, ge=0)]
9-
y : Annotated[int, Field(default=0, ge=0)]
10-
width : Annotated[int, Field(default=0, gt=0)]
8+
x: Annotated[int, Field(default=0, ge=0)]
9+
y: Annotated[int, Field(default=0, ge=0)]
10+
width: Annotated[int, Field(default=0, gt=0)]
1111
height: Annotated[int, Field(default=0, gt=0)]
1212

1313

1414
class UEyeCamera(Thing):
15-
1615
def get_aoi(self) -> Rect:
1716
"""Get current AOI from camera as Rect object (with x, y, width, height)"""
1817
rect_aoi = ueye.IS_RECT()
19-
ret = ueye.is_AOI(self.handle, ueye.IS_AOI_IMAGE_GET_AOI,
20-
rect_aoi, ueye.sizeof(rect_aoi))
18+
ret = ueye.is_AOI(
19+
self.handle, ueye.IS_AOI_IMAGE_GET_AOI, rect_aoi, ueye.sizeof(rect_aoi)
20+
)
2121
assert return_code_OK(self.handle, ret)
2222
return Rect(
23-
x=rect_aoi.s32X.value,
24-
y=rect_aoi.s32Y.value,
25-
width=rect_aoi.s32Width.value,
26-
height=rect_aoi.s32Height.value
27-
)
23+
x=rect_aoi.s32X.value,
24+
y=rect_aoi.s32Y.value,
25+
width=rect_aoi.s32Width.value,
26+
height=rect_aoi.s32Height.value,
27+
)
2828

2929
def set_aoi(self, value: Rect) -> None:
3030
"""Set camera AOI. Specify as x,y,width,height or a tuple
@@ -35,69 +35,73 @@ def set_aoi(self, value: Rect) -> None:
3535
rect_aoi.s32Width = ueye.int(value.width)
3636
rect_aoi.s32Height = ueye.int(value.height)
3737

38-
ret = ueye.is_AOI(self.handle, ueye.IS_AOI_IMAGE_SET_AOI,
39-
rect_aoi, ueye.sizeof(rect_aoi))
38+
ret = ueye.is_AOI(
39+
self.handle, ueye.IS_AOI_IMAGE_SET_AOI, rect_aoi, ueye.sizeof(rect_aoi)
40+
)
4041
assert return_code_OK(self.handle, ret)
4142

42-
AOI = Property(fget=get_aoi, fset=set_aoi, model=Rect,
43-
doc="Area of interest within the image",) # type: Rect
43+
AOI = Property(
44+
fget=get_aoi,
45+
fset=set_aoi,
46+
model=Rect,
47+
doc="Area of interest within the image",
48+
) # type: Rect
4449

4550

4651
import ctypes
4752
from picosdk.ps6000 import ps6000 as ps
4853
from picosdk.functions import assert_pico_ok
4954

5055
trigger_schema = {
51-
'type': 'object',
52-
'properties' : {
53-
'enabled' : { 'type': 'boolean' },
54-
'channel' : {
55-
'type': 'string',
56-
'enum': ['A', 'B', 'C', 'D', 'EXTERNAL', 'AUX']
56+
"type": "object",
57+
"properties": {
58+
"enabled": {"type": "boolean"},
59+
"channel": {
60+
"type": "string",
61+
"enum": ["A", "B", "C", "D", "EXTERNAL", "AUX"],
5762
# include both external and aux for 5000 & 6000 series
5863
# let the device driver will check if the channel is valid for the series
5964
},
60-
'threshold' : { 'type': 'number' },
61-
'adc' : { 'type': 'boolean' },
62-
'direction' : {
63-
'type': 'string',
64-
'enum': ['above', 'below', 'rising', 'falling', 'rising_or_falling']
65+
"threshold": {"type": "number"},
66+
"adc": {"type": "boolean"},
67+
"direction": {
68+
"type": "string",
69+
"enum": ["above", "below", "rising", "falling", "rising_or_falling"],
6570
},
66-
'delay' : { 'type': 'integer' },
67-
'auto_trigger' : {
68-
'type': 'integer',
69-
'minimum': 0
70-
}
71+
"delay": {"type": "integer"},
72+
"auto_trigger": {"type": "integer", "minimum": 0},
7173
},
72-
"description" : "Trigger settings for a single channel of the picoscope",
74+
"description": "Trigger settings for a single channel of the picoscope",
7375
}
7476

77+
7578
class Picoscope(Thing):
79+
trigger = Property(doc="Trigger settings", model=trigger_schema) # type: dict
7680

77-
trigger = Property(doc="Trigger settings",
78-
model=trigger_schema) # type: dict
79-
8081
@trigger.setter
81-
def set_trigger(self, value : dict) -> None:
82+
def set_trigger(self, value: dict) -> None:
8283
channel = value["channel"].upper()
8384
direction = value["direction"].upper()
8485
enabled = ctypes.c_int16(int(value["enabled"]))
8586
delay = ctypes.c_int32(value["delay"])
86-
direction = ps.PS6000_THRESHOLD_DIRECTION[f'PS6000_{direction}']
87-
if channel in ['A', 'B', 'C', 'D']:
88-
channel = ps.PS6000_CHANNEL['PS6000_CHANNEL_{}'.format(
89-
channel)]
87+
direction = ps.PS6000_THRESHOLD_DIRECTION[f"PS6000_{direction}"]
88+
if channel in ["A", "B", "C", "D"]:
89+
channel = ps.PS6000_CHANNEL["PS6000_CHANNEL_{}".format(channel)]
9090
else:
91-
channel = ps.PS6000_CHANNEL['PS6000_TRIGGER_AUX']
91+
channel = ps.PS6000_CHANNEL["PS6000_TRIGGER_AUX"]
9292
if not value["adc"]:
93-
if channel in ['A', 'B', 'C', 'D']:
94-
threshold = int(threshold * self.max_adc * 1e3
95-
/ self.ranges[self.channel_settings[channel]['v_range']])
93+
if channel in ["A", "B", "C", "D"]:
94+
threshold = int(
95+
threshold
96+
* self.max_adc
97+
* 1e3
98+
/ self.ranges[self.channel_settings[channel]["v_range"]]
99+
)
96100
else:
97-
threshold = int(self.max_adc/5)
101+
threshold = int(self.max_adc / 5)
98102
threshold = ctypes.c_int16(threshold)
99103
auto_trigger = ctypes.c_int16(int(auto_trigger))
100-
self._status['trigger'] = ps.ps6000SetSimpleTrigger(self._ct_handle,
101-
enabled, channel, threshold, direction,
102-
delay, auto_trigger)
103-
assert_pico_ok(self._status['trigger'])
104+
self._status["trigger"] = ps.ps6000SetSimpleTrigger(
105+
self._ct_handle, enabled, channel, threshold, direction, delay, auto_trigger
106+
)
107+
assert_pico_ok(self._status["trigger"])

0 commit comments

Comments
 (0)