Coverage for /home/pi/Software/model-railway-signalling/model_railway_signals/library/gpio_sensors.py: 97%

223 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-10 15:08 +0100

1#--------------------------------------------------------------------------------------------------- 

2# This module is used for creating and managing GPIO Sensor library objects (mapped to GPIO ports) 

3#--------------------------------------------------------------------------------------------------- 

4# 

5# External API - the classes and functions (used by the Schematic Editor): 

6#  

7# gpio_interface_enabled() - returns True if the platform supports GPIO inputs  

8# 

9# get_list_of_available_ports() - returns a list of supported GPIO ports 

10# 

11# gpio_shutdown() - Return all GPIO ports to their default states 

12# 

13# gpio_sensor_exists(sensor_id:int/str) - returns true if the GPIO sensor object 'exists' (either a GPIO port 

14# mapping has been configured for the sensor_id or the sensor_id has been subscribed to via MQTT networking) 

15# 

16# The following API functions are for configuring the local GPIO port mappings. The functions are called 

17# by the editor on 'Apply' of the GPIO settings. First, 'delete_all_local_gpio_sensors' is called to clear 

18# down all the existing mappings, followed by 'create_gpio_sensor' for each sensor that has been mapped 

19# 

20# delete_all_local_gpio_sensors() - Delete all local GPIO sensor objects 

21# 

22# create_gpio_sensor - Creates a GPIO sensor object (effectively a GPIO port mapping) 

23# Mandatory Parameters: 

24# sensor_id:int - The ID to be used for the sensor  

25# gpio_channel:int - The GPIO port port for the sensor (not the physical pin number) 

26# Optional Parameters: 

27# trigger_period:float - Active duration for sensor before triggering - default = 0.001 secs 

28# sensor_timeout:float - Time period for ignoring further triggers - default = 3.0 secs 

29# signal_passed:int - Configure a "signal passed" event for a Signal ID (default = 0) 

30# signal_approach:int - Configure a "approach release" event for a Signal ID (default = 0 

31# sensor_passed:int - Configure a "Track Sensor passed" event for a Track Sensor ID (default = 0) 

32# 

33# The following API functions are for adding/removing Signal and Track Sensor callback events to the GPIO 

34# Sensor. The 'remove_gpio_sensor_callback' function is called by the editor whenever a Signal or Track Sensor 

35# object (which references the GPIO sensor in its configuration) is hard deleted from the schematic. It is 

36# also called on a Signal or Track Sensor configuration object to delete the existing callback mapping prior 

37# to the new mapping being created via the 'add_gpio_sensor_callback' function. Note that Signals and Track 

38# Sensors can be mapped to local or remote GPIO Sensors so the Sensor ID can be an integer or a string. 

39# 

40# get_gpio_sensor_callback(sensor_id:int/str) - returns a list of current callbacks for the GPIO Sensor 

41# list comprises: [signal_passed, signal_approach, sensor_passed] 

42# 

43# remove_gpio_sensor_callback(sensor_id:int/str) - Remove the callback from the specified GPIO Sensor 

44# 

45# add_gpio_sensor_callback  

46# Mandatory Parameters: 

47# sensor_id:int/str - The local or remote (in the form 'node-id') GPIO Sensor ID  

48# Optional Parameters: 

49# signal_passed:int - Configure a "signal passed" event for a Signal ID (default = 0) 

50# signal_approach:int - Configure a "approach release" event for a Signal ID (default = 0 

51# sensor_passed:int - Configure a "Track Sensor passed" event for a Track Sensor ID (default = 0) 

52# 

53# The following API functions are for configuring the pub/sub of GPIO Sensor events. The functions are called 

54# by the editor on 'Apply' of the MQTT settings. First, 'reset_mqtt_configuration' is called to clear down 

55# the existing pub/sub configuration, followed by 'set_sensors_to_publish_state' (with the list of LOCAL GPIO 

56# Sensors to publish) and 'subscribe_to_remote_sensor' for each REMOTE GPIO Sensor that has been subscribed. 

57# 

58# reset_mqtt_configuration() - Clears down the current GPIO Sensor pub/sub configuration 

59#  

60# set_gpio_sensors_to_publish_state (*sensor_ids:int) - Enable the publication of GPIO Sensor trigger events. 

61# 

62# subscribe_to_remote_gpio_sensor - Subscribes to a remote GPIO Sensor object and configures the callback event 

63# Mandatory Parameters: 

64# remote_id:str - The remote GPIO Sensor identifier (in the form 'node-id') 

65# Optional Parameters: 

66# signal_passed:int - Configure a "signal passed" event for a Signal ID (default = 0) 

