@@ -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