UC2-REST 0.2.0.37__tar.gz → 0.2.0.38__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.38}/PKG-INFO +1 -1
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/UC2_REST.egg-info/PKG-INFO +1 -1
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/UC2_REST.egg-info/SOURCES.txt +2 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/UC2Client.py +9 -1
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/__version__.py +1 -1
- uc2_rest-0.2.0.38/uc2rest/gpio.py +171 -0
- uc2_rest-0.2.0.38/uc2rest/i2c.py +190 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/motor.py +1 -1
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/mserial.py +8 -5
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/LICENSE +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/README.md +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/UC2_REST.egg-info/dependency_links.txt +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/UC2_REST.egg-info/not-zip-safe +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/UC2_REST.egg-info/requires.txt +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/UC2_REST.egg-info/top_level.txt +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/setup.cfg +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/setup.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/MockSerial.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/__init__.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/analog.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/camera.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/camera_trigger.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/can.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/canota.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/cmdrecorder.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/digitalin.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/digitalout.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/fan.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/galvo.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/gripper.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/home.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/laser.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/lcddisplay.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/ledmatrix.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/logger.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/message.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/modules.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/motor_config.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/objective.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/pid.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/rotator.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/slm.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/state.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/temperature.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/utils.py +0 -0
- {uc2_rest-0.2.0.37 → uc2_rest-0.2.0.38}/uc2rest/wifi.py +0 -0
|
@@ -22,6 +22,8 @@ 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 .i2c import I2C
|
|
25
27
|
from .rotator import Rotator
|
|
26
28
|
from .logger import Logger
|
|
27
29
|
from .cmdrecorder import cmdRecorder
|
|
@@ -155,7 +157,13 @@ class UC2Client(object):
|
|
|
155
157
|
|
|
156
158
|
# initialize digital in
|
|
157
159
|
self.digitalin = DigitalIn(self)
|
|
158
|
-
|
|
160
|
+
|
|
161
|
+
# initialize GPIO / collision-detector slave interface
|
|
162
|
+
self.gpio = GPIO(self)
|
|
163
|
+
|
|
164
|
+
# initialize generic I2C passthrough (sensors on the GPIO slave bus)
|
|
165
|
+
self.i2c = I2C(self)
|
|
166
|
+
|
|
159
167
|
# initialize messaging
|
|
160
168
|
self.message = Message(self)
|
|
161
169
|
|
|
@@ -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.38"
|
|
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):
|
|
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
|