UC2-REST 0.2.0.39__tar.gz → 0.2.0.41__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/PKG-INFO +1 -1
  2. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/UC2_REST.egg-info/PKG-INFO +1 -1
  3. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/UC2_REST.egg-info/SOURCES.txt +3 -0
  4. uc2_rest-0.2.0.41/tests/test_canota_reconnect.py +122 -0
  5. uc2_rest-0.2.0.41/tests/test_stage_scan.py +213 -0
  6. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/__version__.py +1 -1
  7. uc2_rest-0.2.0.41/uc2rest/aio.py +360 -0
  8. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/camera_trigger.py +4 -1
  9. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/can.py +1 -1
  10. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/canota.py +26 -6
  11. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/galvo.py +67 -15
  12. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/home.py +8 -3
  13. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/laser.py +24 -0
  14. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/motor.py +497 -12
  15. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/mserial.py +14 -9
  16. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/state.py +12 -4
  17. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/LICENSE +0 -0
  18. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/README.md +0 -0
  19. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/UC2_REST.egg-info/dependency_links.txt +0 -0
  20. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/UC2_REST.egg-info/not-zip-safe +0 -0
  21. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/UC2_REST.egg-info/requires.txt +0 -0
  22. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/UC2_REST.egg-info/top_level.txt +0 -0
  23. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/setup.cfg +0 -0
  24. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/setup.py +0 -0
  25. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/MockSerial.py +0 -0
  26. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/UC2Client.py +0 -0
  27. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/__init__.py +0 -0
  28. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/analog.py +0 -0
  29. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/camera.py +0 -0
  30. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/cmdrecorder.py +0 -0
  31. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/digitalin.py +0 -0
  32. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/digitalout.py +0 -0
  33. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/fan.py +0 -0
  34. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/gpio.py +0 -0
  35. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/gripper.py +0 -0
  36. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/i2c.py +0 -0
  37. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/lcddisplay.py +0 -0
  38. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/ledmatrix.py +0 -0
  39. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/logger.py +0 -0
  40. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/message.py +0 -0
  41. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/modules.py +0 -0
  42. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/motor_config.py +0 -0
  43. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/objective.py +0 -0
  44. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/pid.py +0 -0
  45. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/ptz.py +0 -0
  46. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/rotator.py +0 -0
  47. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/slm.py +0 -0
  48. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/temperature.py +0 -0
  49. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/utils.py +0 -0
  50. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.41}/uc2rest/wifi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: UC2-REST
3
- Version: 0.2.0.39
3
+ Version: 0.2.0.41
4
4
  Summary: This pacage will help you to drive the ESP32-driven microscopy control modules from UC2
5
5
  Home-page: https://github.com/openUC2/UC2-REST
6
6
  Author: Benedict Diederich
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: UC2-REST
3
- Version: 0.2.0.39
3
+ Version: 0.2.0.41
4
4
  Summary: This pacage will help you to drive the ESP32-driven microscopy control modules from UC2
5
5
  Home-page: https://github.com/openUC2/UC2-REST
6
6
  Author: Benedict Diederich
@@ -7,10 +7,13 @@ UC2_REST.egg-info/dependency_links.txt
7
7
  UC2_REST.egg-info/not-zip-safe
8
8
  UC2_REST.egg-info/requires.txt
9
9
  UC2_REST.egg-info/top_level.txt
10
+ tests/test_canota_reconnect.py
11
+ tests/test_stage_scan.py
10
12
  uc2rest/MockSerial.py
11
13
  uc2rest/UC2Client.py
12
14
  uc2rest/__init__.py
13
15
  uc2rest/__version__.py
16
+ uc2rest/aio.py
14
17
  uc2rest/analog.py
15
18
  uc2rest/camera.py
16
19
  uc2rest/camera_trigger.py
