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

353 statements  

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

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

2# This module is used for creating and managing colour light signal types 

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

4 

5from . import common 

6from . import signals_common 

7from . import dcc_control 

8from . import file_interface 

9 

10from typing import Union 

11import logging 

12import enum 

13 

14# ------------------------------------------------------------------------- 

15# Classes used externally when creating/updating colour light signals  

16# ------------------------------------------------------------------------- 

17 

18# Define the superset of signal sub types that can be created 

19class signal_sub_type(enum.Enum): 

20 home = 1 # 2 aspect - Red/Grn 

21 distant = 2 # 2 aspect - Ylw/Grn 

22 red_ylw = 3 # 2 aspect - Red/Ylw 

23 three_aspect = 4 

24 four_aspect = 5 

25 

26# --------------------------------------------------------------------------------- 

27# Public API Function to create a Colour Light Signal 'object'. The Signal is 

28# normally set to "NOT CLEAR" = RED (or YELLOW if its a 2 aspect distant signal) 

29# unless its fully automatic - when its set to "CLEAR" (with the appropriate aspect) 

30# --------------------------------------------------------------------------------- 

31 

32def create_colour_light_signal (canvas, sig_id: int, x:int, y:int, 

33 signal_subtype = signal_sub_type.four_aspect, 

34 sig_callback = None, 

35 orientation:int = 0, 

36 sig_passed_button:bool=False, 

37 approach_release_button:bool=False, 

38 position_light:bool=False, 

39 mainfeather:bool=False, 

40 lhfeather45:bool=False, 

41 lhfeather90:bool=False, 

42 rhfeather45:bool=False, 

43 rhfeather90:bool=False, 

44 theatre_route_indicator:bool=False, 

45 refresh_immediately = True, 

46 fully_automatic:bool=False): 

47 logging.info ("Signal "+str(sig_id)+": Creating Colour Light Signal") 

48 # Do some basic validation on the parameters we have been given 

49 signal_has_feathers = mainfeather or lhfeather45 or lhfeather90 or rhfeather45 or rhfeather90 

50 if signals_common.sig_exists(sig_id): 50 ↛ 51line 50 didn't jump to line 51, because the condition on line 50 was never true

51 logging.error ("Signal "+str(sig_id)+": Signal already exists") 

52 elif sig_id < 1: 52 ↛ 53line 52 didn't jump to line 53, because the condition on line 52 was never true

53 logging.error ("Signal "+str(sig_id)+": Signal ID must be greater than zero") 

54 elif orientation != 0 and orientation != 180: 54 ↛ 55line 54 didn't jump to line 55, because the condition on line 54 was never true

55 logging.error ("Signal "+str(sig_id)+": Invalid orientation angle - only 0 and 180 currently supported") 

56 elif signal_has_feathers and theatre_route_indicator: 56 ↛ 57line 56 didn't jump to line 57, because the condition on line 56 was never true

57 logging.error ("Signal "+str(sig_id)+": Signal can only have Feathers OR a Theatre Route Indicator") 

58 elif (signal_has_feathers or theatre_route_indicator) and signal_subtype == signal_sub_type.distant: 58 ↛ 59line 58 didn't jump to line 59, because the condition on line 58 was never true

59 logging.error ("Signal "+str(sig_id)+": 2 Aspect distant signals should not have Route Indicators") 

60 elif approach_release_button and signal_subtype == signal_sub_type.distant: 60 ↛ 61line 60 didn't jump to line 61, because the condition on line 60 was never true

61 logging.error ("Signal "+str(sig_id)+": 2 Aspect distant signals should not have Approach Release Control") 

62 else: 

63 # Define the "Tag" for all drawing objects for this signal instance 

64 sig_id_tag = "signal"+str(sig_id) 

65 # Draw the signal base line & signal post  

66 line_coords = common.rotate_line (x,y,0,0,0,-20,orientation) 

67 canvas.create_line (line_coords,width=2,tags=sig_id_tag) 

68 line_coords = common.rotate_line (x,y,0,-20,+30,-20,orientation) 

69 canvas.create_line (line_coords,width=3,tags=sig_id_tag) 

70 

71 # Draw the body of the position light - only if a position light has been specified 

72 if position_light: 

73 point_coords1 = common.rotate_point (x,y,+13,-12,orientation) 

74 point_coords2 = common.rotate_point (x,y,+13,-28,orientation) 

75 point_coords3 = common.rotate_point (x,y,+26,-28,orientation) 

76 point_coords4 = common.rotate_point (x,y,+26,-24,orientation) 

77 point_coords5 = common.rotate_point (x,y,+19,-12,orientation) 

78 points = point_coords1, point_coords2, point_coords3, point_coords4, point_coords5 

79 canvas.create_polygon (points, outline="black", fill="black",tags=sig_id_tag) 

80 

81 # Draw the position light aspects (but hide then if the signal doesn't have a subsidary) 

82 line_coords = common.rotate_line (x,y,+18,-27,+24,-21,orientation) 

83 poslight1 = canvas.create_oval (line_coords,fill="grey",outline="black",tags=sig_id_tag) 

84 line_coords = common.rotate_line (x,y,+14,-14,+20,-20,orientation) 

85 poslight2 = canvas.create_oval (line_coords,fill="grey",outline="black",tags=sig_id_tag) 

86 if not position_light: 

87 canvas.itemconfigure(poslight1,state='hidden') 

88 canvas.itemconfigure(poslight2,state='hidden') 