67# signal_approach:int - Configure a "approach release" event for a Signal ID (default = 0 

68# sensor_passed:int - Configure a "Track Sensor passed" event for a Track Sensor ID (default = 0) 

69#  

70# External API - classes and functions (used by the other library modules): 

71# 

72# handle_mqtt_gpio_sensor_triggered_event(message:dict) - Called on receipt of a remote 'gpio_sensor_event' 

73# 

74#--------------------------------------------------------------------------------------------------- 

75 

76import time 

77import threading 

78import logging 

79from typing import Union 

80 

81from . import common 

82from . import signals_common 

83from . import track_sensors 

84from . import mqtt_interface 

85 

86#--------------------------------------------------------------------------------------------------- 

87# We can only use gpiozero interface if we're running on a Raspberry Pi. Other Platforms may not 

88# include the RPi specific GPIO package so this is a quick and dirty way of detecting it on startup. 

89# The result (True or False) is maintained in the global 'running_on_raspberry_pi' variable 

90#--------------------------------------------------------------------------------------------------- 

91 

92def is_running_on_raspberry_pi(): 

93 global gpiozero 

94 try: 

95 import gpiozero 

96 return (True) 

97 except Exception: 

98 logging.warning("GPIO Interface: Not running on a Raspberry Pi - track sensors will be inoperative") 

99 return (False) 

100 

101running_on_raspberry_pi = is_running_on_raspberry_pi() 

102 

103#--------------------------------------------------------------------------------------------------- 

104# API Function for external modules to test if GPIO inputs are supported by the platform 

105#--------------------------------------------------------------------------------------------------- 

106 

107def gpio_interface_enabled(): 

108 return (running_on_raspberry_pi) 

109 

110#--------------------------------------------------------------------------------------------------- 

111# API function to return a list of available GPIO ports. 

112# We don't use GPIO 14, 15, 16 or 17 as these are used for UART comms with the PI-SPROG-3 (Tx, Rx, CTS, RTS) 

113# We don't use GPIO 0, 1, 2, 3 as these are the I2C (which we might want to use later) 

114#--------------------------------------------------------------------------------------------------- 

115 

116def get_list_of_available_ports(): 

117 return ([4,5,6,7,8,9,10,11,12,13,18,19,20,21,22,23,24,25,26,27]) 

118 

119#--------------------------------------------------------------------------------------------------- 

120# GPIO port mappings are stored in a global dictionary when created - key is the GPIO port ID  

121# Each Entry is a dictionary specific to the GPIO port that has been mapped with the following Keys: 

122# "sensor_id" : Unique ID for the sensor - int (for local sensors) or str (for remote sensors) 

123# "signal_approach" : The signal ID (to raise a 'signal approached' event when triggered) - int 

124# "signal_passed" : The signal ID (to raise a 'signal passed' event for when triggered) - int 

125# "sensor_passed" : The Track Sensor ID (to raise a 'sensor passed' event when triggered) - int 

126# "trigger_delay" : Time that the GPIO port must remain active to raise a trigger event - float 

127# "timeout_value" : Time period (from initial trigger event) for ignoring further triggers - float 

128# "sensor_device" : The reference to the gpiozero button object mapped to the GPIO port 

129# "timeout_start" : The time the sensor was triggered (after any 'debounce period) - time 

130# "timeout_active" : Flag for whether the sensor is still within the timeout period - bool 

131#--------------------------------------------------------------------------------------------------- 

132 

133gpio_port_mappings: dict = {} 

134 

135#--------------------------------------------------------------------------------------------------- 

136# Global list of track GPIO Sensor IDs (integers) to publish to the MQTT Broker 

137#--------------------------------------------------------------------------------------------------- 

138 

139list_of_track_sensors_to_publish=[] 

140 

141#--------------------------------------------------------------------------------------------------- 

142# Internal function to find the key in the gpio_port_mappings dictionary for a given Sensor ID 

143# Note that the gpio_port is returned as a str (as it represents the key to the dict entry) 

144#--------------------------------------------------------------------------------------------------- 

145 

146def mapped_gpio_port(sensor_id:Union[int,str]): 

147 for gpio_port in gpio_port_mappings.keys(): 

148 if str(gpio_port_mappings[gpio_port]["sensor_id"]) == str(sensor_id): 

149 return(gpio_port) 

150 return("None") 

151 

152#--------------------------------------------------------------------------------------------------- 

153# API Function to check if a sensor exists (either mapped or subscribed to via mqtt metworking) 

154#--------------------------------------------------------------------------------------------------- 

155 

156def gpio_sensor_exists(sensor_id:Union[int,str]): 

