Coverage for /home/pi/Software/model-railway-signalling/model_railway_signals/library/signals_common.py: 96%
318 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 contains all of the parameters, funcions and classes that
3# are used across more than one signal type
4# -------------------------------------------------------------------------
6from . import common
7from . import dcc_control
8from . import mqtt_interface
9from . import signals_colour_lights
10from . import signals_semaphores
11from . import signals_ground_position
12from . import signals_ground_disc
14from typing import Union
15import tkinter as Tk
16import logging
17import enum
19# -------------------------------------------------------------------------
20# Global Classes to be used externally when creating/updating signals or
21# processing button change events - Will apply to more that one signal type
22# -------------------------------------------------------------------------
24# Define the routes that a signal can support. Applies to colour light signals
25# with feather route indicators and semaphores (where the "routes" are represented
26# by subsidary "arms" on brackets either side of the main signal arm
27class route_type(enum.Enum):
28 NONE = 0 # internal use - to "inhibit" route indications when signal is at DANGER)
29 MAIN = 1 # Main route
30 LH1 = 2 # immediate left
31 LH2 = 3 # far left
32 RH1 = 4 # immediate right
33 RH2 = 5 # far right
35# Define the different callbacks types for the signal
36# Used for identifying the event that has triggered the callback
37class sig_callback_type(enum.Enum):
38 sig_switched = 1 # The signal has been switched by the user
39 sub_switched = 2 # The subsidary signal has been switched by the user
40 sig_passed = 3 # The "signal passed" has been activated by the user
41 sig_updated = 4 # The signal aspect has been changed/updated via an override
42 sig_released = 5 # The signal has been "released" on the approach of a train
44# -------------------------------------------------------------------------
45# Global Classes used internally/externally when creating/updating signals or
46# processing button change events - Will apply to more that one signal type
47# -------------------------------------------------------------------------
49# The superset of Possible states (displayed aspects) for a signal
50# CAUTION_APROACH_CONTROL represents approach control set with "Release On Yellow"
51class signal_state_type(enum.Enum):
52 DANGER = 1
53 PROCEED = 2
54 CAUTION = 3
55 CAUTION_APP_CNTL = 4
56 PRELIM_CAUTION = 5
57 FLASH_CAUTION = 6
58 FLASH_PRELIM_CAUTION = 7
60# Define the main signal types that can be created
61class sig_type(enum.Enum):
62 remote_signal = 0
63 colour_light = 1
64 ground_position = 2
65 semaphore = 3
66 ground_disc = 4
68# -------------------------------------------------------------------------
69# Signals are to be added to a global dictionary when created
70# -------------------------------------------------------------------------
72signals:dict = {}
74# -------------------------------------------------------------------------
75# Global lists for Signals configured to publish events to the MQTT Broker
76# -------------------------------------------------------------------------
78list_of_signals_to_publish_state_changes=[]
80# -------------------------------------------------------------------------
81# Common Function to check if a Signal exists in the dictionary of Signals
82# Used by most externally-called functions to validate the Sig_ID. We allow
83# a string or an int to be passed in to cope with compound signal identifiers
84# This to support identifiers containing the node and ID of a remote signal
85# -------------------------------------------------------------------------
87def sig_exists(sig_id:Union[int,str]):
88 return (str(sig_id) in signals.keys() )
90# -------------------------------------------------------------------------
91# Define a null callback function for internal use
92# -------------------------------------------------------------------------
94def null_callback (sig_id:int,callback_type):
95 return (sig_id,callback_type)
97# -------------------------------------------------------------------------
98# Callbacks for processing button pushes
99# -------------------------------------------------------------------------
101def signal_button_event (sig_id:int):
102 logging.info("Signal "+str(sig_id)+": Signal Change Button Event *************************************************")
103 # toggle the signal state and refresh the signal
104 toggle_signal(sig_id)
105 auto_refresh_signal(sig_id)
106 # Make the external callback (if one was specified at signal creation time)
107 signals[str(sig_id)]['extcallback'] (sig_id,sig_callback_type.sig_switched)
108 return ()
110def subsidary_button_event (sig_id:int):
111 logging.info("Signal "+str(sig_id)+": Subsidary Change Button Event **********************************************")
112 toggle_subsidary(sig_id)
113 # call the signal type-specific functions to update the signal
114 if signals[str(sig_id)]["sigtype"] == sig_type.colour_light:
115 signals_colour_lights.update_colour_light_subsidary(sig_id)
116 elif signals[str(sig_id)]["sigtype"] == sig_type.semaphore: 116 ↛ 119line 116 didn't jump to line 119, because the condition on line 116 was never false
117 signals_semaphores.update_semaphore_subsidary_arms(sig_id)
118 # Make the external callback (if one was specified at signal creation time)
119 signals[str(sig_id)]['extcallback'] (sig_id,sig_callback_type.sub_switched)
120 return ()
122def reset_sig_passed_button (sig_id:int):
123 if sig_exists(sig_id): signals[str(sig_id)]["passedbutton"].config(bg=common.bgraised)
125def reset_sig_released_button (sig_id:int):
126 if sig_exists(sig_id): signals[str(sig_id)]["releasebutton"].config(bg=common.bgraised)
128def sig_passed_button_event (sig_id:int):
129 if not sig_exists(sig_id):
130 logging.error("Signal "+str(sig_id)+": sig_passed_button_event - signal does not exist")
131 else:
132 logging.info("Signal "+str(sig_id)+": Signal Passed Event **********************************************")
133 # Pulse the signal passed button to provide a visual indication (but not if a shutdown has been initiated)
134 if not common.shutdown_initiated: 134 ↛ 138line 134 didn't jump to line 138, because the condition on line 134 was never false
135 signals[str(sig_id)]["passedbutton"].config(bg="red")
136 common.root_window.after(1000,lambda:reset_sig_passed_button(sig_id))
137 # Reset the approach control 'released' state (if the signal supports approach control)
138 if ( signals[str(sig_id)]["sigtype"] == sig_type.colour_light or
139 signals[str(sig_id)]["sigtype"] == sig_type.semaphore ):
140 signals[str(sig_id)]["released"] = False
141 # Make the external callback (if one was specified at signal creation time)
142 signals[str(sig_id)]['extcallback'] (sig_id,sig_callback_type.sig_passed)
143 return ()
145def approach_release_button_event (sig_id:int):
146 if not sig_exists(sig_id):
147 logging.error("Signal "+str(sig_id)+": approach_release_button_event - signal does not exist")
148 else:
149 logging.info("Signal "+str(sig_id)+": Approach Release Event *******************************************")
150 # Pulse the approach release button to provide a visual indication (but not if a shutdown has been initiated)
151 if not common.shutdown_initiated: 151 ↛ 155line 151 didn't jump to line 155, because the condition on line 151 was never false
152 signals[str(sig_id)]["releasebutton"].config(bg="red")
153 common.root_window.after(1000,lambda:reset_sig_released_button(sig_id))
154 # Set the approach control 'released' state (if the signal supports approach control)
155 if ( signals[str(sig_id)]["sigtype"] == sig_type.colour_light or 155 ↛ 159line 155 didn't jump to line 159, because the condition on line 155 was never false
156 signals[str(sig_id)]["sigtype"] == sig_type.semaphore ):
157 signals[str(sig_id)]["released"] = True
158 # Clear the approach control and refresh the signal
159 clear_approach_control(sig_id)
160 auto_refresh_signal(sig_id)
161 # Make the external callback (if one was specified at signal creation time)
162 signals[str(sig_id)]['extcallback'] (sig_id,sig_callback_type.sig_released)
163 return ()
165# -------------------------------------------------------------------------
166# Common function to refreh a signal following a change in state
167# -------------------------------------------------------------------------
169def auto_refresh_signal(sig_id:int):
170 # call the signal type-specific functions to update the signal (note that we only update
171 # Semaphore and colour light signals if they are configured to update immediately)
172 if signals[str(sig_id)]["sigtype"] == sig_type.colour_light:
173 if signals[str(sig_id)]["refresh"]: signals_colour_lights.update_colour_light_signal(sig_id)
174 elif signals[str(sig_id)]["sigtype"] == sig_type.ground_position:
175 signals_ground_position.update_ground_position_signal (sig_id)
176 elif signals[str(sig_id)]["sigtype"] == sig_type.semaphore:
177 if signals[str(sig_id)]["refresh"]: signals_semaphores.update_semaphore_signal(sig_id)
178 elif signals[str(sig_id)]["sigtype"] == sig_type.ground_disc: 178 ↛ 180line 178 didn't jump to line 180, because the condition on line 178 was never false
179 signals_ground_disc.update_ground_disc_signal(sig_id)
180 return()
182# -------------------------------------------------------------------------
183# Common function to flip the internal state of a signal
184# -------------------------------------------------------------------------
186def toggle_signal (sig_id:int):
187 global signals
188 # Update the state of the signal button - Common to ALL signal types
189 # The Signal Clear boolean value will always be either True or False
190 if signals[str(sig_id)]["sigclear"]:
191 logging.info ("Signal "+str(sig_id)+": Toggling signal to ON")
192 signals[str(sig_id)]["sigclear"] = False
193 if not signals[str(sig_id)]["automatic"]: 193 ↛ 202line 193 didn't jump to line 202, because the condition on line 193 was never false
194 signals[str(sig_id)]["sigbutton"].config(bg=common.bgraised)
195 signals[str(sig_id)]["sigbutton"].config(relief="raised")
196 else:
197 logging.info ("Signal "+str(sig_id)+": Toggling signal to OFF")
198 signals[str(sig_id)]["sigclear"] = True
199 if not signals[str(sig_id)]["automatic"]:
200 signals[str(sig_id)]["sigbutton"].config(relief="sunken")
201 signals[str(sig_id)]["sigbutton"].config(bg=common.bgsunken)
202 return ()
204# -------------------------------------------------------------------------
205# Common function to flip the internal state of a subsidary signal
206# -------------------------------------------------------------------------
208def toggle_subsidary (sig_id:int):
209 global signals
210 # Update the state of the subsidary button - Common to ALL signal types.
211 # The subsidary clear boolean value will always be either True or False
212 if signals[str(sig_id)]["subclear"]:
213 logging.info ("Signal "+str(sig_id)+": Toggling subsidary to ON")
214 signals[str(sig_id)]["subclear"] = False
215 signals[str(sig_id)]["subbutton"].config(relief="raised",bg=common.bgraised)
216 else:
217 logging.info ("Signal "+str(sig_id)+": Toggling subsidary to OFF")
218 signals[str(sig_id)]["subclear"] = True
219 signals[str(sig_id)]["subbutton"].config(relief="sunken",bg=common.bgsunken)
220 return ()
222# -------------------------------------------------------------------------
223# Common function to Set the approach control mode for a signal
224# (shared by Colour Light and semaphore signal types)
225# -------------------------------------------------------------------------
227def set_approach_control (sig_id:int, release_on_yellow:bool = False, force_set:bool = True):
228 global signals
229 # Only set approach control if the signal is not in the period between
230 # 'released' and 'passed' events (unless the force_reset flag is set)
231 if force_set or not signals[str(sig_id)]["released"]:
232 # Give an indication that the approach control has been set for the signal
233 signals[str(sig_id)]["sigbutton"].config(font=('Courier',common.fontsize,"underline"))
234 # Only set approach control if it is not already set for the signal
235 if release_on_yellow and not signals[str(sig_id)]["releaseonyel"]:
236 logging.info ("Signal "+str(sig_id)+": Setting approach control (release on yellow)")
237 signals[str(sig_id)]["releaseonyel"] = True
238 signals[str(sig_id)]["releaseonred"] = False
239 elif not release_on_yellow and not signals[str(sig_id)]["releaseonred"]:
240 logging.info ("Signal "+str(sig_id)+": Setting approach control (release on red)")
241 signals[str(sig_id)]["releaseonred"] = True
242 signals[str(sig_id)]["releaseonyel"] = False
243 # Reset the signal into it's 'not released' state
244 signals[str(sig_id)]["released"] = False
245 return()
247#-------------------------------------------------------------------------
248# Common function to Clear the approach control mode for a signal
249# (shared by Colour Light and semaphore signal types)
250# -------------------------------------------------------------------------
252def clear_approach_control (sig_id:int):
253 global signals
254 # Only Clear approach control if it is currently set for the signal
255 if signals[str(sig_id)]["releaseonred"] or signals[str(sig_id)]["releaseonyel"]:
256 logging.info ("Signal "+str(sig_id)+": Clearing approach control")
257 signals[str(sig_id)]["releaseonyel"] = False
258 signals[str(sig_id)]["releaseonred"] = False
259 signals[str(sig_id)]["sigbutton"].config(font=('Courier',common.fontsize,"normal"))
260 return()
262# -------------------------------------------------------------------------
263# Common Function to set a signal override
264# -------------------------------------------------------------------------
266def set_signal_override (sig_id:int):
267 global signals
268 # Only set the override if the signal is not already overridden
269 if not signals[str(sig_id)]["override"]:
270 logging.info ("Signal "+str(sig_id)+": Setting override")
271 # Set the override state and change the button text to indicate override
272 signals[str(sig_id)]["override"] = True
273 signals[str(sig_id)]["sigbutton"].config(fg="red", disabledforeground="red")
274 return()
276# -------------------------------------------------------------------------
277# Common Function to clear a signal override
278# -------------------------------------------------------------------------
280def clear_signal_override (sig_id:int):
281 global signals
282 # Only clear the override if the signal is already overridden
283 if signals[str(sig_id)]["override"]:
284 logging.info ("Signal "+str(sig_id)+": Clearing override")
285 # Clear the override and change the button colour
286 signals[str(sig_id)]["override"] = False
287 signals[str(sig_id)]["sigbutton"].config(fg="black",disabledforeground="grey50")
288 return()
290# -------------------------------------------------------------------------
291# Common Function to set a signal override
292# -------------------------------------------------------------------------
294def set_signal_override_caution (sig_id:int):
295 global signals
296 # Only set the override if the signal is not already overridden
297 if not signals[str(sig_id)]["overcaution"]:
298 logging.info ("Signal "+str(sig_id)+": Setting override CAUTION")
299 signals[str(sig_id)]["overcaution"] = True
300 return()
302# -------------------------------------------------------------------------
303# Common Function to clear a signal override
304# -------------------------------------------------------------------------
306def clear_signal_override_caution (sig_id:int):
307 global signals
308 # Only clear the override if the signal is already overridden
309 if signals[str(sig_id)]["overcaution"]:
310 logging.info ("Signal "+str(sig_id)+": Clearing override CAUTION")
311 signals[str(sig_id)]["overcaution"] = False
312 return()
314# -------------------------------------------------------------------------
315# Common Function to lock a signal (i.e. for point/signal interlocking)
316# -------------------------------------------------------------------------
318def lock_signal (sig_id:int):
319 global signals
320 # Only lock if it is currently unlocked
321 if not signals[str(sig_id)]["siglocked"]:
322 logging.info ("Signal "+str(sig_id)+": Locking signal")
323 # If signal/point locking has been correctly implemented it should
324 # only be possible to lock a signal that is "ON" (i.e. at DANGER)
325 if signals[str(sig_id)]["sigclear"]: 325 ↛ 326line 325 didn't jump to line 326, because the condition on line 325 was never true
326 logging.warning ("Signal "+str(sig_id)+": Signal to lock is OFF - Locking Anyway")
327 # Disable the Signal button to lock it
328 signals[str(sig_id)]["sigbutton"].config(state="disabled")
329 signals[str(sig_id)]["siglocked"] = True
330 return()
332# -------------------------------------------------------------------------
333# Common Function to unlock a signal (i.e. for point/signal interlocking)
334# -------------------------------------------------------------------------
336def unlock_signal (sig_id:int):
337 global signals
338 # Only unlock if it is currently locked
339 if signals[str(sig_id)]["siglocked"]:
340 logging.info ("Signal "+str(sig_id)+": Unlocking signal")
341 # Enable the Signal button to unlock it (if its not a fully automatic signal)
342 if not signals[str(sig_id)]["automatic"]: 342 ↛ 344line 342 didn't jump to line 344, because the condition on line 342 was never false
343 signals[str(sig_id)]["sigbutton"].config(state="normal")
344 signals[str(sig_id)]["siglocked"] = False
345 return()
347# -------------------------------------------------------------------------
348# Common Function to lock a subsidary (i.e. for point/signal interlocking)
349# -------------------------------------------------------------------------
351def lock_subsidary (sig_id:int):
352 global signals
353 # Only lock if it is currently unlocked
354 if not signals[str(sig_id)]["sublocked"]:
355 logging.info ("Signal "+str(sig_id)+": Locking subsidary")
356 # If signal/point locking has been correctly implemented it should
357 # only be possible to lock a signal that is "ON" (i.e. at DANGER)
358 if signals[str(sig_id)]["subclear"]: 358 ↛ 359line 358 didn't jump to line 359, because the condition on line 358 was never true
359 logging.warning ("Signal "+str(sig_id)+": Subsidary signal to lock is OFF - Locking anyway")
360 # Disable the Button to lock the subsidary signal
361 signals[str(sig_id)]["subbutton"].config(state="disabled")
362 signals[str(sig_id)]["sublocked"] = True
363 return()
365# -------------------------------------------------------------------------
366# Common Function to unlock a subsidary (i.e. for point/signal interlocking)
367# -------------------------------------------------------------------------
369def unlock_subsidary (sig_id:int):
370 global signals
371 # Only unlock if it is currently locked
372 if signals[str(sig_id)]["sublocked"]:
373 logging.info ("Signal "+str(sig_id)+": Unlocking subsidary")
374 # Re-enable the Button to unlock the subsidary signal
375 signals[str(sig_id)]["subbutton"].config(state="normal")
376 signals[str(sig_id)]["sublocked"] = False
377 return()
379# -------------------------------------------------------------------------
380# Common Function to generate all the mandatory signal elements that will apply
381# to all signal types (even if they are not used by the particular signal type)
382# -------------------------------------------------------------------------
384def create_common_signal_elements (canvas,
385 sig_id: int,
386 x:int, y:int,
387 signal_type:sig_type,
388 ext_callback,
389 orientation:int,
390 subsidary:bool=False,
391 sig_passed_button:bool=False,
392 automatic:bool=False,
393 distant_button_offset:int=0,
394 tag:str=""):
395 global signals
396 # If no callback has been specified, use the null callback to do nothing
397 if ext_callback is None: ext_callback = null_callback
398 # Assign the button labels. if a distant_button_offset has been defined then this represents the
399 # special case of a semaphore distant signal being created on the same "post" as a semaphore
400 # home signal. On this case we label the button as "D" to differentiate it from the main
401 # home signal button and then apply the offset to deconflict with the home signal buttons
402 if distant_button_offset !=0 : main_button_text = "D"
403 elif sig_id < 10: main_button_text = "0" + str(sig_id)
404 else: main_button_text = str(sig_id)
405 # Create the Signal and Subsidary Button objects and their callbacks
406 sig_button = Tk.Button (canvas, text=main_button_text, padx=common.xpadding, pady=common.ypadding, 406 ↛ exitline 406 didn't jump to the function exit
407 state="normal", relief="raised", font=('Courier',common.fontsize,"normal"),
408 bg=common.bgraised, command=lambda:signal_button_event(sig_id))
409 sub_button = Tk.Button (canvas, text="S", padx=common.xpadding, pady=common.ypadding, 409 ↛ exitline 409 didn't jump to the function exit
410 state="normal", relief="raised", font=('Courier',common.fontsize,"normal"),
411 bg=common.bgraised, command=lambda:subsidary_button_event(sig_id))
412 # Signal Passed Button - We only want a small button - hence a small font size
413 passed_button = Tk.Button (canvas,text="O",padx=1,pady=1,font=('Courier',2,"normal"), 413 ↛ exitline 413 didn't jump to the function exit
414 command=lambda:sig_passed_button_event(sig_id))
415 # Create the 'windows' in which the buttons are displayed. The Subsidary Button is "hidden"
416 # if the signal doesn't have an associated subsidary. The Button positions are adjusted
417 # accordingly so they always remain in the "right" position relative to the signal
418 # Note that we have to cater for the special case of a semaphore distant signal being
419 # created on the same post as a semaphore home signal. In this case (signified by a
420 # distant_button_offset), we apply the offset to deconflict with the home signal buttons.
421 if distant_button_offset != 0:
422 button_position = common.rotate_point (x,y,distant_button_offset,-14,orientation)
423 if not automatic: canvas.create_window(button_position,window=sig_button,tags=tag)
424 else: canvas.create_window(button_position,window=sig_button,state='hidden',tags=tag)
425 canvas.create_window(button_position,window=sub_button,state='hidden',tags=tag)
426 elif subsidary:
427 if orientation == 0: button_position = common.rotate_point (x,y,-22,-14,orientation)
428 else: button_position = common.rotate_point (x,y,-35,-20,orientation)
429 canvas.create_window(button_position,anchor=Tk.E,window=sig_button,tags=tag)
430 canvas.create_window(button_position,anchor=Tk.W,window=sub_button,tags=tag)
431 else:
432 button_position = common.rotate_point (x,y,-17,-14,orientation)
433 canvas.create_window(button_position,window=sig_button,tags=tag)
434 canvas.create_window(button_position,window=sub_button,state='hidden',tags=tag)
435 # Signal passed button is created on the track at the base of the signal
436 if sig_passed_button:
437 canvas.create_window(x,y,window=passed_button,tags=tag)
438 else:
439 canvas.create_window(x,y,window=passed_button,state='hidden',tags=tag)
440 # Disable the main signal button if the signal is fully automatic
441 if automatic: sig_button.config(state="disabled",relief="sunken",bg=common.bgraised,bd=0)
442 # Create an initial dictionary entry for the signal and add all the mandatory signal elements
443 signals[str(sig_id)] = {}
444 signals[str(sig_id)]["canvas"] = canvas # MANDATORY - canvas object
445 signals[str(sig_id)]["sigtype"] = signal_type # MANDATORY - Type of the signal
446 signals[str(sig_id)]["automatic"] = automatic # MANDATORY - True = signal is fully automatic
447 signals[str(sig_id)]["extcallback"] = ext_callback # MANDATORY - The External Callback to use for the signal
448 signals[str(sig_id)]["routeset"] = route_type.MAIN # MANDATORY - Route setting for signal (MAIN at creation)
449 signals[str(sig_id)]["sigclear"] = False # MANDATORY - State of the main signal control (ON/OFF)
450 signals[str(sig_id)]["override"] = False # MANDATORY - Signal is "Overridden" to most restrictive aspect
451 signals[str(sig_id)]["overcaution"] = False # MANDATORY - Signal is "Overridden" to CAUTION
452 signals[str(sig_id)]["sigstate"] = None # MANDATORY - Displayed 'aspect' of the signal (None on creation)
453 signals[str(sig_id)]["hassubsidary"] = subsidary # MANDATORY - Whether the signal has a subsidary aspect or arms
454 signals[str(sig_id)]["subclear"] = False # MANDATORY - State of the subsidary sgnal control (ON/OFF - or None)
455 signals[str(sig_id)]["siglocked"] = False # MANDATORY - State of signal interlocking
456 signals[str(sig_id)]["sublocked"] = False # MANDATORY - State of subsidary interlocking
457 signals[str(sig_id)]["sigbutton"] = sig_button # MANDATORY - Button Drawing object (main Signal)
458 signals[str(sig_id)]["subbutton"] = sub_button # MANDATORY - Button Drawing object (main Signal)
459 signals[str(sig_id)]["passedbutton"] = passed_button # MANDATORY - Button drawing object (subsidary signal)
460 return()
462# -------------------------------------------------------------------------
463# Common Function to generate all the signal elements for Approach Control
464# (shared by Colour Light and semaphore signal types)
465# -------------------------------------------------------------------------
467def create_approach_control_elements (canvas,sig_id:int,
468 x:int,y:int,
469 orientation:int,
470 approach_button:bool):
471 global signals
472 # Define the "Tag" for all drawing objects for this signal instance
473 tag = "signal"+str(sig_id)
474 # Create the approach release button - We only want a small button - hence a small font size
475 approach_release_button = Tk.Button(canvas,text="O",padx=1,pady=1,font=('Courier',2,"normal"), 475 ↛ exitline 475 didn't jump to the function exit
476 command=lambda:approach_release_button_event (sig_id))
477 button_position = common.rotate_point(x,y,-50,0,orientation)
478 if approach_button:
479 canvas.create_window(button_position,window=approach_release_button,tags=tag)
480 else:
481 canvas.create_window(button_position,window=approach_release_button,state="hidden",tags=tag)
482 # Add the Theatre elements to the dictionary of signal objects
483 signals[str(sig_id)]["released"] = False # SHARED - State between 'released' and 'passed' events
484 signals[str(sig_id)]["releaseonred"] = False # SHARED - State of the "Approach Release for the signal
485 signals[str(sig_id)]["releaseonyel"] = False # SHARED - State of the "Approach Release for the signal
486 signals[str(sig_id)]["releasebutton"] = approach_release_button # SHARED - Button drawing object
487 return()
489# -------------------------------------------------------------------------
490# Common Function to generate all the signal elements for a theatre route
491# display (shared by Colour Light and semaphore signal types)
492# -------------------------------------------------------------------------
494def create_theatre_route_elements (canvas,sig_id:int,
495 x:int,y:int,
496 xoff:int,yoff:int,
497 orientation:int,
498 has_theatre:bool):
499 global signals
500 # Define the "Tag" for all drawing objects for this signal instance
501 tag = "signal"+str(sig_id)
502 # Draw the theatre route indicator box only if one is specified for this particular signal
503 # The text object is created anyway - but 'hidden' if not required for this particular signal
504 text_coordinates = common.rotate_point(x,y,xoff,yoff,orientation)
505 tag = "signal"+str(sig_id)
506 if has_theatre:
507 rectangle_coords = common.rotate_line(x,y,xoff-10,yoff+8,xoff+10,yoff-8,orientation)
508 canvas.create_rectangle(rectangle_coords,fill="black",tags=tag)
509 theatre_text = canvas.create_text(text_coordinates,fill="white",text="",angle=orientation-90,state='normal',tags=tag)
510 else:
511 theatre_text = canvas.create_text(text_coordinates,fill="white",text="",angle=orientation-90,state='hidden',tags=tag)
512 # Add the Theatre elements to the dictionary of signal objects
513 signals[str(sig_id)]["theatretext"] = "NONE" # SHARED - Initial Theatre Text to display (none)
514 signals[str(sig_id)]["hastheatre"] = has_theatre # SHARED - Whether the signal has a theatre display or not
515 signals[str(sig_id)]["theatreobject"] = theatre_text # SHARED - Text drawing object
516 signals[str(sig_id)]["theatreenabled"] = None # SHARED - State of the Theatre display (None at creation)
517 return()
519# -------------------------------------------------------------------------
520# Common function to change the theatre route indication
521# (shared by Colour Light and semaphore signal types)
522# -------------------------------------------------------------------------
524def update_theatre_route_indication (sig_id,theatre_text:str):
525 global signals
526 # Only update the Theatre route indication if one exists for the signal
527 if signals[str(sig_id)]["hastheatre"]:
528 # Deal with route changes (if a new route has been passed in) - but only if the theatre text has changed
529 if theatre_text != signals[str(sig_id)]["theatretext"]:
530 signals[str(sig_id)]["canvas"].itemconfig(signals[str(sig_id)]["theatreobject"],text=theatre_text)
531 signals[str(sig_id)]["theatretext"] = theatre_text
532 if signals[str(sig_id)]["theatreenabled"] == True:
533 logging.info ("Signal "+str(sig_id)+": Changing theatre route display to \'" + theatre_text + "\'")
534 dcc_control.update_dcc_signal_theatre(sig_id,signals[str(sig_id)]["theatretext"],signal_change=False,sig_at_danger=False)
535 else:
536 logging.info ("Signal "+str(sig_id)+": Setting theatre route to \'" + theatre_text + "\'")
537 # We always call the function to update the DCC route indication on a change in route even if the signal
538 # is at Danger to cater for DCC signal types that automatically enable/disable the route indication
539 dcc_control.update_dcc_signal_theatre(sig_id,signals[str(sig_id)]["theatretext"],signal_change=False,sig_at_danger=True)
540 return()
542# -------------------------------------------------------------------------
543# Common Function that gets called on a signal aspect change - will
544# Enable/disable the theatre route indicator on a change to/from DANGER
545# (shared by Colour Light and semaphore signal types)
546# -------------------------------------------------------------------------
548def enable_disable_theatre_route_indication (sig_id):
549 global signals
550 # Only update the Theatre route indication if one exists for the signal
551 if signals[str(sig_id)]["hastheatre"]:
552 # Deal with the theatre route inhibit/enable cases (i.e. signal at DANGER or not at DANGER)
553 # We test for Not True and Not False to support the initial state when the signal is created (state = None)
554 if signals[str(sig_id)]["sigstate"] == signal_state_type.DANGER and signals[str(sig_id)]["theatreenabled"] != False:
555 logging.info ("Signal "+str(sig_id)+": Disabling theatre route display (signal is at DANGER)")
556 signals[str(sig_id)]["canvas"].itemconfig (signals[str(sig_id)]["theatreobject"],state="hidden")
557 signals[str(sig_id)]["theatreenabled"] = False
558 # This is where we send the special character to inhibit the theatre route indication
559 dcc_control.update_dcc_signal_theatre(sig_id,"#",signal_change=True,sig_at_danger=True)
561 elif signals[str(sig_id)]["sigstate"] != signal_state_type.DANGER and signals[str(sig_id)]["theatreenabled"] != True:
562 logging.info ("Signal "+str(sig_id)+": Enabling theatre route display of \'"+signals[str(sig_id)]["theatretext"]+"\'")
563 signals[str(sig_id)]["canvas"].itemconfig (signals[str(sig_id)]["theatreobject"],state="normal")
564 signals[str(sig_id)]["theatreenabled"] = True
565 dcc_control.update_dcc_signal_theatre(sig_id,signals[str(sig_id)]["theatretext"],signal_change=True,sig_at_danger=False)
566 return()
568# --------------------------------------------------------------------------------
569# Callbacks for handling MQTT messages received from a remote Signal
570# --------------------------------------------------------------------------------
572def handle_mqtt_signal_updated_event(message):
573 global signals
574 if "sourceidentifier" in message.keys() and "sigstate" in message.keys(): 574 ↛ 583line 574 didn't jump to line 583, because the condition on line 574 was never false
575 signal_identifier = message["sourceidentifier"]
576 # The sig state is an enumeration type - so its the VALUE that gets passed in the message
577 signals[signal_identifier]["sigstate"] = signal_state_type(message["sigstate"])
578 logging.info("Signal "+signal_identifier+": State update from remote signal *****************************")
579 logging.info ("Signal "+signal_identifier+": Aspect has changed to : "+
580 str(signals[signal_identifier]["sigstate"]).rpartition('.')[-1])
581 # Make the external callback (if one has been defined)
582 signals[signal_identifier]["extcallback"] (signal_identifier,sig_callback_type.sig_updated)
583 return()
585# --------------------------------------------------------------------------------
586# Common functions for building and sending MQTT messages - but only if the Signal
587# has been configured to publish the specified updates via the mqtt broker. As this
588# function is called on signal creation, we also need to handle the case of a signal
589# configured to NOT to refresh on creation (i.e. it gets set when 'update_signal' is
590# called for the first time - in this case (sigstate = None) we don't publish
591# --------------------------------------------------------------------------------
593def publish_signal_state(sig_id:int):
594 if sig_id in list_of_signals_to_publish_state_changes and signals[str(sig_id)]["sigstate"] is not None:
595 data = {}
596 # The sig state is an enumeration type - so its the VALUE that gets passed in the message
597 data["sigstate"] = signals[str(sig_id)]["sigstate"].value
598 log_message = "Signal "+str(sig_id)+": Publishing signal state to MQTT Broker"
599 # Publish as "retained" messages so remote items that subscribe later will always pick up the latest state
600 mqtt_interface.send_mqtt_message("signal_updated_event",sig_id,data=data,log_message=log_message,retain=True)
601 return()
603# ------------------------------------------------------------------------------------------
604# Common internal functions for deleting a signal object (including all the drawing objects)
605# This is used by the schematic editor for moving signals and changing signal types where we
606# delete the existing signal with all its data and then recreate it in its new configuration
607# Note that we don't delete the signal from the list_of_signals_to_publish (via MQTT) as
608# the MQTT configuration can be set completely asynchronously from create/delete signals
609# ------------------------------------------------------------------------------------------
611def delete_signal(sig_id:int):
612 global signals
613 if sig_exists(sig_id): 613 ↛ 625line 613 didn't jump to line 625, because the condition on line 613 was never false
614 # Delete all the tkinter canvas drawing objects created for the signal
615 signals[str(sig_id)]["canvas"].delete("signal"+str(sig_id))
616 # Delete all the tkinter button objects created for the signal
617 signals[str(sig_id)]["sigbutton"].destroy()
618 signals[str(sig_id)]["subbutton"].destroy()
619 signals[str(sig_id)]["passedbutton"].destroy()
620 # This buttons is only common to colour light and semaphore types
621 if signals[str(sig_id)]["sigtype"] in (sig_type.colour_light,sig_type.semaphore):
622 signals[str(sig_id)]["releasebutton"].destroy()
623 # Finally, delete the signal entry from the dictionary of signals
624 del signals[str(sig_id)]
625 return()
627# ------------------------------------------------------------------------------------------
628# Non public API function to reset the list of published/subscribed signals. Used
629# by the schematic editor for re-setting the MQTT configuration prior to re-configuring
630# via the signal-specific publish and subscribe configuration functions
631# ------------------------------------------------------------------------------------------
633def reset_mqtt_configuration():
634 global signals
635 global list_of_signals_to_publish_state_changes
636 # We only need to clear the list to stop any further signal events being published
637 list_of_signals_to_publish_state_changes.clear()
638 # For subscriptions we unsubscribe from all topics associated with the message_type
639 mqtt_interface.unsubscribe_from_message_type("signal_updated_event")
640 # Finally remove all "remote" signals from the dictionary of signals - these will
641 # be re-created if they are subsequently re-subscribed to. Note we don't iterate
642 # through the dictionary of signals to remove items as it will change under us
643 new_signals = {}
644 for key in signals:
645 if key.isdigit(): new_signals[key] = signals[key]
646 signals = new_signals
647 return()
649# ------------------------------------------------------------------------------------------
650# Non public API function to return the tkinter canvas 'tags' for the signal
651# ------------------------------------------------------------------------------------------
653def get_tags(sig_id:int):
654 return("signal"+str(sig_id))
656#################################################################################################