89 

90 # Draw all aspects for a 4-aspect signal (running from bottom to top) 

91 # Unused spects (if its a 2 or 3 aspect signal) get 'hidden' later 

92 line_coords = common.rotate_line (x,y,+40,-25,+30,-15,orientation) 

93 red = canvas.create_oval (line_coords,fill="grey",tags=sig_id_tag) 

94 line_coords = common.rotate_line (x,y,+50,-25,+40,-15,orientation) 

95 yel = canvas.create_oval (line_coords,fill="grey",tags=sig_id_tag) 

96 line_coords = common.rotate_line (x,y,+60,-25,+50,-15,orientation) 

97 grn = canvas.create_oval (line_coords,fill="grey",tags=sig_id_tag) 

98 line_coords = common.rotate_line (x,y,+70,-25,+60,-15,orientation) 

99 yel2 = canvas.create_oval (line_coords,fill="grey",tags=sig_id_tag) 

100 # Hide the aspects we don't need and define the 'offset' for the route indications based on 

101 # the signal type - so that the feathers and theatre route indicator sit on top of the signal 

102 # If its a 2 aspect signal we need to hide the green and the 2nd yellow aspect 

103 # We also need to 'reassign" the other aspects if its a Home or Distant signal 

104 if signal_subtype in (signal_sub_type.home, signal_sub_type.distant, signal_sub_type.red_ylw): 

105 offset = -20 

106 canvas.itemconfigure(yel2,state='hidden') 

107 canvas.itemconfigure(grn,state='hidden') 

108 if signal_subtype == signal_sub_type.home: 

109 grn = yel # Reassign the green aspect to aspect#2 (normally yellow in 3/4 aspect signals) 

110 elif signal_subtype == signal_sub_type.distant: 

111 grn = yel # Reassign the green aspect to aspect#2 (normally yellow in 3/4 aspect signals) 

112 yel = red # Reassign the Yellow aspect to aspect#1 (normally red in 3/4 aspect signals) 

113 # If its a 3 aspect signal we need to hide the 2nd yellow aspect 

114 elif signal_subtype == signal_sub_type.three_aspect: 

115 canvas.itemconfigure(yel2,state='hidden') 

116 offset = -10 

117 else: # its a 4 aspect signal 

118 offset = 0 

119 

120 # Now draw the feathers (x has been adjusted for the no of aspects)  

121 line_coords = common.rotate_line (x,y,offset+71,-20,offset+85,-20,orientation) 

122 main = canvas.create_line (line_coords,width=3,fill="black",tags=sig_id_tag) 

123 line_coords = common.rotate_line (x,y,offset+71,-20,offset+81,-10,orientation) 

124 rhf45 = canvas.create_line (line_coords,width=3,fill="black",tags=sig_id_tag) 

125 line_coords = common.rotate_line (x,y,offset+71,-20,offset+71,-5,orientation) 

126 rhf90 = canvas.create_line (line_coords,width=3,fill="black",tags=sig_id_tag) 

127 line_coords = common.rotate_line (x,y,offset+71,-20,offset+81,-30,orientation) 

128 lhf45 = canvas.create_line (line_coords,width=3,fill="black",tags=sig_id_tag) 

129 line_coords = common.rotate_line (x,y,offset+71,-20,offset+71,-35,orientation) 

130 lhf90 = canvas.create_line (line_coords,width=3,fill="black",tags=sig_id_tag) 

131 # Hide any feather drawing objects we don't need for this particular signal 

132 if not mainfeather: canvas.itemconfigure(main,state='hidden') 

133 if not lhfeather45: canvas.itemconfigure(lhf45,state='hidden') 

134 if not lhfeather90: canvas.itemconfigure(lhf90,state='hidden') 

135 if not rhfeather45: canvas.itemconfigure(rhf45,state='hidden') 

136 if not rhfeather90: canvas.itemconfigure(rhf90,state='hidden') 

137 

138 # Set the "Override" Aspect - this is the default aspect that will be displayed 

139 # by the signal when it is overridden - This will be RED apart from 2 aspect 

140 # Distant signals where it will be YELLOW 

141 if signal_subtype == signal_sub_type.distant: 

142 override_aspect = signals_common.signal_state_type.CAUTION 

143 else: 

144 override_aspect = signals_common.signal_state_type.DANGER 

145 

146 # Create all of the signal elements common to all signal types 

147 signals_common.create_common_signal_elements (canvas, sig_id, x, y, 

148 signal_type = signals_common.sig_type.colour_light, 

149 ext_callback = sig_callback, 

150 orientation = orientation, 

151 subsidary = position_light, 

152 sig_passed_button = sig_passed_button, 

153 automatic = fully_automatic, 

154 tag = sig_id_tag) 

155 

156 # Create the signal elements for a Theatre Route indicator 

157 signals_common.create_theatre_route_elements (canvas, sig_id, x, y, xoff=offset+80, yoff = -20, 

158 orientation = orientation,has_theatre = theatre_route_indicator) 

159 

160 # Create the signal elements to support Approach Control 

161 signals_common.create_approach_control_elements (canvas, sig_id, x, y, orientation = orientation, 

162 approach_button = approach_release_button) 

163 

164 # Add all of the signal-specific elements we need to manage colour light signal types 

165 # Note that setting a "sigstate" of RED is valid for all 2 aspect signals 

166 # as the associated drawing objects have been "swapped" by the code above 

167 # All SHARED attributes are signals_common to more than one signal Types 