157 # The sensor_id could be either an int (local sensor) or a str (remote sensor) 

158 if not isinstance(sensor_id, int) and not isinstance(sensor_id, str): 

159 logging.error("GPIO Sensor "+str(sensor_id)+": gpio_sensor_exists - Sensor ID must be an int or str") 

160 sensor_exists = False 

161 else: 

162 sensor_exists = mapped_gpio_port(sensor_id) != "None" 

163 return(sensor_exists) 

164 

165#--------------------------------------------------------------------------------------------------- 

166# Thread to "lock" the sensor for the specified timeout period after gpio_sensor_triggered 

167# Any re-triggers during this period are ignored (they just extend the timeout period) 

168# Note that the thread_to_timeout_sensor would normally only get run for 'real' GPIO events 

169# where the gpiozero device would exist and would be released during the timeout period. 

170# We therefore have to provide a specific "testing" flag to enable the code to be tested. 

171#--------------------------------------------------------------------------------------------------- 

172 

173def thread_to_timeout_sensor(gpio_port:int, testing:bool): 

174 global gpio_port_mappings 

175 # We put exception handling round the entirethread to handle the case of a gpio sensor mapping 

176 # being deleted whilst the timeout period is still active - in this case we just exit gracefully 

177 try: 

178 # Wait for the timeout period to expire (if the sensor is released and triggered again within the 

179 # timeout period then the timeout will just be extended). Note that the loop will immediately exit 

180 # if the shutdown has been initiated (which will delete the gpio zero button objects) 

181 while (time.time() < (gpio_port_mappings[str(gpio_port)]["timeout_start"] 

182 + gpio_port_mappings[str(gpio_port)]["timeout_value"]) and not common.shutdown_initiated): 

183 # We also wait for the button to be released before trporting the Event release. 

184 while not testing and not common.shutdown_initiated and gpio_port_mappings[str(gpio_port)]["sensor_device"].is_pressed: 184 ↛ 185line 184 didn't jump to line 185, because the condition on line 184 was never true

185 time.sleep(0.0001) 

186 # Reset the sensor at the end of the timeout period 

187 sensor_id = gpio_port_mappings[str(gpio_port)]["sensor_id"] 

188 logging.debug("GPIO Sensor "+str(sensor_id)+": Event Reset **********************************************") 

189 gpio_port_mappings[str(gpio_port)]["timeout_active"] = False 

190 except: 

191 pass 

192 return() 

193 

194#--------------------------------------------------------------------------------------------------- 

195# Internal function called whenever a "Button Press" event is detected for the external GPIO port. 

196# A timeout is applied to the button press event to prevent re-triggering until the timeout has completed. 

197# This is to handle optical sensors which might Fall and then Rise when each carrage/waggon passes over. 

198# If the sensor is still within the timeout period (from the last time it was triggered) then the timeout 

199# will effectively be extended - otherwise a new timeout period will be started and the callback made. 

200# Note that the gpio_sensor_triggered function would normally only get triggered for 'real' GPIO events 

201# where the gpiozero device would exist and would still be pressed at the end of the trigger period. 

202# We therefore have to provide a specific "testing" flag to enable the code to be tested. 

203#--------------------------------------------------------------------------------------------------- 

204 

205def gpio_sensor_triggered(gpio_port:int, testing:bool=False): 

206 global gpio_port_mappings 

207 # Wait for the trigger period to complete 

208 time.sleep(gpio_port_mappings[str(gpio_port)]["trigger_period"]) 

209 # Only process the event if shutdown has not been not initiated and the trigger is still active 

210 if not common.shutdown_initiated and (testing or gpio_port_mappings[str(gpio_port)]["sensor_device"].is_pressed): 210 ↛ 229line 210 didn't jump to line 229, because the condition on line 210 was never false

211 # If we are still in the timeout period then ignore the trigger event (but Reset the timeout period) 

212 if gpio_port_mappings[str(gpio_port)]["timeout_active"]: 

213 gpio_port_mappings[str(gpio_port)]["timeout_start"] = time.time() 

214 else: 

215 # Start a new timeout thread 

216 gpio_port_mappings[str(gpio_port)]["timeout_active"] = True 

217 gpio_port_mappings[str(gpio_port)]["timeout_start"] = time.time() 

218 timeout_thread = threading.Thread (target=thread_to_timeout_sensor, args=(gpio_port,testing,)) 

219 timeout_thread.setDaemon(True) 

220 timeout_thread.start() 

221 # Transmit the state via MQTT networking (will only be sent if configured to publish) and  

222 # Make the appropriate callback (triggered callback or signal approach/passed event) 

223 sensor_id = gpio_port_mappings[str(gpio_port)]["sensor_id"] 

224 logging.info("GPIO Sensor "+str(sensor_id)+": Triggered Event *******************************************") 

225 send_mqtt_gpio_sensor_triggered_event(sensor_id) 

226 # Note that this function will be running in a different thread to the main Tkinter thread 

227 # We therefore callback into the main Tkinter thread to process the event 

228 common.execute_function_in_tkinter_thread(lambda:make_gpio_sensor_triggered_callback(sensor_id)) 

229 return() 

230 

231#--------------------------------------------------------------------------------------------------- 

232# Internal function for building and sending MQTT messages (if configured to publish) 

233#--------------------------------------------------------------------------------------------------- 

234 

235def send_mqtt_gpio_sensor_triggered_event(sensor_id:int): 

236 if sensor_id in list_of_track_sensors_to_publish: 

237 log_message = "GPIO Sensor "+str(sensor_id)+": Publishing 'sensor triggered' event to MQTT Broker" 

238 # Publish as non "retained" messages as these events are transitory 

239 mqtt_interface.send_mqtt_message("gpio_sensor_event",sensor_id,data={},log_message=log_message,retain=False) 

240 return() 

241 

242#--------------------------------------------------------------------------------------------------- 

243# API callback function for handling received MQTT messages from a remote track sensor 

244# Note that this function will already be running in the main Tkinter thread 

245#--------------------------------------------------------------------------------------------------- 

246 

247def handle_mqtt_gpio_sensor_triggered_event(message): 

248 if "sourceidentifier" not in message.keys() or not gpio_sensor_exists(message["sourceidentifier"]): 

249 logging.warning("GPIO Interface: handle_mqtt_gpio_sensor_triggered_event - Unhandled MQTT message - "+str(message)) 

250 else: 

251 # Note that the remote sensor object would have been created with the sensor_identifier  

252 # as the 'key' to the dict of gpio_port_mappings rather than the GPIO port number 

253 logging.info("GPIO Sensor "+message["sourceidentifier"]+": Remote GPIO sensor has been triggered *********************") 

254 # We are already running in the main Tkinter thread so just call the function to make the callback 

255 make_gpio_sensor_triggered_callback(message["sourceidentifier"]) 

256 return() 

257 

258#--------------------------------------------------------------------------------------------------- 

259# Internal Function to raise the appropriate event ('signal passed', 'signal approached' or 

260# 'track sensor passed') - by calling in to the appropriate library module. Note that this 

261# function will be called from within the main Tkinter thread so the callback we make 

262# will also be executed in the main Tkinter Thread 

263#--------------------------------------------------------------------------------------------------- 

264 

265def make_gpio_sensor_triggered_callback(sensor_id:Union[int,str]): 

266 # The sensor_id could be either an int (local sensor) or a str (remote sensor) 

267 str_gpio_port = mapped_gpio_port(sensor_id) 

268 if gpio_port_mappings[str_gpio_port]["signal_passed"] > 0: 

269 sig_id = gpio_port_mappings[str_gpio_port]["signal_passed"] 

270 signals_common.sig_passed_button_event(sig_id) 

271 elif gpio_port_mappings[str_gpio_port]["signal_approach"] > 0: 

272 sig_id = gpio_port_mappings[str_gpio_port]["signal_approach"] 

273 signals_common.approach_release_button_event(sig_id) 

274 elif gpio_port_mappings[str_gpio_port]["sensor_passed"] > 0: 

275 sensor_id = gpio_port_mappings[str_gpio_port]["sensor_passed"] 

276 track_sensors.track_sensor_triggered(sensor_id) 

277 return() 

278 

279#--------------------------------------------------------------------------------------------------- 

280# Public API function to create a sensor object (mapped to a GPIO channel) 

281# All attributes (that need to be tracked) are stored as a dictionary 

282# This is then added to a dictionary of sensors for later reference 

283#--------------------------------------------------------------------------------------------------- 

284 

285def create_gpio_sensor (sensor_id:int, gpio_channel:int, 

286 signal_passed:int = 0, 

287 signal_approach:int = 0, 

288 sensor_passed:int = 0, 

289 sensor_timeout:float = 3.0, 

290 trigger_period:float = 0.001): 

291 global gpio_port_mappings 

292 # Validate the parameters we have been given as this is a library API function 

293 if not isinstance(sensor_id,int) or sensor_id < 1: 

294 logging.error("GPIO Sensor "+str(sensor_id)+": create_track_sensor - Sensor ID must be a positive integer") 

295 elif gpio_sensor_exists(sensor_id): 

