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.
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/PKG-INFO +1 -1
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/UC2_REST.egg-info/PKG-INFO +1 -1
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/UC2_REST.egg-info/SOURCES.txt +3 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/UC2Client.py +13 -1
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/__version__.py +1 -1
- uc2_rest-0.2.0.39/uc2rest/gpio.py +171 -0
- uc2_rest-0.2.0.39/uc2rest/i2c.py +190 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/motor.py +1 -1
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/mserial.py +8 -5
- uc2_rest-0.2.0.39/uc2rest/ptz.py +156 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/LICENSE +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/README.md +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/UC2_REST.egg-info/dependency_links.txt +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/UC2_REST.egg-info/not-zip-safe +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/UC2_REST.egg-info/requires.txt +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/UC2_REST.egg-info/top_level.txt +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/setup.cfg +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/setup.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/MockSerial.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/__init__.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/analog.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/camera.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/camera_trigger.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/can.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/canota.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/cmdrecorder.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/digitalin.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/digitalout.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/fan.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/galvo.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/gripper.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/home.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/laser.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/lcddisplay.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/ledmatrix.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/logger.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/message.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/modules.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/motor_config.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/objective.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/pid.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/rotator.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/slm.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/state.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/temperature.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/utils.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.39}/uc2rest/wifi.py +0 -0
|
@@ -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.
|
|
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
|
-
|
|
368
|
-
self.
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|