168 signals_common.signals[str(sig_id)]["overriddenaspect"] = override_aspect # Type-specific - The 'Overridden' aspect 

169 signals_common.signals[str(sig_id)]["subtype"] = signal_subtype # Type-specific - subtype of the signal 

170 signals_common.signals[str(sig_id)]["refresh"] = refresh_immediately # Type-specific - controls when aspects are updated 

171 signals_common.signals[str(sig_id)]["hasfeathers"] = signal_has_feathers # Type-specific - If there is a Feather Route display 

172 signals_common.signals[str(sig_id)]["featherenabled"] = None # Type-specific - State of the Feather Route display 

173 signals_common.signals[str(sig_id)]["grn"] = grn # Type-specific - drawing object 

174 signals_common.signals[str(sig_id)]["yel"] = yel # Type-specific - drawing object 

175 signals_common.signals[str(sig_id)]["red"] = red # Type-specific - drawing object 

176 signals_common.signals[str(sig_id)]["yel2"] = yel2 # Type-specific - drawing object 

177 signals_common.signals[str(sig_id)]["pos1"] = poslight1 # Type-specific - drawing object 

178 signals_common.signals[str(sig_id)]["pos2"] = poslight2 # Type-specific - drawing object 

179 signals_common.signals[str(sig_id)]["mainf"] = main # Type-specific - drawing object 

180 signals_common.signals[str(sig_id)]["lhf45"] = lhf45 # Type-specific - drawing object 

181 signals_common.signals[str(sig_id)]["lhf90"] = lhf90 # Type-specific - drawing object 

182 signals_common.signals[str(sig_id)]["rhf45"] = rhf45 # Type-specific - drawing object 

183 signals_common.signals[str(sig_id)]["rhf90"] = rhf90 # Type-specific - drawing object 

184 

185 # Create the timed sequence class instances for the signal (one per route) 

186 signals_common.signals[str(sig_id)]["timedsequence"] = [] 

187 for route in signals_common.route_type: 

188 signals_common.signals[str(sig_id)]["timedsequence"].append(timed_sequence(sig_id,route)) 

189 

190 # Get the initial state for the signal (if layout state has been successfully loaded) 

191 # Note that each element of 'loaded_state' will be 'None' if no data was loaded 

192 loaded_state = file_interface.get_initial_item_state("signals",sig_id) 

193 # Note that for Enum types we load the value - need to turn this back into the Enum 

194 if loaded_state["routeset"] is not None: 

195 loaded_state["routeset"] = signals_common.route_type(loaded_state["routeset"]) 

196 # Set the initial state from the "loaded" state 

197 if loaded_state["releaseonred"]: signals_common.set_approach_control(sig_id,release_on_yellow=False) 

198 if loaded_state["releaseonyel"]: signals_common.set_approach_control(sig_id,release_on_yellow=True) 

199 if loaded_state["theatretext"]: signals_common.update_theatre_route_indication(sig_id,loaded_state["theatretext"]) 

200 if loaded_state["routeset"]: update_feather_route_indication(sig_id,loaded_state["routeset"]) 

201 if loaded_state["override"]: signals_common.set_signal_override(sig_id) 

202 # If no state was loaded we still need to toggle fully automatic signals to OFF 

203 if loaded_state["sigclear"] or fully_automatic: signals_common.toggle_signal(sig_id) 

204 # Update the signal to show the initial aspect (and send out DCC commands) 

205 # We only refresh the signal if it is set to refresh immediately 

206 if signals_common.signals[str(sig_id)]["refresh"]: update_colour_light_signal(sig_id) 

207 # finally Lock the signal if required 

208 if loaded_state["siglocked"]: signals_common.lock_signal(sig_id) 

209 

210 if position_light: 

211 # Set the initial state of the subsidary from the "loaded" state 

212 if loaded_state["subclear"]: signals_common.toggle_subsidary(sig_id) 

213 # Update the signal to show the initial aspect (and send out DCC commands) 

214 update_colour_light_subsidary(sig_id) 

215 # finally Lock the subsidary if required  

216 if loaded_state["sublocked"]: signals_common.lock_subsidary(sig_id) 

217 

218 # Publish the initial state to the broker (for other nodes to consume). Note that changes will 

219 # only be published if the MQTT interface has been configured for publishing updates for this  

220 # signal. This allows publish/subscribe to be configured prior to signal creation 

221 signals_common.publish_signal_state(sig_id) 

222 

223 return () 

224 

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

226# Internal Function to update the current state of the Subsidary signal 

227# (on/off). If a Subsidary was not specified at creation time then the 

228# objects are hidden' and the function will have no effect. 

229#------------------------------------------------------------------ 

230 

231def update_colour_light_subsidary (sig_id:int): 

232 if signals_common.signals[str(sig_id)]["subclear"]: 

233 logging.info ("Signal "+str(sig_id)+": Changing subsidary aspect to PROCEED") 

234 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["pos1"],fill="white") 

235 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["pos2"],fill="white") 

236 dcc_control.update_dcc_signal_element(sig_id,True,element="main_subsidary") 

237 else: 

238 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["pos1"],fill="grey") 

239 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["pos2"],fill="grey") 

240 logging.info ("Signal "+str(sig_id)+": Changing subsidary aspect to UNLIT") 

241 dcc_control.update_dcc_signal_element(sig_id,False,element="main_subsidary") 

242 return () 

243 

244# ------------------------------------------------------------------------- 