@@ -0,0 +1,122 @@
1
+ """How the CAN streaming OTA worker hands the port back to the parent link.
2
+
3
+ Run: python -m unittest tests.test_canota_reconnect (from the repo root)
4
+
5
+ The parent link must come back at its own baudrate, and without the DTR/RTS
6
+ pulse (a hard reset of the master) when the master itself ended the session.
7
+ It keeps the reset when the host gave up, because the master may then still
8
+ be in binary receive mode.
9
+ """
10
+ import json
11
+ import os
12
+ import tempfile
13
+ import unittest
14
+ from unittest import mock
15
+
16
+ import uc2rest.canota as canota
17
+ from uc2rest.canota import CANOTA
18
+
19
+
20
+ class FakeStreamPort:
21
+ """The raw pyserial port the worker opens for streaming. `reply` maps the
22
+ /ota_start preamble to the master's answer (None = no answer)."""
23
+
24
+ def __init__(self, reply, ack=True, final=None):
25
+ self.reply, self.ack, self.final = reply, ack, final
26
+ self.rx = bytearray()
27
+ self.received = 0
28
+ self.pending = b"" # final status, sent once the last ACK was read
29
+
30
+ def _emit(self, obj):
31
+ self.rx += (json.dumps(obj) + "\n").encode()
32
+
33
+ def write(self, data):
34
+ if b"/ota_start" in data:
35
+ if self.reply is not None:
36
+ self._emit(self.reply)
37
+ return
38
+ self.received += len(data)
39
+ if self.ack:
40
+ self._emit({"ota_rx": self.received})
41
+ if self.final and self.received >= self.reply.get("size", 0):
42
+ self.pending = (json.dumps(self.final) + "\n").encode()
43
+
44
+ @property
45
+ def in_waiting(self):
46
+ if not self.rx and self.pending:
47
+ self.rx, self.pending = bytearray(self.pending), b""
48
+ return len(self.rx)
49
+
50
+ def read(self, n=1):
51
+ out, self.rx = bytes(self.rx[:n]), self.rx[n:]
52
+ return out
53
+
54
+ def flush(self): pass
55
+ def reset_input_buffer(self): pass
56
+ def isOpen(self): return True
57
+ def close(self): pass
58
+
59
+
60
+ class _Parent:
61
+ def __init__(self, baudrate):
62
+ self.serial = mock.Mock(serialport="/dev/ttyTEST", baudrate=baudrate)
63
+ self.serial.serialdevice.isOpen.return_value = True
64
+
65
+
66
+ FIRMWARE = b"\xe9" + bytes(4999) # two 4 KB chunks
67
+
68
+
69
+ class ReconnectTest(unittest.TestCase):
70
+ def setUp(self):
71
+ fd, self.firmware = tempfile.mkstemp(suffix=".bin")
72
+ os.write(fd, FIRMWARE)
73
+ os.close(fd)
74
+ self.addCleanup(os.remove, self.firmware)
75
+
76
+ def run_upload(self, port, parent_baud=115200):
77
+ parent = _Parent(parent_baud)
78
+ opened = []
79
+
80
+ def open_port(p, baud, timeout=None):
81
+ opened.append(baud)
82
+ return port
83
+
84
+ with mock.patch.object(canota.serial, "Serial", side_effect=open_port), \
85
+ mock.patch.object(canota.time, "sleep"), \
86
+ mock.patch.object(canota, "READY_TIMEOUT_S", 0.2), \
87
+ mock.patch.object(canota, "ACK_TIMEOUT_S", 0.2), \
88
+ mock.patch.object(canota, "FLASH_TIMEOUT_S", 0.2):
89
+ ok = CANOTA(parent).start_can_streaming_ota_blocking(
90
+ can_id=11, firmware_path=self.firmware)
91
+ return ok, opened, parent.serial.openDevice
92
+
93
+ def test_master_refusal_restores_without_reset_at_link_baud(self):
94
+ ok, opened, reopen = self.run_upload(
95
+ FakeStreamPort({"ota_status": "error", "error": "slave_unreachable"}))
96
+ self.assertFalse(ok)
97
+ self.assertEqual(opened, [115200]) # streamed at the link's baud
98
+ reopen.assert_called_once_with("/dev/ttyTEST", 115200, reset=False)
99
+
100
+ def test_success_restores_without_reset(self):
101
+ size = len(FIRMWARE)
102
+ port = FakeStreamPort({"ota_status": "ready", "size": size},
103
+ final={"ota_status": "success"})
104
+ ok, _, reopen = self.run_upload(port, parent_baud=921600)
105
+ self.assertTrue(ok)
106
+ reopen.assert_called_once_with("/dev/ttyTEST", 921600, reset=False)
107
+
108
+ def test_silent_master_keeps_the_reset(self):
109
+ ok, _, reopen = self.run_upload(FakeStreamPort(reply=None))
110
+ self.assertFalse(ok)
111
+ reopen.assert_called_once_with("/dev/ttyTEST", 115200, reset=True)
112
+
113
+ def test_missing_acks_keep_the_reset(self):
114
+ size = len(FIRMWARE)
115
+ ok, _, reopen = self.run_upload(
116
+ FakeStreamPort({"ota_status": "ready", "size": size}, ack=False))
117
+ self.assertFalse(ok)
118
+ reopen.assert_called_once_with("/dev/ttyTEST", 115200, reset=True)
119
+
120
+
121
+ if __name__ == "__main__":
122
+ unittest.main()
@@ -0,0 +1,213 @@
1
+ """stagescan payload + completion callback, without hardware.
2
+
3
+ Run: python -m unittest tests.test_stage_scan (from the repo root)
4
+ """
5
+ import unittest
6
+ from unittest import mock
7
+
8
+ from uc2rest.motor import Motor
9
+
10
+
11
+ class _Parent:
12
+ """Just enough of UC2Client for Motor: a serial with register_callback and post_json."""
13
+
14
+ def __init__(self):
15
+ self.serial = mock.Mock()
16
+ self.logger = mock.Mock()
17
+ self.sent = []
18
+
19
+ def post_json(self, path, payload, **kwargs):
20
+ self.sent.append((path, payload))
21
+ return {"qid": 1, "return": 1}
22
+
23
+
24
+ class StageScanPayloadTest(unittest.TestCase):
25
+ def setUp(self):
26
+ self.parent = _Parent()
27
+ self.motor = Motor(parent=self.parent)
28
+ self.motor.stepSizeX, self.motor.stepSizeY, self.motor.stepSizeZ = 2.0, 4.0, 0.5
29
+
30
+ def _scan(self):
31
+ self.assertEqual(len(self.parent.sent), 1)
32
+ path, payload = self.parent.sent[0]
33
+ self.assertEqual(path, "/motor_act")
34
+ self.assertEqual(payload["task"], "/motor_act")
35
+ return payload["stagescan"]
36
+
37
+ def test_positions_are_converted_to_steps(self):
38
+ self.motor.start_stage_scanning(xstart=10, xstep=20, nx=3, ystart=40, ystep=8, ny=2,
39
+ zstart=1, zstep=2, nz=4)
40
+ s = self._scan()
41
+ self.assertEqual((s["xStart"], s["xStep"], s["nX"]), (5, 10, 3))
42
+ self.assertEqual((s["yStart"], s["yStep"], s["nY"]), (10, 2, 2))
43
+ self.assertEqual((s["zStart"], s["zStep"], s["nZ"]), (2, 4, 4))
44
+
45
+ def test_firmware_keys_and_defaults(self):
46
+ self.motor.start_stage_scanning(tsettle=90, tExposure=50)
47
+ s = self._scan()
48
+ self.assertEqual((s["tPre"], s["tPost"]), (90, 50))
49
+ self.assertEqual(s["speed"], 20000)
50
+ self.assertEqual(s["acceleration"], self.motor.DEFAULT_ACCELERATION)
51
+ self.assertNotIn("accel", s) # the firmware reads "acceleration"
52
+ self.assertNotIn("tTrig", s) # firmware default unless requested
53
+ self.assertEqual((s["zicZac"], s["nonstop"]), (1, 0))
54
+
55
+ def test_illumination_keeps_its_channel_index(self):
56
+ self.motor.start_stage_scanning(illumination=[0, 50], led=None)
57
+ s = self._scan()
58
+ self.assertEqual(s["illumination"], [0, 50, 0, 0, 0]) # padded at the end, never shifted
59
+ self.assertEqual(s["led"], 0)
60
+ self.parent.sent.clear()
61
+ self.motor.start_stage_scanning(illumination=(1, 2, 3, 4, 5, 6), led=255, tTrig=2, nonstop=True, zicZac=False)
62
+ s = self._scan()
63
+ self.assertEqual(s["illumination"], [1, 2, 3, 4, 5])
64
+ self.assertEqual((s["led"], s["tTrig"], s["nonstop"], s["zicZac"]), (255, 2, 1, 0))
65
+
66
+ def test_stop_payload(self):
67
+ self.motor.stop_stage_scanning()
68
+ self.assertEqual(self._scan(), {"stopped": 1})
69
+
70
+ def test_completion_callback_sets_flag_and_notifies(self):
71
+ got = []
72
+ self.motor.register_stagescan_callback(got.append)
73
+ self.motor.start_stage_scanning()
74
+ self.assertFalse(self.motor._stagescan_complete)
75
+ self.motor._callback_stagescan_complete({"stagescan": 1, "qid": 7, "success": 1})
76
+ self.assertTrue(self.motor._stagescan_complete)
77
+ self.assertEqual(got, [{"stagescan": 1, "qid": 7, "success": 1}])
78
+ self.motor.unregister_stagescan_callback(got.append)
79
+ self.motor._callback_stagescan_complete({"stagescan": 1, "qid": 8, "success": 1})
80
+ self.assertEqual(len(got), 1)
81
+
82
+
83
+ if __name__ == "__main__":
84
+ unittest.main()
85
+
86
+
87
+ class CameraTriggerNotificationTest(unittest.TestCase):
88
+ """{"cam":1} (old firmware) and {"cam":1,"frame":n} (new) both reach the callback."""
89
+
90
+ def setUp(self):
91
+ from uc2rest.camera_trigger import CameraTrigger
92
+ self.parent = _Parent()
93
+ self.trigger = CameraTrigger(parent=self.parent)
94
+ self.got = []
95
+ self.trigger.register_callback(0, self.got.append)
96
+
97
+ def test_plain_trigger_counts_from_one(self):
98
+ self.trigger._callback_camera_trigger({"cam": 1})
99
+ self.trigger._callback_camera_trigger({"cam": 1})
100
+ self.assertEqual([g["frame_id"] for g in self.got], [1, 2])
101
+ self.assertNotIn("frame", self.got[0])
102
+
103
+ def test_numbered_trigger_passes_firmware_frame_through(self):
104
+ self.trigger._callback_camera_trigger({"cam": 1, "frame": 0})
105
+ self.trigger._callback_camera_trigger({"cam": 1, "frame": 5})
106
+ self.assertEqual([g["frame"] for g in self.got], [0, 5])
107
+ self.assertEqual([g["frame_id"] for g in self.got], [1, 2])
108
+
109
+
110
+ class StrobeSweepTest(unittest.TestCase):
111
+ """Strobed sweep payloads, unit conversion and event parsing."""
112
+
113
+ def setUp(self):
114
+ self.parent = _Parent()
115
+ self.motor = Motor(parent=self.parent)
116
+ self.motor.stepSizeX = 0.5
117
+ self.motor.direction[1] = -1
118
+
119
+ def _sweep(self):
120
+ path, payload = self.parent.sent[-1]
121
+ self.assertEqual(path, "/motor_act")
122
+ return payload["strobesweep"]
123
+
124
+ def test_registers_pattern_callbacks(self):
125
+ patterns = [c.kwargs.get("pattern") for c in self.parent.serial.register_callback.call_args_list]
126
+ self.assertIn("strobesweep", patterns)
127
+ self.assertIn("modules", patterns)
128
+
129
+ def test_start_payload_converts_target_to_steps(self):
130
+ self.motor.start_strobe_sweep(axis="X", target=1000.0, speed=4000, period_us=20000,
131
+ laser=4, delay_us=1200, width_us=20, report=8)
132
+ s = self._sweep()
133
+ self.assertEqual(s["axis"], 1)
134
+ self.assertEqual(s["target"], -2000) # 1000 µm / 0.5 µm/step, direction -1
135
+ self.assertEqual((s["speed"], s["periodUs"], s["trigUs"]), (4000, 20000, 100))
136
+ self.assertEqual((s["laser"], s["delayUs"], s["widthUs"]), (4, 1200, 20))
137
+ self.assertEqual((s["latch"], s["report"], s["maxFrames"]), (1, 8, 0))
138
+ self.assertNotIn("acceleration", s)
139
+
140
+ def test_strobe_keys_only_when_both_given(self):
141
+ self.motor.start_strobe_sweep(axis="X", target=0, laser=4, delay_us=1200)
142
+ s = self._sweep()
143
+ self.assertNotIn("delayUs", s)
144
+ self.assertNotIn("widthUs", s)
145
+
146
+ def test_relative_target_and_calibration_mode(self):
147
+ self.motor.currentPosition[1] = 250.0
148
+ self.motor.start_strobe_sweep(axis=1, target=-50.0, is_absolute=False, max_frames=12, latch=False)
149
+ s = self._sweep()
150
+ self.assertEqual(s["target"], -400) # (250 - 50) / 0.5 * -1
151
+ self.assertEqual((s["maxFrames"], s["latch"], s["laser"]), (12, 0, -1))
152
+
153
+ def test_stop_payload(self):
154
+ self.motor.stop_strobe_sweep()
155
+ self.assertEqual(self._sweep(), {"stopped": 1})
156
+
157
+ def test_report_and_done_events(self):
158
+ events = []
159
+ self.motor.register_strobesweep_callback(events.append)
160
+ self.motor.start_strobe_sweep(axis="X", target=10)
161
+ self.assertTrue(self.motor.is_strobe_sweep_running())
162
+ self.motor._callback_strobesweep({"strobesweep": {"n": [1, 2, 4], "x": [100, 102, 106]}})
163
+ self.motor._callback_strobesweep({"strobesweep": True, "frames": 4, "camera": 4, "flashes": 4,
164
+ "positions": 3, "latch": 1, "strobe": 1, "aborted": 0,
165
+ "success": 1, "qid": 5})
166
+ self.assertEqual(events[0], {"type": "report", "n": [1, 2, 4], "x": [-50.0, -51.0, -53.0],
167
+ "x_steps": [100, 102, 106]})
168
+ self.assertEqual(events[1]["type"], "done")
169
+ self.assertEqual((events[1]["frames"], events[1]["success"]), (4, 1))
170
+ self.assertFalse(self.motor.is_strobe_sweep_running())
171
+ self.motor.unregister_strobesweep_callback(events.append)
172
+ self.motor._callback_strobesweep({"strobesweep": {"n": [5], "x": [1]}})
173
+ self.assertEqual(len(events), 2)
174
+
175
+ def test_capability_probe(self):
176
+ # new firmware answers /modules_get with the flag; the answer arrives by pattern
177
+ def answer(path, payload, **kwargs):
178
+ self.parent.sent.append((path, payload))
179
+ self.motor._callback_modules({"modules": {"motor": 1, "strobesweep": 1}})
180
+ self.parent.post_json = answer
181
+ self.assertTrue(self.motor.has_strobe_sweep(timeout=0.2))
182
+ self.assertEqual(self.parent.sent[-1][1]["task"], "/modules_get")
183
+
184
+ def test_capability_probe_old_firmware(self):
185
+ def old(path, payload, **kwargs):
186
+ self.motor._callback_modules({"modules": {"motor": 1}})
187
+ self.parent.post_json = old
188
+ self.assertFalse(self.motor.has_strobe_sweep(timeout=0.2))
189
+ self.parent.post_json = lambda *a, **k: None # no answer at all
190
+ self.assertFalse(self.motor.has_strobe_sweep(timeout=0.05))
191
+
192
+
193
+ class LaserStrobeTest(unittest.TestCase):
194
+ def setUp(self):
195
+ from uc2rest.laser import Laser
196
+ self.parent = _Parent()
197
+ self.laser = Laser(parent=self.parent)
198
+
199
+ def test_payload_and_answer(self):
200
+ self.parent.post_json = lambda path, payload, **kw: (
201
+ self.parent.sent.append((path, payload)) or
202
+ [{"strobe": {"supported": 1, "enabled": 1, "delayUs": 1200, "widthUs": 20, "count": 0},
203
+ "return": 1, "qid": 3}])
204
+ r = self.laser.set_strobe(4, enable=True, delay_us=1200, width_us=20)
205
+ path, payload = self.parent.sent[-1]
206
+ self.assertEqual(path, "/laser_act")
207
+ self.assertEqual(payload["LASERid"], 4)
208
+ self.assertEqual(payload["strobe"], {"enable": 1, "delayUs": 1200, "widthUs": 20})
209
+ self.assertEqual(r["strobe"]["enabled"], 1)
210
+
211
+ def test_old_firmware_returns_none(self):
212
+ self.parent.post_json = lambda *a, **k: "communication interrupted by timeout or reset: 3"
213
+ self.assertIsNone(self.laser.set_strobe(4, enable=False))
@@ -6,7 +6,7 @@ __version__.py
6
6
 
7
7
  __title__ = 'UC2-REST'
8
8
  __description__ = 'This pacage will help you to drive the ESP32-driven microscopy control modules from UC2'
9
- __version__ = "v0.2.0.39"
9
+ __version__ = "v0.2.0.41"
10
10
  __author__ = 'Benedict Diederich'
11
11
  __author_email__ = 'benedictdied@gmail.com'
12
12
  __license__ = 'GPL v3'