UC2-REST 0.2.0.38__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.38 → uc2_rest-0.2.0.39}/PKG-INFO +1 -1
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/UC2_REST.egg-info/PKG-INFO +1 -1
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/UC2_REST.egg-info/SOURCES.txt +1 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/UC2Client.py +4 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/__version__.py +1 -1
- uc2_rest-0.2.0.39/uc2rest/ptz.py +156 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/LICENSE +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/README.md +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/UC2_REST.egg-info/dependency_links.txt +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/UC2_REST.egg-info/not-zip-safe +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/UC2_REST.egg-info/requires.txt +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/UC2_REST.egg-info/top_level.txt +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/setup.cfg +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/setup.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/MockSerial.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/__init__.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/analog.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/camera.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/camera_trigger.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/can.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/canota.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/cmdrecorder.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/digitalin.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/digitalout.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/fan.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/galvo.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/gpio.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/gripper.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/home.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/i2c.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/laser.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/lcddisplay.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/ledmatrix.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/logger.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/message.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/modules.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/motor.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/motor_config.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/mserial.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/objective.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/pid.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/rotator.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/slm.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/state.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/temperature.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/utils.py +0 -0
- {uc2_rest-0.2.0.38 → uc2_rest-0.2.0.39}/uc2rest/wifi.py +0 -0
|
@@ -23,6 +23,7 @@ from .modules import Modules
|
|
|
23
23
|
from .digitalout import DigitalOut
|
|
24
24
|
from .digitalin import DigitalIn
|
|
25
25
|
from .gpio import GPIO
|
|
26
|
+
from .ptz import PTZ
|
|
26
27
|
from .i2c import I2C
|
|
27
28
|
from .rotator import Rotator
|
|
28
29
|
from .logger import Logger
|
|
@@ -161,6 +162,9 @@ class UC2Client(object):
|
|
|
161
162
|
# initialize GPIO / collision-detector slave interface
|
|
162
163
|
self.gpio = GPIO(self)
|
|
163
164
|
|
|
165
|
+
# initialize PTZ keyboard bridge interface (CAN node-id 61)
|
|
166
|
+
self.ptz = PTZ(self)
|
|
167
|
+
|
|
164
168
|
# initialize generic I2C passthrough (sensors on the GPIO slave bus)
|
|
165
169
|
self.i2c = I2C(self)
|
|
166
170
|
|
|
@@ -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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|