UC2-REST 0.2.0.37__tar.gz → 0.2.0.39__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 (47) hide show
  1. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/PKG-INFO +1 -1
  2. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/UC2_REST.egg-info/PKG-INFO +1 -1
  3. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/UC2_REST.egg-info/SOURCES.txt +3 -0
  4. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/UC2Client.py +13 -1
  5. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/__version__.py +1 -1
  6. uc2_rest-0.2.0.39/uc2rest/gpio.py +171 -0
  7. uc2_rest-0.2.0.39/uc2rest/i2c.py +190 -0
  8. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/motor.py +1 -1
  9. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/mserial.py +8 -5
  10. uc2_rest-0.2.0.39/uc2rest/ptz.py +156 -0
  11. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/LICENSE +0 -0
  12. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/README.md +0 -0
  13. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/UC2_REST.egg-info/dependency_links.txt +0 -0
  14. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/UC2_REST.egg-info/not-zip-safe +0 -0
  15. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/UC2_REST.egg-info/requires.txt +0 -0
  16. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/UC2_REST.egg-info/top_level.txt +0 -0
  17. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/setup.cfg +0 -0
  18. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/setup.py +0 -0
  19. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/MockSerial.py +0 -0
  20. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/__init__.py +0 -0
  21. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/analog.py +0 -0
  22. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/camera.py +0 -0
  23. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/camera_trigger.py +0 -0
  24. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/can.py +0 -0
  25. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/canota.py +0 -0
  26. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/cmdrecorder.py +0 -0
  27. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/digitalin.py +0 -0
  28. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/digitalout.py +0 -0
  29. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/fan.py +0 -0
  30. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/galvo.py +0 -0
  31. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/gripper.py +0 -0
  32. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/home.py +0 -0
  33. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/laser.py +0 -0
  34. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/lcddisplay.py +0 -0
  35. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/ledmatrix.py +0 -0
  36. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/logger.py +0 -0
  37. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/message.py +0 -0
  38. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/modules.py +0 -0
  39. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/motor_config.py +0 -0
  40. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/objective.py +0 -0
  41. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/pid.py +0 -0
  42. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/rotator.py +0 -0
  43. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/slm.py +0 -0
  44. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/state.py +0 -0
  45. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/temperature.py +0 -0
  46. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/utils.py +0 -0
  47. {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/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.37
3
+ Version: 0.2.0.39
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.37
3
+ Version: 0.2.0.39
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
@@ -21,8 +21,10 @@ uc2rest/digitalin.py
21
21
  uc2rest/digitalout.py
22
22
  uc2rest/fan.py
23
23
  uc2rest/galvo.py
24
+ uc2rest/gpio.py
24
25
  uc2rest/gripper.py
25
26
  uc2rest/home.py
27
+ uc2rest/i2c.py
26
28
  uc2rest/laser.py
27
29
  uc2rest/lcddisplay.py
28
30
  uc2rest/ledmatrix.py
@@ -34,6 +36,7 @@ uc2rest/motor_config.py
34
36
  uc2rest/mserial.py
35
37
  uc2rest/objective.py
36
38
  uc2rest/pid.py
39
+ uc2rest/ptz.py
37
40
  uc2rest/rotator.py
38
41
  uc2rest/slm.py
39
42
  uc2rest/state.py
@@ -22,6 +22,9 @@ from .analog import Analog
22
22
  from .modules import Modules
23
23
  from .digitalout import DigitalOut
24
24
  from .digitalin import DigitalIn
25
+ from .gpio import GPIO
26
+ from .ptz import PTZ
27
+ from .i2c import I2C
25
28
  from .rotator import Rotator
26
29
  from .logger import Logger
27
30
  from .cmdrecorder import cmdRecorder
@@ -155,7 +158,16 @@ class UC2Client(object):
155
158
 
156
159
  # initialize digital in
157
160
  self.digitalin = DigitalIn(self)
158
-
161
+
162
+ # initialize GPIO / collision-detector slave interface
163
+ self.gpio = GPIO(self)
164
+
165
+ # initialize PTZ keyboard bridge interface (CAN node-id 61)
166
+ self.ptz = PTZ(self)
167
+
168
+ # initialize generic I2C passthrough (sensors on the GPIO slave bus)
169
+ self.i2c = I2C(self)
170
+
159
171
  # initialize messaging
160
172
  self.message = Message(self)
161
173
 
@@ -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.37"
9
+ __version__ = "v0.2.0.39"
10
10
  __author__ = 'Benedict Diederich'
11
11
  __author_email__ = 'benedictdied@gmail.com'
12
12
  __license__ = 'GPL v3'
@@ -0,0 +1,171 @@
1
+ """GPIO / collision-detector interface for the UC2 CAN GPIO slave.
2
+
3
+ The CAN master exposes the GPIO slave (XIAO node, default CAN node-id 60)
4
+ through two serial endpoints:
5
+
6
+ {"task":"/gpio_get"}
7
+ -> {"gpio":{"node":60,"mean":585,"filtered":584,"raw":581,
8
+ "reference":585,"threshold":150,"sensitivity":4,
9
+ "trip":0,"estop":0},"ok":true,"qid":N}
10
+
11
+ {"task":"/gpio_act","threshold":150,"sensitivity":4,
12
+ "reference":585,"calibrate":1} (all value fields optional)
13
+
14
+ Collision detection runs ON THE SLAVE: the sensor has an idle value (the
15
+ "reference"); a collision = `sensitivity` consecutive samples deviating more
16
+ than `threshold` ADC counts from the reference (rising OR falling). Sensor
17
+ values are never streamed on the CAN bus — polling /gpio_get triggers SDO
18
+ reads on demand. Only the collision/E-stop EVENT is pushed asynchronously;
19
+ the master then prints:
20
+
21
+ {"gpio":{"event":1,"node":60,"trip":1,"estop":0,"flags":3,
22
+ "filtered":2365,"raw":2937,...}}
23
+
24
+ The "event":1 marker distinguishes pushed edges from /gpio_get responses
25
+ (both share the top-level "gpio" key and therefore both arrive at the same
26
+ serial callback).
27
+ """
28
+
29
+
30
+ class GPIO(object):
31
+ def __init__(self, parent):
32
+ self._parent = parent
33
+
34
+ # last known state (merged from get_status responses and async events)
35
+ self.status = {}
36
+
37
+ # register a callback for any {"gpio":{...}} frame on the serial loop
38
+ if hasattr(self._parent, "serial"):
39
+ self._parent.serial.register_callback(self._callback_gpio, pattern="gpio")
40
+
41
+ # user-registered callbacks
42
+ self._collision_callbacks = [] # fired on async trip/clear events
43
+ self._status_callbacks = [] # fired on every gpio frame
44
+
45
+ # ─────────────────────────────────────────────────────────────────
46
+ # Async event plumbing
47
+ # ─────────────────────────────────────────────────────────────────
48
+ def _callback_gpio(self, data):
49
+ """Handle any serial frame with a top-level "gpio" key.
50
+
51
+ Pushed events carry "event":1 and fire the collision callbacks;
52
+ /gpio_get responses only refresh the cached status."""
53
+ try:
54
+ gpio = data.get("gpio", None)
55
+ if not isinstance(gpio, dict):
56
+ return
57
+ self.status.update(gpio)
58
+
59
+ for cb in self._status_callbacks:
60
+ try:
61
+ cb(dict(self.status))
62
+ except Exception as e:
63
+ print(f"Error in gpio status callback: {e}")
64
+
65
+ if gpio.get("event", 0):
66
+ for cb in self._collision_callbacks:
67
+ try:
68
+ cb(dict(gpio))
69
+ except Exception as e:
70
+ print(f"Error in gpio collision callback: {e}")
71
+ except Exception as e:
72
+ print("Error in _callback_gpio: ", e)
73
+
74
+ def register_collision_callback(self, callbackfct):
75
+ """Register a function called on every asynchronously pushed GPIO
76
+ event (collision trip/clear, E-stop press/release). The callback
77
+ receives the event dict, e.g. {"event":1,"trip":1,"estop":0,...}.
78
+ Check ["trip"] to distinguish collision-began from collision-cleared."""
79
+ if callbackfct not in self._collision_callbacks:
80
+ self._collision_callbacks.append(callbackfct)
81
+
82
+ def register_status_callback(self, callbackfct):
83
+ """Register a function called on EVERY gpio frame (async events and
84
+ /gpio_get responses alike) with the merged status dict."""
85
+ if callbackfct not in self._status_callbacks:
86
+ self._status_callbacks.append(callbackfct)
87
+
88
+ # ─────────────────────────────────────────────────────────────────
89
+ # Query / configuration
90
+ # ─────────────────────────────────────────────────────────────────
91
+ @staticmethod
92
+ def _extract_gpio(response):
93
+ """Unwrap a post_json return into the "gpio" dict.
94
+
95
+ mserial.sendMessage returns a LIST of response dicts on success
96
+ (self.responses[qid] is a per-frame list), a plain string on
97
+ timeout/interrupt, or an int qid when getReturn=False — so handle
98
+ all shapes defensively."""
99
+ candidates = response if isinstance(response, list) else [response]
100
+ for item in candidates:
101
+ if isinstance(item, dict):
102
+ gpio = item.get("gpio")
103
+ if isinstance(gpio, dict):
104
+ return gpio
105
+ return {}
106
+
107
+ def get_status(self, node=None, timeout=1):
108
+ """Poll the collision detector (SDO reads on the CAN bus).
109
+
110
+ Returns the "gpio" dict: mean (rolling average), filtered, raw,
111
+ baseline, sigma, mode (0=auto/1=manual), reference, threshold,
112
+ sensitivity, trip, estop."""
113
+ path = "/gpio_get"
114
+ payload = {"task": path}
115
+ if node is not None:
116
+ payload["node"] = node
117
+ r = self._parent.post_json(path, payload, getReturn=True, timeout=timeout)
118
+ gpio = self._extract_gpio(r)
119
+ if gpio:
120
+ self.status.update(gpio)
121
+ return gpio
122
+ # Response didn't parse (timeout/odd shape) — fall back to the cache
123
+ # maintained by the async serial callback, which sees every frame.
124
+ return dict(self.status)
125
+
126
+ def _act(self, node=None, timeout=1, **fields):
127
+ path = "/gpio_act"
128
+ payload = {"task": path}
129
+ if node is not None:
130
+ payload["node"] = node
131
+ payload.update(fields)
132
+ return self._parent.post_json(path, payload, getReturn=True, timeout=timeout)
133
+
134
+ def set_threshold(self, threshold, node=None, timeout=1):
135
+ """Deviation band (ADC counts) around the reference outside which a
136
+ sample votes for "collision". Persisted in the slave's NVS."""
137
+ return self._act(node=node, timeout=timeout, threshold=int(threshold))
138
+
139
+ def set_sensitivity(self, sensitivity, node=None, timeout=1):
140
+ """Number of CONSECUTIVE out-of-band samples (at 50 Hz) required to
141
+ trip — and in-band samples to clear. 3-4 rejects single-sample spikes
142
+ while confirming a real collision within ~60-80 ms."""
143
+ return self._act(node=node, timeout=timeout, sensitivity=int(sensitivity))
144
+
145
+ def set_reference(self, reference, node=None, timeout=1):
146
+ """Explicitly set the idle baseline (ADC counts). Typically you poll
147
+ get_status()["mean"] while the system is idle and write that value
148
+ here — or simply use calibrate()."""
149
+ return self._act(node=node, timeout=timeout, reference=int(reference))
150
+
151
+ def calibrate(self, node=None, timeout=1):
152
+ """Tell the slave to take its CURRENT rolling mean as the new
153
+ reference (persisted in NVS). MANUAL mode only — in AUTO the baseline
154
+ tracks itself. Do this while the system is idle and collision-free."""
155
+ return self._act(node=node, timeout=timeout, calibrate=1)
156
+
157
+ def set_mode(self, mode, node=None, timeout=1):
158
+ """Select the detection mode (persisted on the slave in NVS).
159
+
160
+ mode: "auto" (default, recommended) — adaptive baseline + robust
161
+ sigma z-score. Parameter-free: tracks a slowly drifting
162
+ background and trips on a fast deflection. No calibration.
163
+ mode: "manual" — fixed reference +/- threshold. Deterministic;
164
+ requires the reference to be calibrated to the idle level.
165
+
166
+ Accepts "auto"/"manual" or 0/1."""
167
+ if isinstance(mode, str):
168
+ mval = 1 if mode.lower() == "manual" else 0
169
+ else:
170
+ mval = 1 if int(mode) else 0
171
+ return self._act(node=node, timeout=timeout, mode=mval)
@@ -0,0 +1,190 @@
1
+ """Generic raw-I2C passthrough for the UC2 CAN GPIO slave.
2
+
3
+ Any I2C device wired to the GPIO slave's sensor bus (SDA=GPIO5/D4,
4
+ SCL=GPIO6/D5 on the can-gpio board) can be read/written from Python WITHOUT
5
+ a device driver on the ESP32. The firmware just relays bytes; the register map
6
+ lives here. One JSON request = one raw transaction (optional write, optional
7
+ inter-phase delay, optional read):
8
+
9
+ {"task":"/i2c_act","node":60,"addr":68,"write":[253],"read":6,
10
+ "delay":12,"stop":1}
11
+ -> {"node":60,"status":2,"data":[.. up to 40 bytes ..],"ok":true}
12
+
13
+ addr : 7-bit I2C address (decimal; 0x44 == 68)
14
+ write : bytes to send first (register pointer / command / config)
15
+ read : number of bytes to read back (0 = write-only)
16
+ delay : ms to wait between the write and the read (device conversion time)
17
+ stop : 1 → send STOP after the write (device wants a full stop before the
18
+ read, e.g. SHT4x); 0/omitted → repeated-START (typical register read)
19
+
20
+ Status byte: 2 = ok, 0x81..0x85 = write failed (I2C NACK / bus error),
21
+ 0x90 = fewer bytes read than requested, 0xA0 = bad parameters.
22
+
23
+ See uc2-ESP: main/src/i2c/I2cBridge.cpp and DeviceRouter::handleI2cAct.
24
+ """
25
+
26
+ import time
27
+
28
+
29
+ class I2C(object):
30
+ STATUS_OK = 0x02
31
+
32
+ def __init__(self, parent):
33
+ self._parent = parent
34
+
35
+ # ─────────────────────────────────────────────────────────────────
36
+ # Generic transaction
37
+ # ─────────────────────────────────────────────────────────────────
38
+ @staticmethod
39
+ def _extract(response):
40
+ """Unwrap a post_json return into the response dict carrying "data".
41
+
42
+ mserial.sendMessage returns a LIST of frames on success, a plain
43
+ string on timeout, or an int qid when getReturn=False — handle all."""
44
+ candidates = response if isinstance(response, list) else [response]
45
+ for item in candidates:
46
+ if isinstance(item, dict) and ("data" in item or "status" in item):
47
+ return item
48
+ return {}
49
+
50
+ def transaction(self, addr, write=None, read=0, delay_ms=0, stop=False,
51
+ node=None, timeout=2):
52
+ """Run one raw I2C transaction on the slave and return the response
53
+ dict {"status":int, "data":[...], ...}. `write` is an iterable of
54
+ byte values; `read` is how many bytes to read back."""
55
+ payload = {"task": "/i2c_act",
56
+ "addr": int(addr) & 0x7F,
57
+ "read": int(read),
58
+ "delay": int(delay_ms)}
59
+ if write:
60
+ payload["write"] = [int(b) & 0xFF for b in write]
61
+ if stop:
62
+ payload["stop"] = 1
63
+ if node is not None:
64
+ payload["node"] = node
65
+ r = self._parent.post_json("/i2c_act", payload, getReturn=True,
66
+ timeout=timeout)
67
+ return self._extract(r)
68
+
69
+ def ok(self, response):
70
+ """True if a transaction response reports success."""
71
+ return isinstance(response, dict) and response.get("status") == self.STATUS_OK
72
+
73
+ # ─────────────────────────────────────────────────────────────────
74
+ # Register-level convenience wrappers
75
+ # ─────────────────────────────────────────────────────────────────
76
+ def read_register(self, addr, reg, length, node=None,
77
+ repeated_start=True, timeout=2):
78
+ """Classic register read: write the register pointer, then read
79
+ `length` bytes (repeated-START by default). Returns a bytes object
80
+ (empty on failure)."""
81
+ res = self.transaction(addr, write=[reg], read=length,
82
+ stop=not repeated_start, node=node, timeout=timeout)
83
+ return bytes(res.get("data", []) or [])
84
+
85
+ def write_register(self, addr, reg, values, node=None, timeout=2):
86
+ """Write `values` (int or iterable) to register `reg`. Returns True on
87
+ an acknowledged write."""
88
+ if isinstance(values, int):
89
+ values = [values]
90
+ res = self.transaction(addr, write=[reg] + [int(v) & 0xFF for v in values],
91
+ read=0, stop=True, node=node, timeout=timeout)
92
+ return self.ok(res)
93
+
94
+ def scan(self, node=None, start=0x08, end=0x77, timeout=2):
95
+ """Probe the bus for responding devices (0-byte write per address).
96
+ Returns a list of 7-bit addresses that ACKed."""
97
+ found = []
98
+ for a in range(start, end + 1):
99
+ # zero-length write: beginTransmission + endTransmission(true).
100
+ res = self.transaction(a, write=[], read=0, stop=True,
101
+ node=node, timeout=timeout)
102
+ if self.ok(res):
103
+ found.append(a)
104
+ return found
105
+
106
+ # ─────────────────────────────────────────────────────────────────
107
+ # Adafruit SHT45 — humidity + temperature (addr 0x44)
108
+ # ─────────────────────────────────────────────────────────────────
109
+ @staticmethod
110
+ def _sht_crc(data):
111
+ """Sensirion CRC-8: poly 0x31, init 0xFF."""
112
+ crc = 0xFF
113
+ for b in data:
114
+ crc ^= b
115
+ for _ in range(8):
116
+ crc = ((crc << 1) ^ 0x31) & 0xFF if (crc & 0x80) else (crc << 1) & 0xFF
117
+ return crc
118
+
119
+ def read_sht45(self, addr=0x44, node=None, precision="high", timeout=2):
120
+ """Read the SHT45. Sends the measure command, waits for the
121
+ conversion, reads 6 bytes (T, T_crc, RH, RH_crc) and converts.
122
+
123
+ precision: "high" (0xFD, ~9 ms), "med" (0xF6), "low" (0xE0).
124
+ Returns {"temperature_c", "humidity_pct", "crc_ok"} or None on error."""
125
+ cmd = {"high": 0xFD, "med": 0xF6, "low": 0xE0}.get(precision, 0xFD)
126
+ res = self.transaction(addr, write=[cmd], read=6, delay_ms=12,
127
+ stop=True, node=node, timeout=timeout)
128
+ data = res.get("data", []) or []
129
+ if len(data) < 6:
130
+ return None
131
+ t_ticks = (data[0] << 8) | data[1]
132
+ rh_ticks = (data[3] << 8) | data[4]
133
+ temp_c = -45.0 + 175.0 * t_ticks / 65535.0
134
+ rh = -6.0 + 125.0 * rh_ticks / 65535.0
135
+ rh = max(0.0, min(100.0, rh))
136
+ crc_ok = (self._sht_crc(data[0:2]) == data[2] and
137
+ self._sht_crc(data[3:5]) == data[5])
138
+ return {"temperature_c": temp_c, "humidity_pct": rh, "crc_ok": crc_ok}
139
+
140
+ # ─────────────────────────────────────────────────────────────────
141
+ # Adafruit TSL2591 — high-dynamic-range light sensor (addr 0x29)
142
+ # ─────────────────────────────────────────────────────────────────
143
+ # Command byte: 0xA0 = normal register access; low 5 bits = register addr.
144
+ _TSL_CMD = 0xA0
145
+ _TSL_ENABLE = 0x00 # PON(0x01) | AEN(0x02) | (AIEN/NPIEN optional)
146
+ _TSL_CONTROL = 0x01 # AGAIN[5:4] | ATIME[2:0]
147
+ _TSL_C0DATAL = 0x14 # CH0 low; auto-increments through 0x17 (CH1 high)
148
+ _TSL_GAIN = {0x00: 1.0, 0x10: 25.0, 0x20: 428.0, 0x30: 9876.0}
149
+
150
+ def read_tsl2591(self, addr=0x29, node=None, gain=0x10, integration=0x01,
151
+ timeout=2):
152
+ """Read the TSL2591 broadband (CH0) and IR (CH1) channels and estimate
153
+ lux. Powers the ALS on, applies gain/integration, waits one integration
154
+ period, then reads the four data bytes.
155
+
156
+ gain: 0x00=1x, 0x10=25x (default), 0x20=428x, 0x30=9876x
157
+ integration: 0x00=100ms .. 0x05=600ms (default 0x01 = 200ms)
158
+ Returns {"ch0_full","ch1_ir","visible","gain","integration_ms",
159
+ "lux","saturated"} or None on error."""
160
+ # Enable ALS (PON | AEN).
161
+ if not self.write_register(addr, self._TSL_CMD | self._TSL_ENABLE, 0x03,
162
+ node=node, timeout=timeout):
163
+ return None
164
+ # Set gain + integration time.
165
+ self.write_register(addr, self._TSL_CMD | self._TSL_CONTROL,
166
+ (gain & 0x30) | (integration & 0x07),
167
+ node=node, timeout=timeout)
168
+ atime_ms = 100 * ((integration & 0x07) + 1)
169
+ # Wait one full integration period (Python-side so we aren't limited to
170
+ # the firmware's 255 ms per-transaction delay cap). Small margin added.
171
+ time.sleep(atime_ms / 1000.0 + 0.05)
172
+ # Burst-read CH0/CH1 (4 bytes, auto-increment) via repeated-START.
173
+ res = self.transaction(addr, write=[self._TSL_CMD | self._TSL_C0DATAL],
174
+ read=4, stop=False, node=node, timeout=timeout)
175
+ data = res.get("data", []) or []
176
+ if len(data) < 4:
177
+ return None
178
+ ch0 = data[0] | (data[1] << 8) # full spectrum
179
+ ch1 = data[2] | (data[3] << 8) # IR
180
+ saturated = ch0 >= 0xFFFF or ch1 >= 0xFFFF
181
+
182
+ # Approximate lux (standard TSL2591 formula; device lot variation ~±).
183
+ again = self._TSL_GAIN.get(gain & 0x30, 1.0)
184
+ lux = None
185
+ if ch0 > 0 and not saturated:
186
+ cpl = (atime_ms * again) / 408.0 # counts-per-lux, DF = 408
187
+ lux = max(0.0, (ch0 - ch1) * (1.0 - float(ch1) / float(ch0)) / cpl)
188
+ return {"ch0_full": ch0, "ch1_ir": ch1, "visible": ch0 - ch1,
189
+ "gain": gain, "integration_ms": atime_ms,
190
+ "lux": lux, "saturated": saturated}
@@ -1098,7 +1098,7 @@ class Motor(object):
1098
1098
  '''
1099
1099
  if type(axis) != int:
1100
1100
  axis = self.xyztTo1230(axis)
1101
-
1101
+ # {"task":"/motor_act", "joystickdir": {"steppers": [{"stepperid": 1, "inverted": 1}]}}
1102
1102
  path = "/motor_act"
1103
1103
  payload = {
1104
1104
  "task": path,
@@ -364,11 +364,14 @@ class Serial:
364
364
  for i in range(500):
365
365
  # if we just want to send but not even wait for a response
366
366
  mReadline = self._read(ser)
367
- if self.DEBUG and mReadline != "" and mReadline != "\n" and mReadline != b'' and mReadline != b'\n':
368
- self._logger.debug("[checkFirmware]: "+str(mReadline))
369
- if mReadline.decode('utf-8').strip() == "++" or mReadline.decode('utf-8').strip().find("++")>=0:
370
- self._freeSerialBuffer(ser)
371
- return True
367
+ try:
368
+ if self.DEBUG and mReadline != "" and mReadline != "\n" and mReadline != b'' and mReadline != b'\n':
369
+ self._logger.debug("[checkFirmware]: "+str(mReadline))
370
+ if mReadline.decode('utf-8').strip() == "++" or mReadline.decode('utf-8').strip().find("++")>=0:
371
+ self._freeSerialBuffer(ser)
372
+ return True
373
+ except Exception as e:
374
+ self._logger.error("Error in checkFirmware: "+str(e))
372
375
  return False
373
376
 
374
377
  def _generate_identifier(self):
@@ -0,0 +1,156 @@
1
+ """PTZ keyboard interface for the UC2 CAN PTZ-bridge slave.
2
+
3
+ A low-cost CCTV PTZ joystick keyboard (Pelco-D/P over RS-485) is read by a
4
+ dedicated CAN bridge node (XIAO, default CAN node-id 61). The bridge maps the
5
+ joystick to motor moves directly on the bus, and forwards every DISCRETE key
6
+ (presets, AUX, iris) to the master, which prints an asynchronous frame:
7
+
8
+ {"ptz":{"event":1,"node":61,"type":6,"name":"iris_open","arg":0,"seq":21}}
9
+
10
+ type/name: 1 preset_call 2 preset_set 3 preset_clear
11
+ 4 aux_on 5 aux_off 6 iris_open
12
+ 7 iris_close 8 camera_onoff 9 autoscan
13
+ arg: preset number (1..255) or AUX number (1..6); 0 otherwise
14
+ seq: wraps 0..255, one per key event (the master de-dupes on it)
15
+
16
+ This mirrors the collision "gpio" event plumbing (see gpio.py): a single
17
+ serial callback on the top-level "ptz" key, an "event":1 marker to tell a
18
+ pushed key from a /ptz_get response, and a list of user callbacks. ImSwitch
19
+ registers one such callback and maps each key to a microscope function
20
+ (snap image, switch objective, toggle an LED …) — see UC2ConfigController.
21
+
22
+ Debug / config endpoints (handled locally on the bridge node):
23
+ {"task":"/ptz_act","debug":2} raw RS-485 hex dump (wiring bring-up)
24
+ {"task":"/ptz_act","debug":1} decoded frame JSON
25
+ {"task":"/ptz_act","addr":3} accept only camera address 3 (0=any)
26
+ {"task":"/ptz_act","timeout":2000} motion watchdog ms (0=off)
27
+ {"task":"/ptz_get"} parser stats + last frame + motion
28
+ """
29
+
30
+
31
+ class PTZ(object):
32
+ def __init__(self, parent):
33
+ self._parent = parent
34
+
35
+ # last known state (merged from /ptz_get responses and async events)
36
+ self.status = {}
37
+ # last discrete key event pushed by the bridge
38
+ self.last_event = {}
39
+
40
+ # register a callback for any {"ptz":{...}} frame on the serial loop
41
+ if hasattr(self._parent, "serial"):
42
+ self._parent.serial.register_callback(self._callback_ptz, pattern="ptz")
43
+
44
+ # user-registered callbacks
45
+ self._event_callbacks = [] # fired on async key events (event==1)
46
+ self._status_callbacks = [] # fired on every ptz frame
47
+
48
+ # ─────────────────────────────────────────────────────────────────
49
+ # Async event plumbing
50
+ # ─────────────────────────────────────────────────────────────────
51
+ def _callback_ptz(self, data):
52
+ """Handle any serial frame with a top-level "ptz" key.
53
+
54
+ Pushed key events carry "event":1 and fire the event callbacks;
55
+ /ptz_get responses only refresh the cached status."""
56
+ try:
57
+ ptz = data.get("ptz", None)
58
+ if not isinstance(ptz, dict):
59
+ return
60
+ self.status.update(ptz)
61
+
62
+ for cb in self._status_callbacks:
63
+ try:
64
+ cb(dict(self.status))
65
+ except Exception as e:
66
+ print(f"Error in ptz status callback: {e}")
67
+
68
+ if ptz.get("event", 0):
69
+ self.last_event = dict(ptz)
70
+ for cb in self._event_callbacks:
71
+ try:
72
+ cb(dict(ptz))
73
+ except Exception as e:
74
+ print(f"Error in ptz event callback: {e}")
75
+ except Exception as e:
76
+ print("Error in _callback_ptz: ", e)
77
+
78
+ def register_event_callback(self, callbackfct):
79
+ """Register a function called on every asynchronously pushed PTZ key
80
+ event (preset call/set/clear, AUX on/off, iris, …). The callback
81
+ receives the event dict, e.g.
82
+ {"event":1,"node":61,"type":1,"name":"preset_call","arg":1,"seq":7}.
83
+ Use ["name"] and ["arg"] to decide which microscope function to run."""
84
+ if callbackfct not in self._event_callbacks:
85
+ self._event_callbacks.append(callbackfct)
86
+
87
+ def register_status_callback(self, callbackfct):
88
+ """Register a function called on EVERY ptz frame (async key events and
89
+ /ptz_get responses alike) with the merged status dict."""
90
+ if callbackfct not in self._status_callbacks:
91
+ self._status_callbacks.append(callbackfct)
92
+
93
+ # ─────────────────────────────────────────────────────────────────
94
+ # Query / configuration
95
+ # ─────────────────────────────────────────────────────────────────
96
+ @staticmethod
97
+ def _extract_ptz(response):
98
+ """Unwrap a post_json return into the "ptz" dict.
99
+
100
+ mserial.sendMessage returns a LIST of response dicts on success, a
101
+ plain string on timeout, or an int qid — handle all shapes (same
102
+ defensive pattern as GPIO._extract_gpio)."""
103
+ candidates = response if isinstance(response, list) else [response]
104
+ for item in candidates:
105
+ if isinstance(item, dict):
106
+ ptz = item.get("ptz")
107
+ if isinstance(ptz, dict):
108
+ return ptz
109
+ return {}
110
+
111
+ def get_status(self, node=None, timeout=1):
112
+ """Poll the bridge: parser stats (bytesIn/framesOk/resyncs), the last
113
+ decoded frame and the current motion snapshot. Returns the "ptz" dict.
114
+ Note: discrete key events are PUSHED (see register_event_callback);
115
+ polling is only for diagnostics."""
116
+ path = "/ptz_get"
117
+ payload = {"task": path}
118
+ if node is not None:
119
+ payload["node"] = node
120
+ r = self._parent.post_json(path, payload, getReturn=True, timeout=timeout)
121
+ ptz = self._extract_ptz(r)
122
+ if ptz:
123
+ self.status.update(ptz)
124
+ return ptz
125
+ return dict(self.status)
126
+
127
+ def _act(self, node=None, timeout=1, **fields):
128
+ path = "/ptz_act"
129
+ payload = {"task": path}
130
+ if node is not None:
131
+ payload["node"] = node
132
+ payload.update(fields)
133
+ return self._parent.post_json(path, payload, getReturn=True, timeout=timeout)
134
+
135
+ def set_debug(self, level, node=None, timeout=1):
136
+ """Bridge serial debug verbosity: 0 quiet, 1 decoded frame JSON,
137
+ 2 + raw RS-485 hex dump (use for wiring / baud / A-B-swap bring-up)."""
138
+ return self._act(node=node, timeout=timeout, debug=int(level))
139
+
140
+ def set_address(self, addr, node=None, timeout=1):
141
+ """Only accept keyboard frames for this Pelco camera address
142
+ (0 = accept any). Handy when several bridges share one keyboard."""
143
+ return self._act(node=node, timeout=timeout, addr=int(addr))
144
+
145
+ def set_motion_timeout(self, timeout_ms, node=None, serial_timeout=1):
146
+ """Watchdog (ms): stop all axes if the keyboard goes silent while an
147
+ axis is moving (0 = off). Guards against a lost stop frame.
148
+
149
+ Built explicitly (not via _act) because the firmware field is itself
150
+ named "timeout", which would clash with the serial-call timeout kwarg."""
151
+ path = "/ptz_act"
152
+ payload = {"task": path, "timeout": int(timeout_ms)}
153
+ if node is not None:
154
+ payload["node"] = node
155
+ return self._parent.post_json(path, payload, getReturn=True,
156
+ timeout=serial_timeout)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes