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
« 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#---------------------------------------------------------------------------------------------------
76import time
77import threading
78import logging
79from typing import Union
81from . import common
82from . import signals_common
83from . import track_sensors
84from . import mqtt_interface
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#---------------------------------------------------------------------------------------------------
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)
101running_on_raspberry_pi = is_running_on_raspberry_pi()
103#---------------------------------------------------------------------------------------------------
104# API Function for external modules to test if GPIO inputs are supported by the platform
105#---------------------------------------------------------------------------------------------------
107def gpio_interface_enabled():
108 return (running_on_raspberry_pi)
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#---------------------------------------------------------------------------------------------------
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])
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#---------------------------------------------------------------------------------------------------
133gpio_port_mappings: dict = {}
135#---------------------------------------------------------------------------------------------------
136# Global list of track GPIO Sensor IDs (integers) to publish to the MQTT Broker
137#---------------------------------------------------------------------------------------------------
139list_of_track_sensors_to_publish=[]
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#---------------------------------------------------------------------------------------------------
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")
152#---------------------------------------------------------------------------------------------------
153# API Function to check if a sensor exists (either mapped or subscribed to via mqtt metworking)
154#---------------------------------------------------------------------------------------------------
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)
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#---------------------------------------------------------------------------------------------------
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()
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#---------------------------------------------------------------------------------------------------
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()
231#---------------------------------------------------------------------------------------------------
232# Internal function for building and sending MQTT messages (if configured to publish)
233#---------------------------------------------------------------------------------------------------
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()
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#---------------------------------------------------------------------------------------------------
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()
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#---------------------------------------------------------------------------------------------------
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()
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#---------------------------------------------------------------------------------------------------
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")
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#---------------------------------------------------------------------------------------------------
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()
365#---------------------------------------------------------------------------------------------------
366# API Function to return the current event callback mappings for a GPIO sensor
367#---------------------------------------------------------------------------------------------------
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)
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#---------------------------------------------------------------------------------------------------
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()
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#---------------------------------------------------------------------------------------------------
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()
444#---------------------------------------------------------------------------------------------------
445# Function called on shutdown to set the GPIO ports back to their default states
446#---------------------------------------------------------------------------------------------------
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()
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#---------------------------------------------------------------------------------------------------
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()
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#---------------------------------------------------------------------------------------------------
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()
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#---------------------------------------------------------------------------------------------------
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()
547####################################################################################################