UC2-REST 0.2.0.31__tar.gz → 0.2.0.33__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.31 → uc2_rest-0.2.0.33}/PKG-INFO +1 -1
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/UC2_REST.egg-info/PKG-INFO +1 -1
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/UC2_REST.egg-info/SOURCES.txt +3 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/UC2Client.py +33 -49
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/__version__.py +1 -1
- uc2_rest-0.2.0.33/uc2rest/camera_trigger.py +197 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/can.py +3 -2
- uc2_rest-0.2.0.33/uc2rest/canota.py +801 -0
- uc2_rest-0.2.0.33/uc2rest/digitalin.py +122 -0
- uc2_rest-0.2.0.33/uc2rest/digitalout.py +187 -0
- uc2_rest-0.2.0.33/uc2rest/galvo.py +312 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/home.py +60 -10
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/laser.py +1 -1
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/motor.py +94 -10
- uc2_rest-0.2.0.33/uc2rest/motor_config.py +315 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/mserial.py +119 -134
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/objective.py +4 -3
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/rotator.py +1 -1
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/state.py +1 -1
- uc2_rest-0.2.0.33/uc2rest/wifi.py +44 -0
- uc2_rest-0.2.0.31/uc2rest/canota.py +0 -256
- uc2_rest-0.2.0.31/uc2rest/digitalout.py +0 -65
- uc2_rest-0.2.0.31/uc2rest/galvo.py +0 -116
- uc2_rest-0.2.0.31/uc2rest/wifi.py +0 -98
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/LICENSE +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/README.md +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/UC2_REST.egg-info/dependency_links.txt +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/UC2_REST.egg-info/not-zip-safe +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/UC2_REST.egg-info/requires.txt +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/UC2_REST.egg-info/top_level.txt +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/setup.cfg +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/setup.py +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/MockSerial.py +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/__init__.py +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/analog.py +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/camera.py +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/cmdrecorder.py +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/gripper.py +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/lcddisplay.py +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/ledmatrix.py +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/logger.py +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/message.py +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/modules.py +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/pid.py +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/slm.py +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/temperature.py +0 -0
- {uc2_rest-0.2.0.31 → uc2_rest-0.2.0.33}/uc2rest/utils.py +0 -0
|
@@ -13,9 +13,11 @@ uc2rest/__init__.py
|
|
|
13
13
|
uc2rest/__version__.py
|
|
14
14
|
uc2rest/analog.py
|
|
15
15
|
uc2rest/camera.py
|
|
16
|
+
uc2rest/camera_trigger.py
|
|
16
17
|
uc2rest/can.py
|
|
17
18
|
uc2rest/canota.py
|
|
18
19
|
uc2rest/cmdrecorder.py
|
|
20
|
+
uc2rest/digitalin.py
|
|
19
21
|
uc2rest/digitalout.py
|
|
20
22
|
uc2rest/galvo.py
|
|
21
23
|
uc2rest/gripper.py
|
|
@@ -27,6 +29,7 @@ uc2rest/logger.py
|
|
|
27
29
|
uc2rest/message.py
|
|
28
30
|
uc2rest/modules.py
|
|
29
31
|
uc2rest/motor.py
|
|
32
|
+
uc2rest/motor_config.py
|
|
30
33
|
uc2rest/mserial.py
|
|
31
34
|
uc2rest/objective.py
|
|
32
35
|
uc2rest/pid.py
|
|
@@ -21,6 +21,7 @@ from .camera import Camera
|
|
|
21
21
|
from .analog import Analog
|
|
22
22
|
from .modules import Modules
|
|
23
23
|
from .digitalout import DigitalOut
|
|
24
|
+
from .digitalin import DigitalIn
|
|
24
25
|
from .rotator import Rotator
|
|
25
26
|
from .logger import Logger
|
|
26
27
|
from .cmdrecorder import cmdRecorder
|
|
@@ -28,18 +29,20 @@ from .temperature import Temperature
|
|
|
28
29
|
from .message import Message
|
|
29
30
|
from .can import CAN
|
|
30
31
|
from .canota import CANOTA
|
|
32
|
+
from .camera_trigger import CameraTrigger
|
|
33
|
+
|
|
34
|
+
# requests is no longer used for direct ESP communication (serial-only).
|
|
35
|
+
# Kept as optional import for other modules that may need HTTP (e.g. firmware downloads).
|
|
31
36
|
try:
|
|
32
37
|
import requests
|
|
33
|
-
except:
|
|
34
|
-
|
|
38
|
+
except Exception:
|
|
39
|
+
requests = None
|
|
35
40
|
|
|
36
41
|
class UC2Client(object):
|
|
37
|
-
# headers = {'ESP32-version': '*'}
|
|
38
42
|
headers={"Content-Type":"application/json"}
|
|
39
43
|
getmessage = ""
|
|
40
44
|
is_connected = False
|
|
41
45
|
|
|
42
|
-
is_wifi = False
|
|
43
46
|
is_serial = False
|
|
44
47
|
BAUDRATE = 115200
|
|
45
48
|
|
|
@@ -50,12 +53,13 @@ class UC2Client(object):
|
|
|
50
53
|
This client connects to the UC2-REST microcontroller that can be found here
|
|
51
54
|
https://github.com/openUC2/UC2-REST
|
|
52
55
|
|
|
56
|
+
Communication is via USB/serial only.
|
|
57
|
+
The host/port parameters are deprecated and will be ignored.
|
|
58
|
+
|
|
53
59
|
generally speaking you send/receive JSON documents that will cause an:
|
|
54
60
|
1. action => "/XXX_act"
|
|
55
61
|
2. getting => "/XXX_get"
|
|
56
62
|
3. setting => "/XXX_set"
|
|
57
|
-
|
|
58
|
-
you can send commands through wifi/http or usb/serial
|
|
59
63
|
'''
|
|
60
64
|
if True: #logger is None:
|
|
61
65
|
self.logger = Logger()
|
|
@@ -65,28 +69,27 @@ class UC2Client(object):
|
|
|
65
69
|
# perhaps we are in the browser?
|
|
66
70
|
self.isPyScript = isPyScript
|
|
67
71
|
|
|
68
|
-
#
|
|
72
|
+
# Deprecation notice for WiFi mode
|
|
73
|
+
if host is not None and serialport is None and SerialManager is None:
|
|
74
|
+
self.logger.warning(
|
|
75
|
+
"WiFi/HTTP communication has been removed. "
|
|
76
|
+
"Please use serialport= instead. Ignoring host parameter."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# initialize communication channel (serial only)
|
|
69
80
|
if serialport is not None:
|
|
70
81
|
# use USB connection
|
|
71
82
|
self.serial = Serial(serialport, baudrate, parent=self, identity=identity, DEBUG=DEBUG, skipFirmwareCheck=skipFirmwareCheck)
|
|
72
83
|
self.is_serial = True
|
|
73
84
|
self.is_connected = self.serial.is_connected
|
|
74
85
|
self.serial.DEBUG = DEBUG
|
|
75
|
-
elif host is not None:
|
|
76
|
-
# use client in wireless mode
|
|
77
|
-
self.is_wifi = True
|
|
78
|
-
self.host = host
|
|
79
|
-
self.port = port
|
|
80
|
-
|
|
81
|
-
# check if host is up
|
|
82
|
-
self.logger.debug(f"Connecting to microscope {self.host}:{self.port}")
|
|
83
|
-
#self.is_connected = self.isConnected()
|
|
84
86
|
elif SerialManager is not None:
|
|
85
|
-
# we are trying to access the controller from
|
|
87
|
+
# we are trying to access the controller from a web browser
|
|
86
88
|
self.serial = SerialManagerWrapper(SerialManager, parent=self)
|
|
87
89
|
self.isPyScript = True
|
|
90
|
+
self.is_serial = True
|
|
88
91
|
else:
|
|
89
|
-
self.logger.error("No ESP32 device is connected -
|
|
92
|
+
self.logger.error("No ESP32 device is connected - please provide a serialport!")
|
|
90
93
|
|
|
91
94
|
|
|
92
95
|
# import libraries depending on API version
|
|
@@ -149,49 +152,30 @@ class UC2Client(object):
|
|
|
149
152
|
# initialize digital out
|
|
150
153
|
self.digitalout = DigitalOut(self)
|
|
151
154
|
|
|
155
|
+
# initialize digital in
|
|
156
|
+
self.digitalin = DigitalIn(self)
|
|
157
|
+
|
|
152
158
|
# initialize messaging
|
|
153
159
|
self.message = Message(self)
|
|
160
|
+
|
|
161
|
+
# initialize camera trigger callback handler
|
|
162
|
+
self.camera_trigger = CameraTrigger(self)
|
|
154
163
|
|
|
155
164
|
# initialize module controller
|
|
156
165
|
self.modules = Modules(parent=self)
|
|
157
166
|
|
|
158
167
|
def post_json(self, path, payload, getReturn=True, nResponses=1, timeout=1):
|
|
159
|
-
if
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
try:
|
|
163
|
-
if timeout==0: timeout=.2
|
|
164
|
-
r = requests.post(url, json=payload, headers=self.headers, timeout=timeout)
|
|
165
|
-
returnMessage = r.json()
|
|
166
|
-
returnMessage["success"] = r.status_code==200
|
|
167
|
-
except Exception as e:
|
|
168
|
-
print(e)
|
|
169
|
-
returnMessage = {}
|
|
170
|
-
returnMessage["error"] = str(e)
|
|
171
|
-
returnMessage["success"] = 0
|
|
172
|
-
return returnMessage
|
|
173
|
-
elif self.is_serial or self.isPyScript:
|
|
174
|
-
if timeout <=0:
|
|
175
|
-
getReturn = False
|
|
176
|
-
return self.serial.post_json(path, payload, getReturn=getReturn, nResponses=nResponses, timeout=timeout)
|
|
177
|
-
else:
|
|
178
|
-
self.logger.error("No ESP32 device is connected - check IP or Serial port!")
|
|
179
|
-
return None
|
|
168
|
+
if timeout <=0:
|
|
169
|
+
getReturn = False
|
|
170
|
+
return self.serial.post_json(path, payload, getReturn=getReturn, nResponses=nResponses, timeout=timeout)
|
|
180
171
|
|
|
181
172
|
def get_json(self, path, getReturn=True, timeout=1):
|
|
182
|
-
if self.
|
|
183
|
-
|
|
184
|
-
url = f"http://{self.host}:{self.port}{path}"
|
|
185
|
-
r = requests.get(url, headers=self.headers, timeout=timeout)
|
|
186
|
-
return r.json()
|
|
187
|
-
elif self.is_serial or self.isPyScript:
|
|
188
|
-
# timeout is not used anymore
|
|
189
|
-
if timeout <=0:
|
|
173
|
+
if self.is_serial or self.isPyScript:
|
|
174
|
+
if timeout <= 0:
|
|
190
175
|
getReturn = False
|
|
191
176
|
return self.serial.post_json(path, payload=None, getReturn=getReturn, nResponses=1, timeout=timeout)
|
|
192
|
-
#return self.serial.read_json()<
|
|
193
177
|
else:
|
|
194
|
-
self.logger.error("No ESP32 device is connected -
|
|
178
|
+
self.logger.error("No ESP32 device is connected - serial not initialized!")
|
|
195
179
|
return None
|
|
196
180
|
|
|
197
181
|
def setDebugging(self, debug=False):
|
|
@@ -6,7 +6,7 @@ __version__.py
|
|
|
6
6
|
|
|
7
7
|
__title__ = 'UC2-REST'
|
|
8
8
|
__description__ = 'This pacage will help you to drive the ESP32-driven microscopy control modules from UC2'
|
|
9
|
-
__version__ = "v0.2.0.
|
|
9
|
+
__version__ = "v0.2.0.33"
|
|
10
10
|
__author__ = 'Benedict Diederich'
|
|
11
11
|
__author_email__ = 'benedictdied@gmail.com'
|
|
12
12
|
__license__ = 'GPL v3'
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Camera trigger callback module for UC2-REST.
|
|
3
|
+
|
|
4
|
+
This module handles camera trigger signals from the firmware ({"cam":1})
|
|
5
|
+
to enable software triggering based on hardware events during stage scanning.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import time
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
gTIMEOUT = 1 # seconds to wait for a response from the ESP32
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CameraTrigger(object):
|
|
17
|
+
"""
|
|
18
|
+
This class parses incoming camera trigger signals from the ESP32 firmware.
|
|
19
|
+
|
|
20
|
+
When the firmware sends {"cam":1}, this module triggers registered callbacks
|
|
21
|
+
which can be used for software-triggered image acquisition during stage scanning.
|
|
22
|
+
|
|
23
|
+
Example usage:
|
|
24
|
+
import uc2rest
|
|
25
|
+
|
|
26
|
+
ESP32 = uc2rest.UC2Client(serialport=port, baudrate=500000)
|
|
27
|
+
|
|
28
|
+
# Register callback for camera trigger
|
|
29
|
+
def my_camera_callback(data):
|
|
30
|
+
print(f"Camera trigger received: {data}")
|
|
31
|
+
# Trigger image acquisition here
|
|
32
|
+
|
|
33
|
+
ESP32.camera_trigger.register_callback(0, my_camera_callback)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, parent=None, nCallbacks=10):
|
|
37
|
+
"""
|
|
38
|
+
Initialize camera trigger handler.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
parent: Parent UC2Client instance
|
|
42
|
+
nCallbacks: Maximum number of callback functions to register
|
|
43
|
+
"""
|
|
44
|
+
self._parent = parent
|
|
45
|
+
self.nCallbacks = nCallbacks
|
|
46
|
+
|
|
47
|
+
# Track trigger count for diagnostics
|
|
48
|
+
self._trigger_count = 0
|
|
49
|
+
self._last_trigger_time = None
|
|
50
|
+
|
|
51
|
+
# Initialize callback functions
|
|
52
|
+
self._callbackPerKey = {}
|
|
53
|
+
self.init_callback_functions(self.nCallbacks)
|
|
54
|
+
|
|
55
|
+
# Register callback for camera trigger on serial loop
|
|
56
|
+
if hasattr(self._parent, "serial"):
|
|
57
|
+
self._parent.serial.register_callback(self._callback_camera_trigger, pattern="cam")
|
|
58
|
+
|
|
59
|
+
def _callback_camera_trigger(self, data):
|
|
60
|
+
"""
|
|
61
|
+
Parse camera trigger message from firmware.
|
|
62
|
+
|
|
63
|
+
Expected JSON format:
|
|
64
|
+
{
|
|
65
|
+
"cam": 1 # Trigger signal
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
or with additional data:
|
|
69
|
+
{
|
|
70
|
+
"cam": {
|
|
71
|
+
"trigger": 1,
|
|
72
|
+
"frame_id": 123,
|
|
73
|
+
"illumination": 0
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
data: JSON data dictionary from firmware
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
# Update trigger statistics
|
|
82
|
+
self._trigger_count += 1
|
|
83
|
+
self._last_trigger_time = time.time()
|
|
84
|
+
|
|
85
|
+
# Extract trigger information
|
|
86
|
+
cam_data = data.get("cam", {})
|
|
87
|
+
|
|
88
|
+
# Handle simple trigger ({"cam": 1})
|
|
89
|
+
if isinstance(cam_data, (int, float)):
|
|
90
|
+
trigger_info = {
|
|
91
|
+
"trigger": int(cam_data),
|
|
92
|
+
"frame_id": self._trigger_count,
|
|
93
|
+
"timestamp": self._last_trigger_time
|
|
94
|
+
}
|
|
95
|
+
self._parent.logger.debug(f"Camera trigger received: {trigger_info}")
|
|
96
|
+
else:
|
|
97
|
+
# Handle extended trigger data
|
|
98
|
+
trigger_info = {
|
|
99
|
+
"trigger": cam_data.get("trigger", 1),
|
|
100
|
+
"frame_id": cam_data.get("frame_id", self._trigger_count),
|
|
101
|
+
"illumination": cam_data.get("illumination", -1),
|
|
102
|
+
"timestamp": self._last_trigger_time
|
|
103
|
+
}
|
|
104
|
+
self._parent.logger.debug(f"Camera trigger with data received: {trigger_info}")
|
|
105
|
+
|
|
106
|
+
# Call all registered callbacks
|
|
107
|
+
for key, callback in self._callbackPerKey.items():
|
|
108
|
+
if callback is not None and callable(callback):
|
|
109
|
+
try:
|
|
110
|
+
callback(trigger_info)
|
|
111
|
+
except Exception as callback_error:
|
|
112
|
+
print(f"Error in camera trigger callback {key}: {callback_error}")
|
|
113
|
+
|
|
114
|
+
except Exception as e:
|
|
115
|
+
print(f"Error in _callback_camera_trigger: {e}")
|
|
116
|
+
|
|
117
|
+
def init_callback_functions(self, nCallbacks=10):
|
|
118
|
+
"""
|
|
119
|
+
Initialize callback function dictionary.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
nCallbacks: Number of callback slots to create
|
|
123
|
+
"""
|
|
124
|
+
self._callbackPerKey = {}
|
|
125
|
+
self.nCallbacks = nCallbacks
|
|
126
|
+
for i in range(nCallbacks):
|
|
127
|
+
self._callbackPerKey[i] = None
|
|
128
|
+
|
|
129
|
+
def register_callback(self, key, callback):
|
|
130
|
+
"""
|
|
131
|
+
Register a callback function for camera trigger events.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
key: Integer key (0 to nCallbacks-1) for this callback
|
|
135
|
+
callback: Function to call when trigger is received.
|
|
136
|
+
Function signature: callback(trigger_info: dict)
|
|
137
|
+
|
|
138
|
+
Example:
|
|
139
|
+
def on_camera_trigger(info):
|
|
140
|
+
print(f"Frame {info['frame_id']} triggered at {info['timestamp']}")
|
|
141
|
+
camera.snap_image()
|
|
142
|
+
|
|
143
|
+
ESP32.camera_trigger.register_callback(0, on_camera_trigger)
|
|
144
|
+
"""
|
|
145
|
+
if key < 0 or key >= self.nCallbacks:
|
|
146
|
+
raise ValueError(f"Callback key must be between 0 and {self.nCallbacks-1}")
|
|
147
|
+
self._callbackPerKey[key] = callback
|
|
148
|
+
|
|
149
|
+
def unregister_callback(self, key):
|
|
150
|
+
"""
|
|
151
|
+
Remove a registered callback.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
key: Integer key of callback to remove
|
|
155
|
+
"""
|
|
156
|
+
if key in self._callbackPerKey:
|
|
157
|
+
self._callbackPerKey[key] = None
|
|
158
|
+
|
|
159
|
+
def clear_all_callbacks(self):
|
|
160
|
+
"""Remove all registered callbacks."""
|
|
161
|
+
self.init_callback_functions(self.nCallbacks)
|
|
162
|
+
|
|
163
|
+
def get_trigger_count(self):
|
|
164
|
+
"""
|
|
165
|
+
Get the total number of triggers received since initialization.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
int: Number of camera triggers received
|
|
169
|
+
"""
|
|
170
|
+
return self._trigger_count
|
|
171
|
+
|
|
172
|
+
def get_last_trigger_time(self):
|
|
173
|
+
"""
|
|
174
|
+
Get the timestamp of the last trigger received.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
float or None: Unix timestamp of last trigger, or None if no triggers received
|
|
178
|
+
"""
|
|
179
|
+
return self._last_trigger_time
|
|
180
|
+
|
|
181
|
+
def reset_trigger_count(self):
|
|
182
|
+
"""Reset the trigger counter to zero."""
|
|
183
|
+
self._trigger_count = 0
|
|
184
|
+
self._last_trigger_time = None
|
|
185
|
+
|
|
186
|
+
def get_trigger_stats(self):
|
|
187
|
+
"""
|
|
188
|
+
Get statistics about trigger events.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
dict: Dictionary with trigger statistics
|
|
192
|
+
"""
|
|
193
|
+
return {
|
|
194
|
+
"total_triggers": self._trigger_count,
|
|
195
|
+
"last_trigger_time": self._last_trigger_time,
|
|
196
|
+
"callbacks_registered": sum(1 for cb in self._callbackPerKey.values() if cb is not None)
|
|
197
|
+
}
|