Skip to content

Commit 62a9f41

Browse files
authored
Merge pull request #141 from openUC2/mergemaster
Add State emergency/power APIs and callbacks
2 parents 1d6be0e + b871a01 commit 62a9f41

2 files changed

Lines changed: 124 additions & 4 deletions

File tree

uc2rest/UC2Client.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,6 @@ def __init__(self, host=None, port=31950, serialport=None, identity="UC2_Feather
135135
# initialize fan controller
136136
self.fan = Fan(self)
137137

138-
# initialize laser
139-
self.state = State(self)
140-
141138
# initialize galvo
142139
self.galvo = Galvo(self)
143140

uc2rest/state.py

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,130 @@ class State(object):
77

88
def __init__(self, parent):
99
self._parent = parent
10-
10+
11+
# Emergency-STOP event handling. The CANopen master firmware emits an
12+
# unsolicited block when the hardware E-stop is pressed/released:
13+
# ++
14+
# {"emergency":{"active":1,"reason":"estop","msg":"..."},"qid":0}
15+
# --
16+
# We surface it through registered callbacks (see register_emergency_callback).
17+
self.emergency_active = False
18+
self.last_emergency = None
19+
self._emergency_callbacks = []
20+
if hasattr(self._parent, "serial") and hasattr(self._parent.serial, "register_callback"):
21+
self._parent.serial.register_callback(self._callback_emergency, pattern="emergency")
22+
23+
# ──────────────────────────────────────────────────────────────────────
24+
# CAN-bus power (CANopen master gate driven by BUSPOWER_OFF_PIN)
25+
# ──────────────────────────────────────────────────────────────────────
26+
def set_power(self, power=1, getReturn=True, timeout=1):
27+
'''
28+
Enable (1, default) or disable (0) the high-current CAN-bus power that
29+
feeds the slaves. Maps to /state_act {"power":0|1} on the master.
30+
'''
31+
path = "/state_act"
32+
payload = {
33+
"task": path,
34+
"power": int(bool(power))
35+
}
36+
return self._parent.post_json(path, payload, getReturn=getReturn, timeout=timeout)
37+
38+
def get_power(self, timeout=1):
39+
'''
40+
Query the current CAN-bus power state (1=ON, 0=OFF).
41+
Maps to /state_get {"power":1}. Returns an int when parseable,
42+
otherwise the raw response.
43+
'''
44+
path = "/state_get"
45+
payload = {
46+
"task": path,
47+
"power": 1
48+
}
49+
r = self._parent.post_json(path, payload, timeout=timeout)
50+
try:
51+
if isinstance(r, list):
52+
r = r[0]
53+
return r["state"]["power"]
54+
except Exception:
55+
return r
56+
57+
# ──────────────────────────────────────────────────────────────────────
58+
# Emergency-STOP (async event pushed by the firmware)
59+
# ──────────────────────────────────────────────────────────────────────
60+
def set_estop_polarity(self, polarity=0, getReturn=True, timeout=1):
61+
'''
62+
Set the emergency-STOP asserted (emergency) logic level. Persisted on the
63+
device and applied live.
64+
polarity=0 -> active-LOW (asserted reads LOW, idle HIGH)
65+
polarity=1 -> active-HIGH (asserted reads HIGH, idle LOW)
66+
Maps to /state_act {"estopPolarity":0|1}.
67+
'''
68+
path = "/state_act"
69+
payload = {"task": path, "estopPolarity": int(bool(polarity))}
70+
return self._parent.post_json(path, payload, getReturn=getReturn, timeout=timeout)
71+
72+
def get_estop(self, timeout=1):
73+
'''
74+
Read E-stop diagnostics as a dict:
75+
{"estopPolarity":0|1, "estopRaw":0|1|-1, "estopActive":0|1}.
76+
estopRaw is the live pin level — flip the polarity (set_estop_polarity)
77+
until pressing the button makes estopActive read 1.
78+
Maps to /state_get {"estop":1}.
79+
'''
80+
path = "/state_get"
81+
payload = {"task": path, "estop": 1}
82+
r = self._parent.post_json(path, payload, timeout=timeout)
83+
try:
84+
if isinstance(r, list):
85+
r = r[0]
86+
return r["state"]
87+
except Exception:
88+
return r
89+
90+
def _callback_emergency(self, data):
91+
''' Handle the async {"emergency":{...}} event from the firmware. '''
92+
try:
93+
em = data.get("emergency", {})
94+
self.emergency_active = bool(em.get("active", 0))
95+
self.last_emergency = em
96+
if self.emergency_active:
97+
self._parent.logger.warning(
98+
"EMERGENCY STOP: " + str(em.get("msg", "E-stop asserted")))
99+
else:
100+
self._parent.logger.info("Emergency stop released")
101+
for callback in list(self._emergency_callbacks):
102+
try:
103+
callback(em)
104+
except Exception as callback_error:
105+
self._parent.logger.error(
106+
"Error in emergency callback execution: " + str(callback_error))
107+
except Exception as e:
108+
self._parent.logger.error("Error in _callback_emergency: " + str(e))
109+
110+
def register_emergency_callback(self, callbackfct):
111+
'''
112+
Register a function invoked whenever an emergency event arrives.
113+
The callback receives the "emergency" dict, e.g.
114+
{"active":1,"reason":"estop","msg":"..."}.
115+
116+
Example:
117+
def on_estop(em):
118+
if em.get("active"):
119+
print("E-STOP! stopping host-side acquisition")
120+
ESP32.state.register_emergency_callback(on_estop)
121+
'''
122+
if callbackfct not in self._emergency_callbacks:
123+
self._emergency_callbacks.append(callbackfct)
124+
125+
def unregister_emergency_callback(self, callbackfct):
126+
''' Remove a previously registered emergency callback. '''
127+
if callbackfct in self._emergency_callbacks:
128+
self._emergency_callbacks.remove(callbackfct)
129+
130+
def is_emergency_active(self):
131+
''' Last known emergency state as reported by the firmware. '''
132+
return self.emergency_active
133+
11134
def get_state(self, timeout=3):
12135
path = "/state_get"
13136

0 commit comments

Comments
 (0)