245# Function to Refresh the displayed signal aspect according the signal state 

246# Also takes into account the state of the signal ahead if one is specified 

247# to ensure the correct aspect is displayed (for 3/4 aspect types and 2 aspect  

248# distant signals). E.g. for a 3/4 aspect signal - if the signal ahead is ON 

249# and this signal is OFF then we want to change it to YELLOW rather than GREEN 

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

251 

252def update_colour_light_signal (sig_id:int, sig_ahead_id:Union[str,int]=None): 

253 

254 route = signals_common.signals[str(sig_id)]["routeset"] 

255 

256 # --------------------------------------------------------------------------------- 

257 # First deal with the Signal ON, Overridden or "Release on Red" cases 

258 # as they will apply to all colour light signal types (2, 3 or 4 aspect) 

259 # --------------------------------------------------------------------------------- 

260 

261 # If signal is set to "ON" then its DANGER (or CAUTION if its a 2 aspect distant) 

262 if not signals_common.signals[str(sig_id)]["sigclear"]: 

263 if signals_common.signals[str(sig_id)]["subtype"] == signal_sub_type.distant: 

264 new_aspect = signals_common.signal_state_type.CAUTION 

265 log_message = " (signal is ON and 2-aspect distant)" 

266 else: 

267 new_aspect = signals_common.signal_state_type.DANGER 

268 log_message = " (signal is ON)" 

269 

270 # If signal is Overriden the set the signal to its overriden aspect 

271 elif signals_common.signals[str(sig_id)]["override"]: 

272 new_aspect = signals_common.signals[str(sig_id)]["overriddenaspect"] 

273 log_message = " (signal is OVERRIDEN)" 

274 

275 # If signal is Overriden to CAUTION set the signal to display CAUTION 

276 # Note we are relying on the public API function to only allow this to 

277 # be set for signal types apart from 2 aspect home signals 

278 elif signals_common.signals[str(sig_id)]["overcaution"]: 

279 new_aspect = signals_common.signal_state_type.CAUTION 

280 log_message = " (signal is OVERRIDDEN to CAUTION)" 

281 

282 # If signal is triggered on a timed sequence then set to the sequence aspect 

283 elif signals_common.signals[str(sig_id)]["timedsequence"][route.value].sequence_in_progress: 

284 new_aspect = signals_common.signals[str(sig_id)]["timedsequence"][route.value].aspect 

285 log_message = " (signal is on a timed sequence)" 

286 

287 # Set to DANGER if the signal is subject to "Release on Red" approach control 

288 # Note that this state should never apply to 2 aspect distant signals 

289 elif signals_common.signals[str(sig_id)]["releaseonred"]: 289 ↛ 290line 289 didn't jump to line 290, because the condition on line 289 was never true

290 new_aspect = signals_common.signal_state_type.DANGER 

291 log_message = " (signal is OFF - but subject to \'release on red\' approach control)" 

292 

293 # --------------------------------------------------------------------------------- 

294 # From here, the Signal is Displaying "OFF" - but could still be of any type 

295 # --------------------------------------------------------------------------------- 

296 

297 # If the signal is a 2 aspect home signal or a 2 aspect red/yellow signal 

298 # we can ignore the signal ahead and set it to its "clear" aspect 

299 elif signals_common.signals[str(sig_id)]["subtype"] == signal_sub_type.home: 

300 new_aspect = signals_common.signal_state_type.PROCEED 

301 log_message = " (signal is OFF and 2-aspect home)" 

302 

303 elif signals_common.signals[str(sig_id)]["subtype"] == signal_sub_type.red_ylw: 

304 new_aspect = signals_common.signal_state_type.CAUTION 

305 log_message = " (signal is OFF and 2-aspect R/Y)" 

306 

307 # --------------------------------------------------------------------------------- 

308 # From here, the Signal is CLEAR and is a 2 aspect Distant or a 3/4 aspect signal 

309 # --------------------------------------------------------------------------------- 

310 

311 # Set to CAUTION if the signal is subject to "Release on YELLOW" approach control 

312 # We use the special CAUTION_APPROACH_CONTROL for "update on signal ahead" purposes 

313 elif signals_common.signals[str(sig_id)]["releaseonyel"]: 

314 new_aspect = signals_common.signal_state_type.CAUTION_APP_CNTL 

315 log_message = " (signal is OFF - but subject to \'release on yellow\' approach control)" 

316 

317 # --------------------------------------------------------------------------------- 

318 # From here Signal the Signal is CLEAR and is a 2 aspect Distant or 3/4 aspect signal 

319 # and will display the "normal" aspects based on the signal ahead (if one has been specified) 

320 # --------------------------------------------------------------------------------- 

321 

322 # If no signal ahead has been specified then we can set the signal to its "clear" aspect 

323 # (Applies to 2 aspect distant signals as well as the remaining 3 and 4 aspect signals types) 

324 elif sig_ahead_id is None: 

325 new_aspect = signals_common.signal_state_type.PROCEED 

326 log_message = " (signal is OFF and no signal ahead specified)" 

327 

328 # --------------------------------------------------------------------------------- 

329 # From here Signal the Signal is CLEAR and is a 2 aspect Distant or 3/4 aspect signal 

330 # and will display the "normal" aspects based on the signal ahead (one has been specified 

331 # --------------------------------------------------------------------------------- 

332 

333 else: 

334 

335 if signals_common.signals[str(sig_ahead_id)]["sigstate"] == signals_common.signal_state_type.DANGER: 