296 logging.error("GPIO Sensor "+str(sensor_id)+": create_track_sensor - Sensor ID already exists") 

297 elif not isinstance(signal_passed,int): 

298 logging.error("GPIO Sensor "+str(sensor_id)+": create_track_sensor - Signal ID (passed) must be an integer") 

299 elif not isinstance(signal_approach,int): 

300 logging.error("GPIO Sensor "+str(sensor_id)+": create_track_sensor - Signal ID (approach) must be integer") 

301 elif not isinstance(sensor_passed,int): 

302 logging.error("GPIO Sensor "+str(sensor_id)+": create_track_sensor - Track Sensor ID must be an integer") 

303 elif ( (signal_passed > 0 and signal_approach > 0) or (signal_passed > 0 and sensor_passed > 0) 

304 or (signal_approach > 0 and sensor_passed > 0) ): 

305 logging.error("GPIO Sensor "+str(sensor_id)+": create_track_sensor - Multiple linked events specified") 

306 elif not isinstance(sensor_timeout,float): 

307 logging.error("GPIO Sensor "+str(sensor_id)+": create_track_sensor - Sensor timeout must be a float") 

308 elif sensor_timeout < 0.0: 

309 logging.error("GPIO Sensor "+str(sensor_id)+": create_track_sensor - Sensor timeout must be >= 0.0 seconds") 

310 elif not isinstance(trigger_period,float): 

311 logging.error("GPIO Sensor "+str(sensor_id)+": create_track_sensor - Trigger period must be a float") 

312 elif trigger_period < 0.0: 

313 logging.error("GPIO Sensor "+str(sensor_id)+": create_track_sensor - Trigger period must be >= 0.0 seconds") 

314 elif not isinstance(gpio_channel,int): 

315 logging.error("GPIO Sensor "+str(sensor_id)+": create_track_sensor - GPIO port must be integer") 

316 elif gpio_channel not in get_list_of_available_ports(): 

317 logging.error("GPIO Sensor "+str(sensor_id)+": create_track_sensor - Invalid GPIO Port "+str(gpio_channel)) 

318 elif str(gpio_channel) in gpio_port_mappings.keys(): 

319 logging.error("GPIO Sensor "+str(sensor_id)+": create_track_sensor - GPIO port "+str(gpio_channel)+" is already mapped") 

320 else: 

321 if signal_passed > 0: message = " - will trigger Signal "+str(signal_passed)+ " 'passed' event" 

322 elif signal_approach > 0: message = " - will trigger Signal "+str(signal_approach)+ " 'approach' event" 

323 elif sensor_passed > 0: message = " - will trigger Track Section "+str(sensor_passed)+ " 'passed' event" 

324 else: message = "" 

325 logging.debug("GPIO Sensor "+str(sensor_id)+": Mapping sensor to GPIO Port "+str(gpio_channel)+message) 

326 # Create the track sensor entry in the dictionary of gpio_port_mappings 

327 gpio_port_mappings[str(gpio_channel)] = {"sensor_id" : sensor_id, 

328 "signal_approach" : signal_approach, 

329 "signal_passed" : signal_passed, 

330 "sensor_passed" : sensor_passed, 

331 "trigger_period" : trigger_period, 

332 "timeout_value" : sensor_timeout, 

333 "sensor_device" : None, 

334 "timeout_start" : None, 

335 "timeout_active" : False, 

336 "sensor_state" : False} 

337 # We only create the gpiozero sensor device if we are running on a raspberry pi 

338 if running_on_raspberry_pi: 338 ↛ exitline 338 didn't return from function 'create_gpio_sensor', because the condition on line 338 was never false

339 try: 

340 sensor_device = gpiozero.Button(gpio_channel) 

341 sensor_device.when_pressed = lambda:gpio_sensor_triggered(gpio_channel) 341 ↛ exitline 341 didn't run the lambda on line 341

342 gpio_port_mappings[str(gpio_channel)]["sensor_device"] = sensor_device 

343 except: 

344 logging.error("GPIO Sensor "+str(sensor_id)+": create_track_sensor - GPIO port "+str(gpio_channel)+" cannot be mapped") 

345 

346#--------------------------------------------------------------------------------------------------- 

347# API function to delete all LOCAL GPIO sensor mappings. Called when the GPIO sensor mappings have been 

348# updated by the editor (on 'apply' of the GPIO sensor configuration). The editor will then go on to 

349# re-create all LOCAL GPIO sensors (that have a GPIO mapping defined) with their updated mappings. 

350#--------------------------------------------------------------------------------------------------- 

351 

352def delete_all_local_gpio_sensors(): 

353 global gpio_port_mappings 

