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

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

2# This module contains all of the parameters, funcions and classes that  

3# are used across more than one signal type 

4# ------------------------------------------------------------------------- 

5 

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 

13 

14from typing import Union 

15import tkinter as Tk 

16import logging 

17import enum 

18 

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

23 

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 

34 

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 

43 

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

48 

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 

59 

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 

67 

68# ------------------------------------------------------------------------- 

69# Signals are to be added to a global dictionary when created 

70# ------------------------------------------------------------------------- 

71 

72signals:dict = {} 

73 

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

75# Global lists for Signals configured to publish events to the MQTT Broker 

76# ------------------------------------------------------------------------- 

77 

78list_of_signals_to_publish_state_changes=[] 

79 

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

86 

87def sig_exists(sig_id:Union[int,str]): 

88 return (str(sig_id) in signals.keys() ) 

89 

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

91# Define a null callback function for internal use 

92# ------------------------------------------------------------------------- 

93 

94def null_callback (sig_id:int,callback_type): 

95 return (sig_id,callback_type) 

96 

97# ------------------------------------------------------------------------- 

98# Callbacks for processing button pushes 

99# ------------------------------------------------------------------------- 

100 

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 () 

109 

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 () 

121 

122def reset_sig_passed_button (sig_id:int): 

123 if sig_exists(sig_id): signals[str(sig_id)]["passedbutton"].config(bg=common.bgraised) 

124 

125def reset_sig_released_button (sig_id:int): 

126 if sig_exists(sig_id): signals[str(sig_id)]["releasebutton"].config(bg=common.bgraised) 

127 

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 () 

144 

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 () 

164 

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

166# Common function to refreh a signal following a change in state 

167# ------------------------------------------------------------------------- 

168 

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() 

181 

182# ------------------------------------------------------------------------- 

183# Common function to flip the internal state of a signal 

184# ------------------------------------------------------------------------- 

185 

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 () 

203 

204# ------------------------------------------------------------------------- 

205# Common function to flip the internal state of a subsidary signal 

206# ------------------------------------------------------------------------- 

207 

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 () 

221 

222# ------------------------------------------------------------------------- 

223# Common function to Set the approach control mode for a signal 

224# (shared by Colour Light and semaphore signal types) 

225# ------------------------------------------------------------------------- 

226 

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() 

246 

247#------------------------------------------------------------------------- 

248# Common function to Clear the approach control mode for a signal 

249# (shared by Colour Light and semaphore signal types) 

250# ------------------------------------------------------------------------- 

251 

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() 

261 

262# ------------------------------------------------------------------------- 

263# Common Function to set a signal override 

264# ------------------------------------------------------------------------- 

265 

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() 

275 

276# ------------------------------------------------------------------------- 

277# Common Function to clear a signal override 

278# ------------------------------------------------------------------------- 

279 

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() 

289 

290# ------------------------------------------------------------------------- 

291# Common Function to set a signal override 

292# ------------------------------------------------------------------------- 

293 

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() 

301 

302# ------------------------------------------------------------------------- 

303# Common Function to clear a signal override 

304# ------------------------------------------------------------------------- 

305 

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() 

313 

314# ------------------------------------------------------------------------- 

315# Common Function to lock a signal (i.e. for point/signal interlocking) 

316# ------------------------------------------------------------------------- 

317 

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() 

331 

332# ------------------------------------------------------------------------- 

333# Common Function to unlock a signal (i.e. for point/signal interlocking) 

334# ------------------------------------------------------------------------- 

335 

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() 

346 

347# ------------------------------------------------------------------------- 

348# Common Function to lock a subsidary (i.e. for point/signal interlocking) 

349# ------------------------------------------------------------------------- 

350 

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() 

364 

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

366# Common Function to unlock a subsidary (i.e. for point/signal interlocking) 

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

368 

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() 

378 

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

383 

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() 

461 

462# ------------------------------------------------------------------------- 

463# Common Function to generate all the signal elements for Approach Control 

464# (shared by Colour Light and semaphore signal types) 

465# ------------------------------------------------------------------------- 

466 

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() 

488 

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

493 

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() 

518 

519# ------------------------------------------------------------------------- 

520# Common function to change the theatre route indication 

521# (shared by Colour Light and semaphore signal types) 

522# ------------------------------------------------------------------------- 

523 

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() 

541 

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

547 

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) 

560 

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() 

567 

568# -------------------------------------------------------------------------------- 

569# Callbacks for handling MQTT messages received from a remote Signal 

570# -------------------------------------------------------------------------------- 

571 

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() 

584 

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

592 

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() 

602 

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

610 

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() 

626 

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

632 

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() 

648 

649# ------------------------------------------------------------------------------------------ 

650# Non public API function to return the tkinter canvas 'tags' for the signal 

651# ------------------------------------------------------------------------------------------ 

652 

653def get_tags(sig_id:int): 

654 return("signal"+str(sig_id)) 

655 

656#################################################################################################