336 # All remaining signal types (3/4 aspects and 2 aspect distants) should display CAUTION 

337 new_aspect = signals_common.signal_state_type.CAUTION 

338 log_message = (" (signal is OFF and signal ahead "+str(sig_ahead_id)+" is displaying DANGER)") 

339 

340 elif signals_common.signals[str(sig_ahead_id)]["sigstate"] == signals_common.signal_state_type.CAUTION_APP_CNTL: 

341 # All remaining signal types (3/4 aspects and 2 aspect distants) should display FLASHING CAUTION 

342 new_aspect = signals_common.signal_state_type.FLASH_CAUTION 

343 log_message = (" (signal is OFF and signal ahead "+str(sig_ahead_id)+ 

344 " is subject to \'release on yellow\' approach control)") 

345 

346 elif signals_common.signals[str(sig_ahead_id)]["sigstate"] == signals_common.signal_state_type.CAUTION: 

347 if signals_common.signals[str(sig_id)]["subtype"] == signal_sub_type.four_aspect: 347 ↛ 353line 347 didn't jump to line 353, because the condition on line 347 was never false

348 # 4 aspect signals should display a PRELIM_CAUTION aspect 

349 new_aspect = signals_common.signal_state_type.PRELIM_CAUTION 

350 log_message = (" (signal is OFF and signal ahead "+str(sig_ahead_id)+" is displaying CAUTION)") 

351 else: 

352 # 3 aspect signals and 2 aspect distant signals should display PROCEED 

353 new_aspect = signals_common.signal_state_type.PROCEED 

354 log_message = (" (signal is OFF and signal ahead "+str(sig_ahead_id)+" is displaying CAUTION)") 

355 

356 elif signals_common.signals[str(sig_ahead_id)]["sigstate"] == signals_common.signal_state_type.FLASH_CAUTION: 

357 if signals_common.signals[str(sig_id)]["subtype"] == signal_sub_type.four_aspect: 357 ↛ 363line 357 didn't jump to line 363, because the condition on line 357 was never false

358 # 4 aspect signals will display a FLASHING PRELIM CAUTION aspect  

359 new_aspect = signals_common.signal_state_type.FLASH_PRELIM_CAUTION 

360 log_message = (" (signal is OFF and signal ahead "+str(sig_ahead_id)+" is displaying FLASHING_CAUTION)") 

361 else: 

362 # 3 aspect signals and 2 aspect distant signals should display PROCEED 

363 new_aspect = signals_common.signal_state_type.PROCEED 

364 log_message = (" (signal is OFF and signal ahead "+str(sig_ahead_id)+" is displaying PROCEED)") 

365 else: 

366 # A signal ahead state is either PRELIM_CAUTION, FLASH PRELIM CAUTION or PROCEED 

367 # These states have have no effect on the signal we are updating - Signal will show PROCEED 

368 new_aspect = signals_common.signal_state_type.PROCEED 

369 log_message = (" (signal is OFF and signal ahead "+str(sig_ahead_id)+" is displaying " 

370 + str(signals_common.signals[str(sig_ahead_id)]["sigstate"]).rpartition('.')[-1] + ")") 

371 

372 current_aspect = signals_common.signals[str(sig_id)]["sigstate"] 

373 

374 # Only refresh the signal if the aspect has been changed 

375 if new_aspect != current_aspect: 

376 logging.info ("Signal "+str(sig_id)+": Changing aspect to " + str(new_aspect).rpartition('.')[-1] + log_message) 

377 # Update the current aspect - note that this dictionary element is also used by the Flash Aspects Thread 

378 signals_common.signals[str(sig_id)]["sigstate"] = new_aspect 

379 refresh_signal_aspects (sig_id) 

380 # Update the Theatre & Feather route indications as these are inhibited/enabled for transitions to/from DANGER 

381 enable_disable_feather_route_indication(sig_id) 

382 signals_common.enable_disable_theatre_route_indication(sig_id) 

383 # Send the required DCC bus commands to change the signal to the desired aspect. Note that commands will only 

384 # be sent if the Pi-SPROG interface has been successfully configured and a DCC mapping exists for the signal 

385 dcc_control.update_dcc_signal_aspects(sig_id, new_aspect) 

386 # Publish the signal changes to the broker (for other nodes to consume). Note that state changes will only 

387 # be published if the MQTT interface has been successfully configured for publishing updates for this signal 

388 signals_common.publish_signal_state(sig_id) 

389 

390 return () 

391 

392# ------------------------------------------------------------------------- 

393# Internal Functions for cycling the flashing aspects. Rather than using a 

394# Thread to do this, we use the tkinter 'after' method to scedule the next 

395# update via the tkinter event queue (as Tkinter is not Threadsafe) 

396# ------------------------------------------------------------------------- 

397 

398def flash_aspect_off(sig_id): 

399 if not common.shutdown_initiated: 399 ↛ 405line 399 didn't jump to line 405, because the condition on line 399 was never false

400 if (signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.FLASH_CAUTION 

401 or signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.FLASH_PRELIM_CAUTION): 

402 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel"],fill="grey") 

403 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel2"],fill="grey") 

404 common.root_window.after(250,lambda:flash_aspect_on(sig_id)) 

405 return() 

406 

407def flash_aspect_on(sig_id): 

408 if not common.shutdown_initiated: 408 ↛ 417line 408 didn't jump to line 417, because the condition on line 408 was never false