354 logging.debug("GPIO Interface: Deleting all local GPIO sensor mappings") 

355 # Remove all "local" GPIO sensors from the dictionary of gpio_port_mappings (where the 

356 # key in the gpio_port_mappings dict will be a a number' rather that a remote identifier). 

357 # We don't iterate through the dictionary to remove items as it will change under us. 

358 new_gpio_port_mappings = {} 

359 for gpio_port in gpio_port_mappings: 

360 if not gpio_port.isdigit(): new_gpio_port_mappings[gpio_port] = gpio_port_mappings[gpio_port] 

361 elif running_on_raspberry_pi: gpio_port_mappings[gpio_port]["sensor_device"].close() 

362 gpio_port_mappings = new_gpio_port_mappings 

363 return() 

364 

365#--------------------------------------------------------------------------------------------------- 

366# API Function to return the current event callback mappings for a GPIO sensor 

367#--------------------------------------------------------------------------------------------------- 

368 

369def get_gpio_sensor_callback(sensor_id:Union[int,str]): 

370 callback_list = [0, 0, 0] 

371 # Validate the parameters we have been given as this is a library API function 

372 if not isinstance(sensor_id,int) and not isinstance(sensor_id,str): 

373 logging.error("GPIO Sensor "+str(sensor_id)+": get_gpio_sensor_callback - Sensor ID must be an integer or string") 

374 elif not gpio_sensor_exists(sensor_id): 

375 logging.error("GPIO Sensor "+str(sensor_id)+": get_gpio_sensor_callback - Sensor does not exist") 

376 else: 

377 str_gpio_port = mapped_gpio_port(sensor_id) 

378 signal_passed = gpio_port_mappings[str_gpio_port]["signal_passed"] 

379 signal_approach = gpio_port_mappings[str_gpio_port]["signal_approach"] 

380 sensor_passed = gpio_port_mappings[str_gpio_port]["sensor_passed"] 

381 callback_list = [signal_passed, signal_approach, sensor_passed] 

382 return(callback_list) 

383 

384#--------------------------------------------------------------------------------------------------- 

385# API Function to remove the callback behavior for existing GPIO sensors (local or remote). This 

386# function is called every time a 'signal' or an 'track sensor' object is deleted from the schematic 

387# either as part of a configuration update (where library objects are deleted and then re-created in 

388# their new state) or as part of a hard delete (where the library objects are deleted permanently). 

389# The GPIO sensor itself will still 'exist' as it will still be mapped to a GPIO input - and can then 

390# be re-allocated to another 'signal' or 'track sensor' as required. 

391# If the GPIO sensor no longer exists then the call will fail silently (this use case is where a GPIO 

392# sensor object has been deleted (either via a GPIO sensor configuration 'Apply' or MQTT pub/sub 

393# 'Apply') but is still referenced from the Signal or Track Sensor configuration. 

394#--------------------------------------------------------------------------------------------------- 

395 

396def remove_gpio_sensor_callback(sensor_id:Union[int,str]): 

397 global gpio_port_mappings 

398 # Validate the parameters we have been given as this is a library API function 

399 if not isinstance(sensor_id,int) and not isinstance(sensor_id,str): 

400 logging.error("GPIO Sensor "+str(sensor_id)+": remove_gpio_sensor_callback - Sensor ID must be an integer or string") 

401 elif not gpio_sensor_exists(sensor_id): 

402 logging.error("GPIO Sensor "+str(sensor_id)+": remove_gpio_sensor_callback - Sensor does not exist") 

403 else: 

404 logging.debug("GPIO Sensor "+str(sensor_id)+": Removing all trigger events for GPIO sensor") 

405 str_gpio_port = mapped_gpio_port(sensor_id) 

406 gpio_port_mappings[str_gpio_port]["signal_approach"] = 0 

407 gpio_port_mappings[str_gpio_port]["signal_passed"] = 0 

408 gpio_port_mappings[str_gpio_port]["sensor_passed"] = 0 

409 return() 

410 

411#--------------------------------------------------------------------------------------------------- 

412# API Function to update the callback behavior for existing GPIO sensors (local or remote). This 

413# function is called by the Editor every time a 'signal' or a 'track sensor' configuration is 

414# 'Applied' (where the mapped GPIO sensors may have changed). 

415#--------------------------------------------------------------------------------------------------- 

416 

417def add_gpio_sensor_callback (sensor_id:Union[int,str], signal_passed:int=0, 

418 signal_approach:int=0, sensor_passed:int=0): 

419 global gpio_port_mappings 

420 # Validate the parameters we have been given as this is a library API function 

