Skip to content

Commit a4d8859

Browse files
committed
Merge branch 'main' into proscan
2 parents f3022d4 + 06bc8b8 commit a4d8859

File tree

3 files changed

+147
-9
lines changed

3 files changed

+147
-9
lines changed

.github/workflows/publish.yml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: Publish to PyPI
2+
3+
on: push
4+
5+
#on:
6+
# release:
7+
# types: [published]
8+
9+
jobs:
10+
build:
11+
name: Build distribution 📦
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- uses: actions/checkout@v4
16+
- name: Set up Python
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: "3.x"
20+
- name: Install pypa/build
21+
run: >-
22+
python3 -m
23+
pip install
24+
build
25+
--user
26+
- name: Build a binary wheel and a source tarball
27+
run: python3 -m build
28+
- name: Store the distribution packages
29+
uses: actions/upload-artifact@v4
30+
with:
31+
name: python-package-distributions
32+
path: dist/
33+
34+
publish-to-testpypi:
35+
name: Publish Python 🐍 distribution 📦 to TestPyPI
36+
needs:
37+
- build
38+
runs-on: ubuntu-latest
39+
40+
environment:
41+
name: testpypi
42+
url: https://test.pypi.org/p/labthings-picamera2
43+
44+
permissions:
45+
id-token: write # IMPORTANT: mandatory for trusted publishing
46+
47+
steps:
48+
- name: Download all the dists
49+
uses: actions/download-artifact@v4
50+
with:
51+
name: python-package-distributions
52+
path: dist/
53+
- name: Publish distribution 📦 to TestPyPI
54+
uses: pypa/gh-action-pypi-publish@release/v1
55+
with:
56+
repository-url: https://test.pypi.org/legacy/
57+
58+
publish-to-pypi:
59+
name: >-
60+
Publish Python 🐍 distribution 📦 to PyPI
61+
if: startsWith(github.ref, 'refs/tags/v') # only publish to PyPI on tag pushes
62+
needs:
63+
- build
64+
runs-on: ubuntu-latest
65+
environment:
66+
name: pypi
67+
url: https://pypi.org/p/labthings-picamera2
68+
permissions:
69+
id-token: write # IMPORTANT: mandatory for trusted publishing
70+
steps:
71+
- name: Download all the dists
72+
uses: actions/download-artifact@v4
73+
with:
74+
name: python-package-distributions
75+
path: dist/
76+
- name: Publish distribution 📦 to PyPI
77+
uses: pypa/gh-action-pypi-publish@release/v1

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "labthings-sangaboard"
3-
version = "0.0.0"
3+
version = "0.0.1"
44
authors = [
55
{ name="Richard Bowman", email="richard.bowman@cantab.net" },
66
]

src/labthings_sangaboard/__init__.py

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66
from labthings_fastapi.descriptors.property import PropertyDescriptor
77
from labthings_fastapi.thing import Thing
88
from 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
1011
from contextlib import contextmanager
1112
from collections.abc import Sequence, Mapping
1213
import sangaboard
1314
import threading
15+
import time
16+
import numpy as np
1417

1518
class 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

Comments
 (0)