409 if signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.FLASH_CAUTION: 

410 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel"],fill="yellow") 

411 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel2"],fill="grey") 

412 common.root_window.after(250,lambda:flash_aspect_off(sig_id)) 

413 if signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.FLASH_PRELIM_CAUTION: 

414 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel"],fill="yellow") 

415 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel2"],fill="yellow") 

416 common.root_window.after(250,lambda:flash_aspect_off(sig_id)) 

417 return() 

418 

419# ------------------------------------------------------------------------- 

420# Internal function to Refresh the displayed signal aspect by 

421# updating the signal drawing objects associated with each aspect 

422# ------------------------------------------------------------------------- 

423 

424def refresh_signal_aspects (sig_id:int): 

425 

426 if signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.DANGER: 

427 # Change the signal to display the RED aspect 

428 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["red"],fill="red") 

429 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel"],fill="grey") 

430 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["grn"],fill="grey") 

431 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel2"],fill="grey") 

432 

433 elif (signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.CAUTION 

434 or signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.CAUTION_APP_CNTL): 

435 # Change the signal to display the Yellow aspect 

436 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["red"],fill="grey") 

437 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel"],fill="yellow") 

438 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["grn"],fill="grey") 

439 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel2"],fill="grey") 

440 

441 elif signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.PRELIM_CAUTION: 

442 # Change the signal to display the Double Yellow aspect 

443 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["red"],fill="grey") 

444 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel"],fill="yellow") 

445 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["grn"],fill="grey") 

446 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel2"],fill="yellow") 

447 

448 elif signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.FLASH_CAUTION: 

449 # The flash_aspect_on function will start the flashing aspect so just turn off the other aspects  

450 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["red"],fill="grey") 

451 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["grn"],fill="grey") 

452 flash_aspect_on(sig_id) 

453 

454 elif signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.FLASH_PRELIM_CAUTION: 

455 # The flash_aspect_on function will start the flashing aspect so just turn off the other aspects  

456 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["red"],fill="grey") 

457 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["grn"],fill="grey") 

458 flash_aspect_on(sig_id) 

459 

460 elif signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.PROCEED: 460 ↛ 467line 460 didn't jump to line 467, because the condition on line 460 was never false

461 # Change the signal to display the Green aspect 

462 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["red"],fill="grey") 

463 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel"],fill="grey") 

464 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["grn"],fill="green") 

465 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["yel2"],fill="grey") 

466 

467 return () 

468 

469# ------------------------------------------------------------------------- 

470# Function to change the feather route indication (on route change) 

471# ------------------------------------------------------------------------- 

472 

473def update_feather_route_indication (sig_id:int,route_to_set): 

474 # Only Change the route indication if the signal has feathers 

475 if signals_common.signals[str(sig_id)]["hasfeathers"]: 

476 # Deal with route changes - but only if the Route has actually been changed 

477 if route_to_set != signals_common.signals[str(sig_id)]["routeset"]: 

478 signals_common.signals[str(sig_id)]["routeset"] = route_to_set 

479 if signals_common.signals[str(sig_id)]["featherenabled"] == True: 479 ↛ 480line 479 didn't jump to line 480, because the condition on line 479 was never true

480 logging.info ("Signal "+str(sig_id)+": Changing feather route display to "+ str(route_to_set).rpartition('.')[-1]) 

481 dcc_control.update_dcc_signal_route (sig_id, signals_common.signals[str(sig_id)]["routeset"], 

482 signal_change = False, sig_at_danger = False) 

483 else: 

484 logging.info ("Signal "+str(sig_id)+": Setting signal route to "+str(route_to_set).rpartition('.')[-1]) 

485 # We always call the function to update the DCC route indication on a change in route even if the signal 

486 # is at Danger to cater for DCC signal types that automatically enable/disable the route indication  

487 dcc_control.update_dcc_signal_route (sig_id, signals_common.signals[str(sig_id)]["routeset"], 

488 signal_change = False, sig_at_danger = True) 

489 # Refresh the signal aspect (a catch-all to ensure the signal displays the correct aspect 

490 # in case the signal is in the middle of a timed sequence for the old route or the new route 

491 if signals_common.signals[str(sig_id)]["refresh"]: update_colour_light_signal(sig_id) 

492 # Update the feathers on the display 

493 update_feathers(sig_id) 

494 return() 

495 

496# ------------------------------------------------------------------------- 

497# Internal Function that gets called on a signal aspect change - will 

498# Enable/disable the feather route indication on a change to/from DANGER 

499# ------------------------------------------------------------------------- 

500 

501def enable_disable_feather_route_indication (sig_id:int): 

502 # Only Enable/Disable the route indication if the signal has feathers 

503 if signals_common.signals[str(sig_id)]["hasfeathers"]: 

504 # We test for !True and !False to support the initial state when the signal is created (state = None) 

505 if (signals_common.signals[str(sig_id)]["sigstate"] == signals_common.signal_state_type.DANGER 

506 and signals_common.signals[str(sig_id)]["featherenabled"] != False): 

507 logging.info ("Signal "+str(sig_id)+": Disabling feather route display (signal is at RED)") 

508 signals_common.signals[str(sig_id)]["featherenabled"] = False 

509 dcc_control.update_dcc_signal_route(sig_id,signals_common.route_type.NONE, 

510 signal_change=True,sig_at_danger=True) 

511 