421 if not isinstance(sensor_id,int) and not isinstance(sensor_id,str): 

422 logging.error("GPIO Sensor "+str(sensor_id)+": add_gpio_sensor_callback - Sensor ID must be an integer or string") 

423 elif not gpio_sensor_exists(sensor_id): 

424 logging.error("GPIO Sensor "+str(sensor_id)+": add_gpio_sensor_callback - Sensor ID does not exist") 

425 elif not isinstance(signal_passed,int) or not isinstance(signal_approach,int) or not isinstance(sensor_passed,int): 

426 logging.error("GPIO Sensor "+str(sensor_id)+": add_gpio_sensor_callback - Linked Item IDs must be integers") 

427 elif ( (signal_passed > 0 and signal_approach > 0) or (signal_passed > 0 and sensor_passed > 0) 

428 or (signal_approach > 0 and sensor_passed > 0) ): 

429 logging.error("GPIO Sensor "+str(sensor_id)+": add_gpio_sensor_callback - More than one trigger event specified") 

430 else: 

431 # Add the appropriate callback event to the GPIO Sensor configuration 

432 str_gpio_port = mapped_gpio_port(sensor_id) 

433 if signal_passed > 0: 

434 logging.debug("GPIO Sensor "+str(sensor_id)+": Adding 'passed' event for Signal "+str(signal_passed)) 

435 elif signal_approach > 0: 

436 logging.debug("GPIO Sensor "+str(sensor_id)+": Adding 'approach' event for Signal "+str(signal_approach)) 

437 elif sensor_passed > 0: 

438 logging.debug("GPIO Sensor "+str(sensor_id)+": Adding 'passed' event for Track Section "+str(sensor_passed)) 

439 gpio_port_mappings[str_gpio_port]["signal_passed"] = signal_passed 

440 gpio_port_mappings[str_gpio_port]["signal_approach"] = signal_approach 

441 gpio_port_mappings[str_gpio_port]["sensor_passed"] = sensor_passed 

442 return() 

443 

444#--------------------------------------------------------------------------------------------------- 

445# Function called on shutdown to set the GPIO ports back to their default states 

446#--------------------------------------------------------------------------------------------------- 

447 

448def gpio_shutdown(): 

449 if running_on_raspberry_pi: 449 ↛ 455line 449 didn't jump to line 455, because the condition on line 449 was never false

450 time.sleep(0.001) # Allow any threads to terminate gracefully 

451 logging.debug("GPIO Interface: Restoring default settings") 

452 # Close all the gpiozero Button objects to reset the GPIO interface 

453 for gpio_port in gpio_port_mappings: 

454 if gpio_port.isdigit(): gpio_port_mappings[gpio_port]["sensor_device"].close() 

455 return() 

456 

457#--------------------------------------------------------------------------------------------------- 

458# API function to reset the list of published/subscribed GPIO sensors. This function is called by 

459# the editor on 'Apply' of the MQTT pub/sub configuration prior to applying the new configuration 

460# via the 'set_gpio_sensors_to_publish_state' & 'subscribe_to_gpio_sensor_updates' functions. 

461#--------------------------------------------------------------------------------------------------- 

462 

463def reset_mqtt_configuration(): 

464 global gpio_port_mappings 

465 global list_of_track_sensors_to_publish 

466 logging.debug("GPIO Interface: Resetting MQTT publish and subscribe configuration") 

467 # Clear the list_of_track_sensors_to_publish to stop GPIO sensor events being published 

468 list_of_track_sensors_to_publish.clear() 

469 # Unsubscribe from all topics associated with the message_type 

470 mqtt_interface.unsubscribe_from_message_type("gpio_sensor_event") 

471 # Remove all "remote" GPIO sensors from the dictionary of gpio_port_mappings (where the 

472 # key in the gpio_port_mappings dict will be the remote identifier rather than a number). 

473 # We don't iterate through the dictionary to remove items as it will change under us. 

474 new_gpio_port_mappings = {} 

475 for gpio_port in gpio_port_mappings: 

476 if gpio_port.isdigit(): new_gpio_port_mappings[gpio_port] = gpio_port_mappings[gpio_port] 

477 gpio_port_mappings = new_gpio_port_mappings 

478 return() 

479 

480#--------------------------------------------------------------------------------------------------- 

481# API function to configure local GPIO sensors to publish 'sensor triggered' events to remote MQTT 

482# nodes. This function is called by the editor on 'Apply' of the MQTT pub/sub configuration. Note 

483# the configuration can be applied independently to whether the gpio sensors 'exist' or not. 

484#--------------------------------------------------------------------------------------------------- 

485 

