66from labthings_fastapi .descriptors .property import PropertyDescriptor
77from labthings_fastapi .thing import Thing
88from labthings_fastapi .decorators import thing_action , thing_property
9- from typing import Iterator
9+ from labthings_fastapi .dependencies .invocation import CancelHook , InvocationCancelledError
10+ from typing import Iterator , Literal
1011from contextlib import contextmanager
1112from collections .abc import Sequence , Mapping
1213import sangaboard
1314import threading
15+ import time
16+ import numpy as np
1417
1518class SangaboardThing (Thing ):
1619 _axis_names = ("x" , "y" , "z" ) # TODO: handle 4th axis gracefully
@@ -26,6 +29,10 @@ def __init__(self, port: str=None, **kwargs):
2629 def __enter__ (self ):
2730 self ._sangaboard = sangaboard .Sangaboard (** self .sangaboard_kwargs )
2831 self ._sangaboard_lock = threading .RLock ()
32+ with self .sangaboard () as sb :
33+ if sb .version_tuple [0 ] != 1 :
34+ raise RuntimeError ("labthings-sangaboard requires firmware v1" )
35+ sb .query ("blocking_moves false" )
2936 self .update_position ()
3037
3138 def __exit__ (self , _exc_type , _exc_value , _traceback ):
@@ -77,16 +84,30 @@ def thing_state(self):
7784 }
7885
7986 @thing_action
80- def move_relative (self , ** kwargs : Mapping [str , int ]):
87+ def move_relative (self , cancel : CancelHook , block_cancellation : bool = False , ** kwargs : Mapping [str , int ]):
8188 """Make a relative move. Keyword arguments should be axis names."""
89+ displacement = [kwargs .get (k , 0 ) for k in self .axis_names ]
8290 with self .sangaboard () as sb :
8391 self .moving = True
84- sb .move_rel ([kwargs .get (k , 0 ) for k in self .axis_names ])
85- self .moving = False
86- self .update_position ()
92+ try :
93+ sb .move_rel (displacement )
94+ if block_cancellation :
95+ sb .query ("notify_on_stop" )
96+ else :
97+ while sb .query ("moving?" ) == "true" :
98+ cancel .sleep (0.1 )
99+ except InvocationCancelledError as e :
100+ # If the move has been cancelled, stop it but don't handle the exception.
101+ # We need the exception to propagate in order to stop any calling tasks,
102+ # and to mark the invocation as "cancelled" rather than stopped.
103+ sb .query ("stop" )
104+ raise e
105+ finally :
106+ self .moving = False
107+ self .update_position ()
87108
88109 @thing_action
89- def move_absolute (self , ** kwargs : Mapping [str , int ]):
110+ def move_absolute (self , cancel : CancelHook , block_cancellation : bool = False , ** kwargs : Mapping [str , int ]):
90111 """Make an absolute move. Keyword arguments should be axis names."""
91112 with self .sangaboard ():
92113 self .update_position ()
@@ -95,7 +116,7 @@ def move_absolute(self, **kwargs: Mapping[str, int]):
95116 for k , v in kwargs .items ()
96117 if k in self .axis_names
97118 }
98- self .move_relative (** displacement )
119+ self .move_relative (cancel , block_cancellation = block_cancellation , ** displacement )
99120
100121 @thing_action
101122 def abort_move (self ):
@@ -108,4 +129,44 @@ def abort_move(self):
108129 tc = self ._sangaboard .termination_character
109130 self ._sangaboard ._ser .write (("stop" + tc ).encode ())
110131 else :
111- raise HTTPException (status_code = 409 , detail = "Stage is not moving." )
132+ raise HTTPException (status_code = 409 , detail = "Stage is not moving." )
133+
134+ @thing_action
135+ def set_zero_position (self ):
136+ """Make the current position zero in all axes
137+
138+ This action does not move the stage, but resets the position to zero.
139+ It is intended for use after manually or automatically recentring the
140+ stage.
141+ """
142+ with self .sangaboard () as sb :
143+ sb .zero_position ()
144+ self .update_position ()
145+
146+ @thing_action
147+ def flash_led (
148+ self ,
149+ number_of_flashes : int = 10 ,
150+ dt : float = 0.5 ,
151+ led_channel : Literal ["cc" ]= "cc" ,
152+ ) -> None :
153+ """Flash the LED to identify the board
154+
155+ This is intended to be useful in situations where there are multiple
156+ Sangaboards in use, and it is necessary to identify which one is
157+ being addressed.
158+ """
159+ with self .sangaboard () as sb :
160+ r = sb .query ("led_cc?" )
161+ if not r .startswith ('CC LED:' ):
162+ raise IOError ("The sangaboard does not support LED control" )
163+ # This suffers from repeated reads and writes decreasing it, so for
164+ # now, I'll fix it at the default value.
165+ # TODO: proper LED control from python
166+ #on_brightness = float(r[7:])
167+ on_brightness = 0.32
168+ for i in range (number_of_flashes ):
169+ sb .query ("led_cc 0" )
170+ time .sleep (dt )
171+ sb .query (f"led_cc { on_brightness } " )
172+ time .sleep (dt )
0 commit comments