512 elif (signals_common.signals[str(sig_id)]["sigstate"] != signals_common.signal_state_type.DANGER 

513 and signals_common.signals[str(sig_id)]["featherenabled"] != True): 

514 logging.info ("Signal "+str(sig_id)+": Enabling feather route display for " 

515 + str(signals_common.signals[str(sig_id)]["routeset"]).rpartition('.')[-1]) 

516 signals_common.signals[str(sig_id)]["featherenabled"] = True 

517 dcc_control.update_dcc_signal_route(sig_id,signals_common.signals[str(sig_id)]["routeset"], 

518 signal_change=True,sig_at_danger=False) 

519 # Update the feathers on the display 

520 update_feathers(sig_id) 

521 return() 

522 

523# ------------------------------------------------------------------------- 

524# Internal Function to update the drawing objects for the feather indicators. 

525# ------------------------------------------------------------------------- 

526 

527def update_feathers(sig_id:int): 

528 # initially set all the indications to OFF - we'll then set what we need 

529 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["lhf45"],fill="black") 

530 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["lhf90"],fill="black") 

531 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["rhf45"],fill="black") 

532 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["rhf90"],fill="black") 

533 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["mainf"],fill="black") 

534 # Only display the route indication if the signal is not at RED 

535 if signals_common.signals[str(sig_id)]["sigstate"] != signals_common.signal_state_type.DANGER: 

536 if signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.LH1: 

537 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["lhf45"],fill="white") 

538 elif signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.LH2: 

539 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["lhf90"],fill="white") 

540 elif signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.RH1: 

541 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["rhf45"],fill="white") 

542 elif signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.RH2: 

543 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["rhf90"],fill="white") 

544 elif signals_common.signals[str(sig_id)]["routeset"] == signals_common.route_type.MAIN: 544 ↛ 546line 544 didn't jump to line 546, because the condition on line 544 was never false

545 signals_common.signals[str(sig_id)]["canvas"].itemconfig (signals_common.signals[str(sig_id)]["mainf"],fill="white") 

546 return() 

547 

548# ------------------------------------------------------------------------- 

549# Class for a timed signal sequence. A class instance is created for each 

550# route for each signal. When a timed signal is triggered the existing 

551# instance is first aborted. A new instance is then created/started 

552# ------------------------------------------------------------------------- 

553 

554class timed_sequence(): 

555 def __init__(self, sig_id:int, route, start_delay:int=0, time_delay:int=0): 

556 self.sig_id = sig_id 

557 self.sig_route = route 

558 self.aspect = signals_common.signals[str(sig_id)]["overriddenaspect"] 

559 self.start_delay = start_delay 

560 self.time_delay = time_delay 

561 self.sequence_abort_flag = False 

562 self.sequence_in_progress = False 

563 

564 def abort(self): 

565 self.sequence_abort_flag = True 

566 

567 def start(self): 

568 if self.sequence_abort_flag or not signals_common.sig_exists(self.sig_id): 568 ↛ 569line 568 didn't jump to line 569, because the condition on line 568 was never true

569 self.sequence_in_progress = False 

570 else: 

571 self.sequence_in_progress = True 

572 # For a start delay of zero we assume the intention is not to make a callback (on the basis 

573 # that the user has triggered the timed signal in the first place). For start delays > 0 the  

574 # sequence is initiated after the specified delay and this will trigger a callback 

575 # Note that we only change the aspect and generate the callback if the same route is set 

576 if signals_common.signals[str(self.sig_id)]["routeset"] == self.sig_route: 576 ↛ 588line 576 didn't jump to line 588, because the condition on line 576 was never false

577 if self.start_delay > 0: 577 ↛ 578line 577 didn't jump to line 578, because the condition on line 577 was never true

578 logging.info("Signal "+str(self.sig_id)+": Timed Signal - Signal Passed Event **************************") 

579 # Update the signal for automatic "signal passed" events as Signal is OVERRIDDEN 

580 update_colour_light_signal(self.sig_id) 

581 # Publish the signal passed event via the mqtt interface. Note that the event will only be published if the 

582 # mqtt interface has been successfully configured and the signal has been set to publish passed events 

583 signals_common.publish_signal_passed_event(self.sig_id) 

584 signals_common.signals[str(self.sig_id)]["extcallback"] (self.sig_id, signals_common.sig_callback_type.sig_passed) 

585 else: 

586 update_colour_light_signal(self.sig_id) 

587 # We only need to schedule the next YELLOW aspect for 3 and 4 aspect signals - otherwise schedule sequence completion 

588 if signals_common.signals[str(self.sig_id)]["subtype"] in (signal_sub_type.three_aspect, signal_sub_type.four_aspect): 588 ↛ 591line 588 didn't jump to line 591, because the condition on line 588 was never false

589 common.root_window.after(self.time_delay*1000,lambda:self.timed_signal_sequence_yellow()) 

590 else: 

591 common.root_window.after(self.time_delay*1000,lambda:self.timed_signal_sequence_end()) 

592 

593 def timed_signal_sequence_yellow(self): 

594 if self.sequence_abort_flag or not signals_common.sig_exists(self.sig_id): 594 ↛ 595line 594 didn't jump to line 595, because the condition on line 594 was never true

595 self.sequence_in_progress = False 

596 else: 

597 # This sequence step only applicable to 3 and 4 aspect signals 

598 self.aspect = signals_common.signal_state_type.CAUTION 

599 # Only change the aspect and generate the callback if the same route is set 