486def set_gpio_sensors_to_publish_state(*sensor_ids:int): 

487 global list_of_track_sensors_to_publish 

488 for sensor_id in sensor_ids: 

489 # Validate the parameters we have been given as this is a library API function 

490 if not isinstance(sensor_id,int) or sensor_id < 1: 

491 logging.error("GPIO Sensor "+str(sensor_id)+": set_gpio_sensors_to_publish_state - ID must be a positive integer") 

492 elif sensor_id in list_of_track_sensors_to_publish: 

493 logging.warning("GPIO Sensor "+str(sensor_id)+": set_gpio_sensors_to_publish_state -" 

494 +" Sensor is already configured to publish state to MQTT broker") 

495 else: 

496 # Add the GPIO sensor to the list_of_track_sensors_to_publish to enable publishing 

497 logging.debug("GPIO Sensor "+str(sensor_id)+": Configuring to publish 'sensor triggered' events to MQTT broker") 

498 list_of_track_sensors_to_publish.append(sensor_id) 

499 return() 

500 

501#--------------------------------------------------------------------------------------------------- 

502# API Function to "subscribe" to remote GPIO sensor triggers (published by other MQTT Nodes) 

503# and map the appropriate callback event (for the Signal or Track Sensor objects on the local 

504# schematic. This function is called by the editor on "Apply' of the MQTT pub/sub configuration 

505# for all subscribed remote GPIO sensors. 

506#--------------------------------------------------------------------------------------------------- 

507 

508def subscribe_to_remote_gpio_sensor(remote_id:str, signal_passed:int=0, 

509 signal_approach:int=0, sensor_passed:int=0): 

510 global gpio_port_mappings 

511 # Validate the parameters we have been given as this is a library API function 

512 if not isinstance(remote_id,str): 

513 logging.error("GPIO Sensor "+str(remote_id)+": subscribe_to_remote_gpio_sensor - Remote ID must be a string") 

514 elif mqtt_interface.split_remote_item_identifier(remote_id) is None: 

515 logging.error("GPIO Sensor "+remote_id+": subscribe_to_remote_gpio_sensor - Remote ID is an invalid format") 

516 elif not isinstance(signal_passed,int): 

517 logging.error("GPIO Sensor "+remote_id+": subscribe_to_remote_gpio_sensor - Signal ID (passed) must be an integer") 

518 elif not isinstance(signal_approach,int): 

519 logging.error("GPIO Sensor "+remote_id+": subscribe_to_remote_gpio_sensor - Signal ID (approach) must be integer") 

520 elif not isinstance(sensor_passed,int): 

521 logging.error("GPIO Sensor "+remote_id+": subscribe_to_remote_gpio_sensor - Track Sensor ID must be an integer") 

522 elif ( (signal_passed > 0 and signal_approach > 0) or (signal_passed > 0 and sensor_passed > 0) 

523 or (signal_approach > 0 and sensor_passed > 0) ): 

524 logging.error("GPIO Sensor "+remote_id+": subscribe_to_remote_gpio_sensor - More than one trigger event specified") 

525 else: 

526 if signal_passed > 0: event = " - will trigger Signal "+str(signal_passed)+ " 'passed' event" 

527 elif signal_approach > 0: event = " - will trigger Signal "+str(signal_approach)+ " 'approach' event" 

528 elif sensor_passed > 0: event = " - will trigger Track Section "+str(sensor_passed)+ " 'passed' event" 

529 else: event = "" 

530 logging.debug("GPIO Sensor "+str(remote_id)+": Subscribing to remote GPIO sensor"+event) 

531 sensor_already_subscribed = gpio_sensor_exists(remote_id) 

532 # Create (or update) the dummy GPIO port mapping to hold the callback information for the remote sensor 

533 # Rather than use the mapped GPIO port number as the dictionary key we will use the remote Sensor ID 

534 gpio_port_mappings[remote_id] = {"sensor_id" : remote_id, 

535 "signal_approach" : signal_approach, 

536 "signal_passed" : signal_passed, 

537 "sensor_passed" : sensor_passed} 

538 # Only subscribe to events from the remote GPIO sensor if we are not already subscribed 

539 if sensor_already_subscribed: 

540 logging.warning("GPIO Sensor "+remote_id+": subscribe_to_remote_gpio_sensor - Already subscribed") 

541 else: 

542 [node_id, item_id] = mqtt_interface.split_remote_item_identifier(remote_id) 

543 mqtt_interface.subscribe_to_mqtt_messages("gpio_sensor_event", node_id, item_id, 

544 handle_mqtt_gpio_sensor_triggered_event) 

545 return() 

546 

547####################################################################################################