600 if signals_common.signals[str(self.sig_id)]["routeset"] == self.sig_route: 600 ↛ 605line 600 didn't jump to line 605, because the condition on line 600 was never false

601 logging.info("Signal "+str(self.sig_id)+": Timed Signal - Signal Updated Event *************************") 

602 update_colour_light_signal(self.sig_id) 

603 signals_common.signals[str(self.sig_id)]["extcallback"] (self.sig_id, signals_common.sig_callback_type.sig_updated) 

604 # We only need to schedule the next DOUBLE YELLOW aspect for 4 aspect signals - otherwise schedule sequence completion 

605 if signals_common.signals[str(self.sig_id)]["subtype"] == signal_sub_type.four_aspect: 605 ↛ 608line 605 didn't jump to line 608, because the condition on line 605 was never false

606 common.root_window.after(self.time_delay*1000,lambda:self.timed_signal_sequence_double_yellow()) 

607 else: 

608 common.root_window.after(self.time_delay*1000,lambda:self.timed_signal_sequence_end()) 

609 

610 def timed_signal_sequence_double_yellow(self): 

611 if self.sequence_abort_flag or not signals_common.sig_exists(self.sig_id): 611 ↛ 612line 611 didn't jump to line 612, because the condition on line 611 was never true

612 self.sequence_in_progress = False 

613 else: 

614 # This sequence step only applicable to 4 aspect signals 

615 self.aspect = signals_common.signal_state_type.PRELIM_CAUTION 

616 # Only change the aspect and generate the callback if the same route is set 

617 if signals_common.signals[str(self.sig_id)]["routeset"] == self.sig_route: 617 ↛ 622line 617 didn't jump to line 622, because the condition on line 617 was never false

618 logging.info("Signal "+str(self.sig_id)+": Timed Signal - Signal Updated Event *************************") 

619 update_colour_light_signal(self.sig_id) 

620 signals_common.signals[str(self.sig_id)]["extcallback"] (self.sig_id, signals_common.sig_callback_type.sig_updated) 

621 # Schedule the next aspect change (which will be the sequence completion) 

622 common.root_window.after(self.time_delay*1000,lambda:self.timed_signal_sequence_end()) 

623 

624 def timed_signal_sequence_end(self): 

625 # We've finished - Set the signal back to its "normal" condition 

626 self.sequence_in_progress = False 

627 if signals_common.sig_exists(self.sig_id): 627 ↛ exitline 627 didn't return from function 'timed_signal_sequence_end', because the condition on line 627 was never false

628 # Only change the aspect and generate the callback if the same route is set 

629 if signals_common.signals[str(self.sig_id)]["routeset"] == self.sig_route: 629 ↛ exitline 629 didn't return from function 'timed_signal_sequence_end', because the condition on line 629 was never false

630 logging.info("Signal "+str(self.sig_id)+": Timed Signal - Signal Updated Event *************************") 

631 update_colour_light_signal(self.sig_id) 

632 signals_common.signals[str(self.sig_id)]["extcallback"] (self.sig_id, signals_common.sig_callback_type.sig_updated) 

633 

634# ------------------------------------------------------------------------- 

635# Function to initiate a timed signal sequence - setting the signal to RED and then 

636# cycling through all of the supported aspects all the way back to GREEN (or YELLOW 

637# in the case of a RED/YELLOW 2-aspect signal). Intended for automation of 'exit'  

638# signals on a layout. The start_delay is the initial delay (in seconds) before the  

639# signal is changed to RED and the time_delay is the delay (in seconds) between each  

640# aspect change. A 'sig_passed' callback event will be generated when the signal is  

641# overriden if a start delay (> 0) is specified. For each subsequent aspect change 

642# a 'sig_updated' callback event will be generated. 

643# ------------------------------------------------------------------------- 

644 

645def trigger_timed_colour_light_signal (sig_id:int,start_delay:int=0,time_delay:int=5): 

646 

647 def delayed_sequence_start(sig_id:int, sig_route): 

648 if signals_common.sig_exists(sig_id): 

649 signals_common.signals[str(sig_id)]["timedsequence"][route.value].start() 

650 

651 # Don't initiate a timed signal sequence if a shutdown has already been initiated 

652 if common.shutdown_initiated: 652 ↛ 653line 652 didn't jump to line 653, because the condition on line 652 was never true

653 logging.warning("Signal "+str(sig_id)+": Timed Signal - Shutdown initiated - not triggering timed signal") 

654 else: 

655 # Abort any timed signal sequences already in progess 

656 route = signals_common.signals[str(sig_id)]["routeset"] 

657 signals_common.signals[str(sig_id)]["timedsequence"][route.value].abort() 

658 # Create a new instnce of the time signal class - this should have the effect of "destroying" 

659 # the old instance when it goes out of scope, leaving us with the newly created instance 

660 signals_common.signals[str(sig_id)]["timedsequence"][route.value] = timed_sequence(sig_id, route, start_delay, time_delay) 

661 # Schedule the start of the sequence (i.e. signal to danger) if the start delay is greater than zero 

662 # Otherwise initiate the sequence straight away (so the signal state is updated immediately) 

663 if start_delay > 0: 663 ↛ 664line 663 didn't jump to line 664, because the condition on line 663 was never true

664 common.root_window.after(start_delay*1000,lambda:delayed_sequence_start(sig_id,route)) 

665 else: 

666 signals_common.signals[str(sig_id)]["timedsequence"][route.value].start() 

667 

668###############################################################################