Coverage for /home/pi/Software/model-railway-signalling/model_railway_signals/editor/configure_signal_tab3.py: 95%

392 statements  

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

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

2# Functions and sub Classes for the Edit Signal "Automation" Tab 

3# 

4# Makes the following external API calls to other editor modules: 

5######################################################################################################### 

6# Note that we need to use the 'objects.section_exists' function as the the library 'section_exists' 

7# function will not work in edit mode as the Track Section library objects don't exist in edit mode 

8# To be addressed in a future software update when the Track Sections functionality is re-factored 

9######################################################################################################### 

10# objects.section_exists(id) - To see if the section exists (local) ################################## 

11# 

12# Makes the following external API calls to library modules: 

13# gpio_sensors.gpio_sensor_exists(id) - To see if the GPIO sensor exists (local or remote) 

14# gpio_sensors.get_gpio_sensor_callback - To see if a GPIO sensor is already mapped 

15# signals_common.sig_exists(id) - To see if the signal exists (local) 

16# track_sections.section_exists(id) - To see if the track section exists #################### 

17# 

18# Inherits the following common editor base classes (from common): 

19# common.str_int_item_id_entry_box 

20# common.int_item_id_entry_box 

21# common.check_box 

22# common.integer_entry_box 

23# common.CreateToolTip 

24# 

25#------------------------------------------------------------------------------------ 

26 

27import tkinter as Tk 

28 

29from . import common 

30from . import objects ############################################################################ 

31 

32from ..library import gpio_sensors 

33from ..library import signals_common 

34from ..library import track_sections 

35 

36#------------------------------------------------------------------------------------ 

37# Class for a Signal Sensor Entry Box - based on the str_int_item_id_entry_box class 

38# Public Class instance methods (inherited from the integer_entry_box) are 

39# "set_value" - will set the current value for the GPIO Sensor ID (integer) 

40# - Also sets the current item ID (int) for validation purposes 

41# "get_value" - will return the last "valid" value (integer) 

42# Overridden Public Class instance methods provided by this class: 

43# "validate" - Must be a valid Sensor ID and not already assigned 

44# Note that we use the current_item_id variable (from the base class) for validation. 

45#------------------------------------------------------------------------------------ 

46 

47class signal_sensor(common.str_int_item_id_entry_box): 

48 def __init__(self, parent_frame, callback, label:str, tool_tip:str): 

49 # We need to hold the current signal_id for validation purposes but we don't pass this  

50 # into the parent class as the entered ID for the gpio sensor can be the same as the current 

51 # item_id (for the signal object) - so we don't want the parent class to validate this. 

52 self.signal_id = 0 

53 # The this function will return true if the GPIO sensor exists 

54 exists_function = gpio_sensors.gpio_sensor_exists 

55 # Create the label and entry box UI elements 

56 self.label = Tk.Label(parent_frame, text=label) 

57 self.label.pack(side=Tk.LEFT, padx=2, pady=2) 

58 super().__init__(parent_frame, callback = callback, tool_tip=tool_tip, exists_function=exists_function) 

59 self.pack(side=Tk.LEFT, padx=2, pady=2) 

60 

61 def validate(self, update_validation_status=True): 

62 # Do the basic integer validation first (is it a valid ID and does it exist (or has been subscribed to) 

63 valid = super().validate(update_validation_status=False) 

64 # Next we need to validate it isn't already assigned to another signal appropach or passed event 

65 if valid and self.entry.get() != "": 

66 sensor_id = self.entry.get() 

67 event_mappings = gpio_sensors.get_gpio_sensor_callback(sensor_id) 

68 if event_mappings[0] > 0 and event_mappings[0] != self.signal_id: 68 ↛ 69line 68 didn't jump to line 69, because the condition on line 68 was never true

69 self.TT.text = ("GPIO Sensor "+sensor_id+" is already mapped to Signal "+str(event_mappings[0])) 

70 valid = False 

71 elif event_mappings[1] > 0 and event_mappings[1] != self.signal_id: 71 ↛ 72line 71 didn't jump to line 72, because the condition on line 71 was never true

72 self.TT.text = ("GPIO Sensor "+sensor_id+" is already mapped to Signal "+str(event_mappings[1])) 

73 valid = False 

74 elif event_mappings[2] > 0: 74 ↛ 75line 74 didn't jump to line 75, because the condition on line 74 was never true

75 self.TT.text = ("GPIO Sensor "+sensor_id+" is already mapped to Track Sensor "+str(event_mappings[2])) 

76 valid = False 

77 if update_validation_status: self.set_validation_status(valid) 

78 return(valid) 

79 

80 # We need to hold the current signal_id for validation purposes but we don't pass this  

81 # into the parent class as the entered ID for the gpio sensor can be the same as the current 

82 # item_id (for the signal object) - so we don't want the parent class to validate this. 

83 def set_value(self, value:str, signal_id:int): 

84 self.signal_id = signal_id 

85 super().set_value(value) 

86 

87#------------------------------------------------------------------------------------ 

88# Class for the Signal Passed Sensor Frame - uses the Signal Sensor Entry Box class 

89# Public Class instance methods used from the base classes are 

90# "approach.enable" - disables/blanks the checkbox and entry box  

91# "approach.disable" - enables/loads the checkbox and entry box 

92# "approach.set_value" - will set the current value (int) 

93# - Also sets the current item ID (int) for validation purposes 

94# "approach.get_value" - returns the last "valid" value (int) 

95# "passed.set_value" - will set the current value (int) 

96# - Also sets the current item ID (int) for validation purposes 

97# "passed.get_value" - returns the last "valid" value (int) 

98# Public Class instance methods provided by this class: 

99# "validate" - validate both entry box values and return True/false 

100#------------------------------------------------------------------------------------ 

101 

102class signal_passed_sensor_frame: 

103 def __init__(self, parent_frame, parent_object): 

104 # The child class instances need the reference to the parent object so they can call 

105 # the sibling class method to get the current value of the Signal ID for validation 

106 self.frame = Tk.LabelFrame(parent_frame, text="GPIO sensor events") 

107 # Create the elements in a subframe so they are centered 

108 self.subframe = Tk.Frame(self.frame) 

109 self.subframe.pack() 

110 tool_tip = ("Specify the ID of a GPIO Sensor (or leave blank) - This "+ 

111 "can be a local sensor ID or a remote sensor ID (in the form 'Node-ID') "+ 

112 "which has been subscribed to via MQTT networking") 

113 self.passed = signal_sensor(self.subframe, callback=self.validate, 

114 label=" Signal 'passed' sensor:", tool_tip = tool_tip) 

115 self.approach = signal_sensor(self.subframe, callback=self.validate, 

116 label=" Signal 'approached' sensor:", tool_tip = tool_tip) 

117 

118 def validate(self): 

119 if self.passed.entry.get() != "" and self.passed.entry.get() == self.approach.entry.get(): 119 ↛ 120line 119 didn't jump to line 120, because the condition on line 119 was never true

120 error_text = "Cannot assign the same GPIO sensor for both signal 'passed' and signal 'approached' events" 

121 self.passed.TT.text = error_text 

122 self.approach.TT.text = error_text 

123 self.passed.set_validation_status(False) 

124 self.approach.set_validation_status(False) 

125 return(False) 

126 else: 

127 # As both validation calls are made before the return statement 

128 # all UI eelements will be updated to show their validation status 

129 self.passed.set_validation_status(self.passed.validate()) 

130 self.approach.set_validation_status(self.approach.validate()) 

131 return(self.passed.validate() and self.approach.validate()) 

132 

133#------------------------------------------------------------------------------------ 

134# Sub Classes for the Track Occupancy automation subframe 

135# Public Class instance methods (inherited from the base class) are 

136# "disable" - disables/blanks the entry box  

137# "enable" enables/loads the entry box 

138# "set_value" - will set the current value (integer) 

139# "get_value" - will return the last "valid" value (integer) 

140# Public Class instance methods provided by the section_ahead_frame class: 

141# "validate" - validate all 'section ahead' entry box values and return True/false 

142# 

143# Note that the software only supports automation of track sections on the local schematic 

144# (local sections should be created to 'mirror' remote sections for networked layouts) 

145# so we use the section_exists function from the objects module for entry validation 

146#------------------------------------------------------------------------------------ 

147 

148class section_behind_element(common.int_item_id_entry_box): 

149 def __init__(self, parent_frame): 

150 self.frame = Tk.Frame(parent_frame) 

151 self.frame.pack() 

152 tool_tip = "Sepecify the track section 'in the rear of' this signal to be cleared when the signal is passed" 

153 super().__init__(self.frame, tool_tip=tool_tip, exists_function=objects.section_exists) 

154 self.pack(side=Tk.LEFT) 

155 self.label = Tk.Label(self.frame, text=" "+u"\u2192") 

156 self.label.pack(side=Tk.LEFT) 

157 

158class section_ahead_element(): 

159 def __init__(self, parent_frame, label): 

160 self.frame = Tk.Frame(parent_frame) 

161 self.frame.pack() 

162 self.label1 = Tk.Label(self.frame, text=label, width=8) 

163 self.label1.pack(side=Tk.LEFT) 

164 tool_tip1 = ("Specify the track section on the route 'ahead of' the signal "+ 

165 "to be occupied when the signal is passed") 

166 tool_tip2 = ("Specify any other track sections on the route that will also override "+ 

167 "this signal to ON if occupied (if enabled on the right)") 

168 self.t1 = common.int_item_id_entry_box(self.frame, exists_function=objects.section_exists, tool_tip=tool_tip1) 

169 self.t1.pack(side = Tk.LEFT) 

170 self.t2 = common.int_item_id_entry_box(self.frame, exists_function=objects.section_exists, tool_tip=tool_tip2) 

171 self.t2.pack(side = Tk.LEFT) 

172 self.t3 = common.int_item_id_entry_box(self.frame, exists_function=objects.section_exists, tool_tip=tool_tip2) 

173 self.t3.pack(side = Tk.LEFT) 

174 

175 def validate(self): 

176 # Validate everything - to highlight ALL validation failures in the UI 

177 valid = True 

178 if not self.t1.validate(): valid = False 

179 if not self.t2.validate(): valid = False 

180 if not self.t3.validate(): valid = False 

181 return(valid) 

182 

183 def enable(self): 

184 self.t1.enable() 

185 self.t2.enable() 

186 self.t3.enable() 

187 

188 def disable(self): 

189 self.t1.disable() 

190 self.t2.disable() 

191 self.t3.disable() 

192 

193 def set_values(self, list_of_sections:[int,int,int]): 

194 # The list_of_sections comprises: [t1,t2,t3] Where each element is 

195 # the ID of a track section on the route ahead 

196 self.t1.set_value(list_of_sections[0]) 

197 self.t2.set_value(list_of_sections[1]) 

198 self.t3.set_value(list_of_sections[2]) 

199 

200 def get_values(self): 

201 # The list_of_sections comprises: [t1,t2,t3] Where each element is 

202 # the ID of a track section on the route ahead 

203 interlocked_route = [ self.t1.get_value(), self.t2.get_value(), self.t3.get_value() ] 

204 return(interlocked_route) 

205 

206class section_ahead_frame(): 

207 def __init__(self, parent_frame): 

208 self.main = section_ahead_element(parent_frame, label="MAIN "+u"\u2192") 

209 self.lh1 = section_ahead_element(parent_frame, label="LH1 "+u"\u2192") 

210 self.lh2 = section_ahead_element(parent_frame, label="LH2 "+u"\u2192") 

211 self.rh1 = section_ahead_element(parent_frame, label="RH1 "+u"\u2192") 

212 self.rh2 = section_ahead_element(parent_frame, label="RH2 "+u"\u2192") 

213 

214 def validate(self): 

215 # Validate everything - to highlight ALL validation errors in the UI 

216 valid = True 

217 if not self.main.validate(): valid = False 

218 if not self.lh1.validate(): valid = False 

219 if not self.lh2.validate(): valid = False 

220 if not self.rh1.validate(): valid = False 

221 if not self.rh2.validate(): valid = False 

222 return (valid) 

223 

224#------------------------------------------------------------------------------------ 

225# Class for the Track Occupancy Frame - inherits from the sub-classes above 

226# Public Class instance methods provided by this class: 

227# "set_values" - will set the current values [behind,[MAIN,LH1,LH2,RH1,RH2]] 

228# "get_values" - will return the "valid" values [behind,[MAIN,LH1,LH2,RH1,RH2]] 

229# "validate" - validate all entry box values and return True/false 

230# Individual routes are enabled/disabled by calling the sub-class methods: 

231# "section_ahead.<route>.disable" - disables/blanks the entry box  

232# "section_ahead.<route>.enable" enables/loads the entry box 

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

234 

235class track_occupancy_frame(): 

236 def __init__(self, parent_frame): 

237 # Create the Label Frame for the UI element (packed by the creating function/class) 

238 self.frame = Tk.LabelFrame(parent_frame, text="Track occupancy changes") 

239 self.subframe1 = Tk.Frame(self.frame) 

240 self.subframe1.pack(side=Tk.LEFT) 

241 self.section_behind = section_behind_element(self.subframe1) 

242 self.subframe2 = Tk.Frame(self.frame) 

243 self.subframe2.pack(side=Tk.LEFT) 

244 self.section_ahead = section_ahead_frame(self.subframe2) 

245 

246 def set_values(self, sections): 

247 # sections is a list of [section_behind, sections_ahead] 

248 # where sections_ahead is a list of routes [MAIN,LH1,LH2,RH1,RH2] 

249 # And each route element is a list of track sections [t1,t2,t3] 

250 self.section_behind.set_value(sections[0]) 

251 self.section_ahead.main.set_values(sections[1][0]) 

252 self.section_ahead.lh1.set_values(sections[1][1]) 

253 self.section_ahead.lh2.set_values(sections[1][2]) 

254 self.section_ahead.rh1.set_values(sections[1][3]) 

255 self.section_ahead.rh2.set_values(sections[1][4]) 

256 

257 def get_values(self): 

258 # sections is a list of [section_behind, sections_ahead] 

259 # where sections_ahead is a list of routes [MAIN,LH1,LH2,RH1,RH2] 

260 # And each route element is a list of track sections [t1,t2,t3] 

261 return ( [ self.section_behind.get_value(), 

262 [ self.section_ahead.main.get_values(), 

263 self.section_ahead.lh1.get_values(), 

264 self.section_ahead.lh2.get_values(), 

265 self.section_ahead.rh1.get_values(), 

266 self.section_ahead.rh2.get_values() ] ]) 

267 

268 def validate(self): 

269 # Validate everything - to highlight ALL validation errors in the UI 

270 valid = True 

271 if not self.section_behind.validate(): valid = False 

272 if not self.section_ahead.validate(): valid = False 

273 return (valid) 

274 

275#------------------------------------------------------------------------------------ 

276# Class for the General automation settings subframe 

277# Public Class instance methods provided by this class: 

278# "override.enable" - enable the override checkbox 

279# "override.disable"- disable the override checkbox 

280# "automatic.enable" - enable the main auto checkbox 

281# "automatic.disable"- disable the main auto checkbox 

282# "distant_automatic.enable" - enable the distant auto checkbox 

283# "distant_automatic.disable"- disable the distant auto checkbox 

284# "override_ahead.enable" - enable the override ahead checkbox 

285# "override_ahead.disable"- disable the override ahead checkbox 

286# "set_values" - will set the current values (override, auto) 

287# "get_values" - will return the "valid" values (override, auto) 

288#------------------------------------------------------------------------------------ 

289 

290class general_settings_frame(): 

291 def __init__(self, parent_frame): 

292 # Create the Label Frame for the UI element (packed by the creating function/class) 

293 self.frame = Tk.LabelFrame(parent_frame, text="General settings") 

294 self.automatic = common.check_box(self.frame, width=39, 

295 label="Fully automatic signal (no control button)", 

296 tool_tip="Select to create without a main signal button "+ 

297 "(signal will have a default signal state of OFF, but can be "+ 

298 "overridden to ON via the selections below)") 

299 self.automatic.pack() 

300 self.distant_automatic = common.check_box(self.frame, width=39, 

301 label="Fully automatic distant arms (no control button)", 

302 tool_tip="Select to create without a distant signal button "+ 

303 "(distant arms will have a default signal state of OFF, but can "+ 

304 "be overridden to CAUTION via the selections below)") 

305 self.distant_automatic.pack() 

306 self.override = common.check_box(self.frame, width=39, 

307 label="Override signal to ON if section(s) ahead occupied", 

308 tool_tip="Select to override the signal to ON if the track "+ 

309 "sections ahead of the signal (specified on the left) are occupied") 

310 self.override.pack() 

311 self.override_ahead = common.check_box(self.frame, width=39, 

312 label="Override to CAUTION to reflect home signals ahead", 

313 tool_tip="Select to override distant signal to CAUTION if "+ 

314 "any home signals on the route ahead are at DANGER") 

315 self.override_ahead.pack() 

316 

317 def set_values(self, override:bool, main_auto:bool, override_ahead:bool, dist_auto:bool): 

318 self.override.set_value(override) 

319 self.automatic.set_value(main_auto) 

320 self.override_ahead.set_value(override_ahead) 

321 self.distant_automatic.set_value(dist_auto) 

322 

323 def get_values(self): 

324 return ( self.override.get_value(), 

325 self.automatic.get_value(), 

326 self.override_ahead.get_value(), 

327 self.distant_automatic.get_value() ) 

328 

329#------------------------------------------------------------------------------------ 

330# Class for a Timed signal route element comprising a route selection checkbox, a 

331# signal ID entry box and two integer entry boxes for specifying the timed sequence 

332# Public class instance methods provided by this class are  

333# "disable" - disables/blanks all checkboxes and selection boxes  

334# "enable" enables/loads all checkboxes and selection boxes 

335# "set_values" - set the initial values for the check box and entry boxes) 

336# Note this class also needs the current signal ID for validation 

337# "get_values" - get the last "validated" values of the check box and entry boxes 

338# 

339# ote that although the signals.sig_exists function will match both local and remote 

340# Signal IDs, the int_item_id_entry_box only allows integers to be selected - so we 

341# can safely use this function here for consistency. 

342#------------------------------------------------------------------------------------ 

343 

344class timed_signal_route_element(): 

345 def __init__(self, parent_frame, parent_object, label:str): 

346 # We need to know the current Signal ID for validation purposes 

347 self.current_item_id = 0 

348 # This is the parent object (the signal instance) 

349 self.parent_object = parent_object 

350 # Create a frame for the route element 

351 self.frame = Tk.Frame(parent_frame) 

352 self.frame.pack() 

353 # Create the route element (selection, sig ID, start delay, aspect change delay) 

354 self.label1 = Tk.Label(self.frame, width=5, text=label, anchor='w') 

355 self.label1.pack(side=Tk.LEFT) 

356 self.route = common.check_box(self.frame, label="", callback=self.route_updated, 

357 tool_tip="Select to trigger a timed sequence (for this route) when the current signal is passed") 

358 self.route.pack(side=Tk.LEFT) 

359 self.label2 = Tk.Label(self.frame, text=" Signal to trigger:") 

360 self.label2.pack(side=Tk.LEFT) 

361 self.sig = common.int_item_id_entry_box(self.frame, allow_empty=False, callback=self.signal_updated, 

362 exists_function=signals_common.sig_exists, tool_tip="Enter the ID of the signal to "+ 

363 "trigger. This can be the current signal or another semaphore / colour light "+ 

364 "signal (on the route ahead of the current signal)") 

365 self.sig.pack(side=Tk.LEFT) 

366 self.label3 = Tk.Label(self.frame, text=" Start delay:") 

367 self.label3.pack(side=Tk.LEFT) 

368 self.start = common.integer_entry_box(self.frame, width=3, min_value=0, max_value=60, 

369 allow_empty=False, tool_tip="Specify the time delay (in seconds) "+ 

370 "before triggering the timed sequence (if triggering the same " + 

371 "signal then this will be zero)") 

372 self.start.pack(side=Tk.LEFT) 

373 self.label4 = Tk.Label(self.frame, text=" Time delay:") 

374 self.label4.pack(side=Tk.LEFT) 

375 self.delay = common.integer_entry_box(self.frame, width=3, min_value=1, max_value=60, 

376 allow_empty=False, tool_tip="Specify the time period (in seconds) "+ 

377 "between signal aspect changes") 

378 self.delay.pack(side=Tk.LEFT) 

379 

380 def signal_updated(self): 

381 # Only enable the start delay if the current signal ID is not selected 

382 if self.sig.get() == str(self.current_item_id): 

383 self.start.disable2() 

384 self.start.TT.text = "Start delay will be zero when triggering the current signal" 

385 else: 

386 self.start.enable2() 

387 

388 def route_updated(self): 

389 if self.route.get_value(): 

390 self.sig.enable1() 

391 self.start.enable1() 

392 self.delay.enable1() 

393 else: 

394 self.sig.disable1() 

395 self.start.disable1() 

396 self.delay.disable1() 

397 self.signal_updated() 

398 

399 def enable(self): 

400 self.route.enable() 

401 self.sig.enable() 

402 self.start.enable() 

403 self.delay.enable() 

404 

405 def disable(self): 

406 self.route.disable() 

407 self.sig.disable() 

408 self.start.disable() 

409 self.delay.disable() 

410 

411 def set_values(self, route:[bool,int,int,int], item_id:int): 

412 # A route comprises a list of [selected, sig_id, start_delay, time_delay) 

413 # If signal to trigger is '0' (no selection) then we set the current signal ID 

414 # to give us a valid default configuration (for the user to edit as required) 

415 # Similarly, we set a default of 5 seconds for the time delay 

416 self.current_item_id = item_id 

417 self.route.set_value(route[0]) 

418 if route[1] == 0: self.sig.set_value(item_id) 

419 else:self.sig.set_value(route[1]) 

420 self.start.set_value(route[2]) 

421 if route[3] == 0: self.delay.set_value(5) 

422 else: self.delay.set_value(route[3]) 

423 # Enable/disable the various route elements as required 

424 self.route_updated() 

425 

426 def get_values(self): 

427 # A route comprises a list of [selected, sig_id,start_delay, time_delay) 

428 return ( [ self.route.get_value(), 

429 self.sig.get_value(), 

430 self.start.get_value(), 

431 self.delay.get_value() ] ) 

432 

433 def validate(self): 

434 # Validate everything - to highlight ALL validation errors in the UI 

435 valid = True 

436 if not self.sig.validate(): valid = False 

437 if not self.start.validate(): valid = False 

438 if not self.delay.validate(): valid = False 

439 return (valid) 

440 

441#------------------------------------------------------------------------------------ 

442# Class for a Timed signal route frame (comprising selections for each route) 

443# Public class instance methods provided by this class are:  

444# "set_values" - set the initial values for the check box and entry boxes 

445# "get_values" - get the last "validated" values of the check box and entry boxes 

446# Note this class also needs the current signal ID for validation 

447# Note that no overall enable/disable functions are provided - External functions  

448# should call the individual enable/disable functions for each route element 

449#------------------------------------------------------------------------------------ 

450 

451class timed_signal_frame(): 

452 def __init__(self, parent_frame, parent_object): 

453 # Create a label frame for the UI element 

454 self.frame = Tk.LabelFrame(parent_frame, text="Trigger timed signal sequence") 

455 # Create a subframe for the context label 

456 self.subframe1 = Tk.Frame(self.frame) 

457 self.subframe1.pack(side=Tk.LEFT, padx=2, pady=2, fill='both') 

458 self.label = Tk.Label(self.frame, text="Routes to\ntrigger", anchor='w') 

459 self.label.pack(side=Tk.LEFT) 

460 # Create a subframe for the route elements 

461 self.subframe2 = Tk.Frame(self.frame) 

462 self.subframe2.pack(side=Tk.LEFT, padx=2, pady=2, fill='x', expand=True) 

463 self.main=timed_signal_route_element(self.subframe2, parent_object, label="MAIN") 

464 self.lh1=timed_signal_route_element(self.subframe2, parent_object, label="LH1") 

465 self.lh2=timed_signal_route_element(self.subframe2, parent_object, label="LH2") 

466 self.rh1=timed_signal_route_element(self.subframe2, parent_object, label="RH1") 

467 self.rh2=timed_signal_route_element(self.subframe2, parent_object, label="RH2") 

468 

469 def set_values(self, timed_sequence:[[bool,int,int,int],], item_id:int): 

470 # A timed_sequence comprises a list of routes [MAIN, LH1, LH2, RH1, RH2] 

471 # Each route comprises a list of [selected, sig_id,start_delay, time_delay) 

472 self.main.set_values(timed_sequence[0], item_id) 

473 self.lh1.set_values(timed_sequence[1], item_id) 

474 self.lh2.set_values(timed_sequence[2], item_id) 

475 self.rh1.set_values(timed_sequence[3], item_id) 

476 self.rh2.set_values(timed_sequence[4], item_id) 

477 

478 def get_values(self): 

479 # A timed_sequence comprises a list of routes [MAIN, LH1, LH2, RH1, RH2] 

480 # Each route comprises a list of [selected, sig_id,start_delay, time_delay) 

481 return ( [ self.main.get_values(), 

482 self.lh1.get_values(), 

483 self.lh2.get_values(), 

484 self.rh1.get_values(), 

485 self.rh2.get_values() ] ) 

486 

487 def validate(self): 

488 # Validate everything - to highlight ALL validation errors in the UI 

489 valid = True 

490 if not self.main.validate(): valid = False 

491 if not self.lh1.validate(): valid = False 

492 if not self.lh2.validate(): valid = False 

493 if not self.rh1.validate(): valid = False 

494 if not self.rh2.validate(): valid = False 

495 return (valid) 

496 

497#------------------------------------------------------------------------------------ 

498# Class for a approach control route element comprising a route selection checkbox, 

499# And radio buttons to select the approach control mode 

500# "disable_route" - disables/blanks all checkboxes and radio buttons  

501# "enable_route" enables/loads all checkboxes and radio buttons 

502# "disable_red" - disables/blanks the "Release on Red" radio button  

503# "enable_red" enables/loads the "Release on Red" radio button 

504# "disable_yel" - disables/blanks the "Release on yellow" radio button  

505# "enable_yel" enables/loads the "Release on yellow" radio button 

506# "set_values" - set the initial values for the check box and radio buttons 

507# "get_values" - get the current values of the check box and radio buttons 

508#------------------------------------------------------------------------------------ 

509 

510class approach_control_route_element(): 

511 def __init__(self, parent_frame, label:str): 

512 # Create a frame for the route element 

513 self.frame = Tk.Frame(parent_frame) 

514 self.frame.pack() 

515 # Create the route element (selection, sig ID, start delay, aspect change delay) 

516 self.label1 = Tk.Label(self.frame, width=5, text=label, anchor='w') 

517 self.label1.pack(side=Tk.LEFT) 

518 self.route = common.check_box(self.frame, label="", callback=self.route_selected, 

519 tool_tip="Select to enable 'Approach Control' for this route") 

520 self.route.pack(side=Tk.LEFT) 

521 # Add a bit of white space 

522 self.label2 = Tk.Label(self.frame, text=" Release on:") 

523 self.label2.pack(side=Tk.LEFT) 

524 # Create the approach control mode selection radiobuttons 

525 self.selection = Tk.IntVar(self.frame, 0) 

526 self.approach_mode = 0 

527 self.red_enabled = True 

528 self.yel_enabled = True 

529 self.sig_enabled = True 

530 self.B1 = Tk.Radiobutton(self.frame, text="Red", anchor='w', 

531 command=self.mode_selected, variable=self.selection, value=1) 

532 self.B1.pack(side=Tk.LEFT) 

533 self.B1TT = common.CreateToolTip(self.B1, "Signal will remain at DANGER until the train approaches") 

534 self.B2 = Tk.Radiobutton(self.frame, text="Yellow", anchor='w', 

535 command=self.mode_selected, variable=self.selection, value=2) 

536 self.B2.pack(side=Tk.LEFT) 

537 self.B2TT = common.CreateToolTip(self.B2, "Signal will remain at CAUTION until the train approaches") 

538 self.B3 = Tk.Radiobutton(self.frame, text="Red (on signals ahead)", anchor='w', 

539 command=self.mode_selected, variable=self.selection, value=3) 

540 self.B3.pack(side=Tk.LEFT) 

541 self.B3TT = common.CreateToolTip(self.B3, "Signal will remain at DANGER until the train approaches "+ 

542 "(approach control will only be applied if there is a home signal ahead at danger)") 

543 

544 def mode_selected(self): 

545 self.approach_mode = self.selection.get() 

546 

547 def route_selected(self): 

548 if self.route.get_value(): 

549 if self.red_enabled: self.B1.configure(state="normal") 549 ↛ 550line 549 didn't jump to line 550, because the condition on line 549 was never false

550 else: self.B1.configure(state="disabled") 

551 if self.yel_enabled: self.B2.configure(state="normal") 

552 else: self.B2.configure(state="disabled") 

553 if self.sig_enabled: self.B3.configure(state="normal") 

554 else: self.B3.configure(state="disabled") 

555 # Ensure the selection is valid 

556 if self.approach_mode == 0: self.approach_mode = 1 

557 if not self.red_enabled and self.approach_mode == 1: self.approach_mode = 2 

558 if not self.yel_enabled and self.approach_mode == 2: self.approach_mode = 1 

559 if not self.sig_enabled and self.approach_mode == 3: self.approach_mode = 1 

560 self.selection.set(self.approach_mode) 

561 else: 

562 self.B1.configure(state="disabled") 

563 self.B2.configure(state="disabled") 

564 self.B3.configure(state="disabled") 

565 self.selection.set(0) 

566 

567 def enable_route(self): 

568 self.route.enable() 

569 self.route_selected() 

570 

571 def disable_route(self): 

572 self.route.disable() 

573 self.route_selected() 

574 

575 def enable_red(self): 

576 self.red_enabled = True 

577 self.route_selected() 

578 

579 def disable_red(self): 

580 self.red_enabled = False 

581 self.route_selected() 

582 

583 def enable_yel(self): 

584 self.yel_enabled = True 

585 self.route_selected() 

586 

587 def disable_yel(self): 

588 self.yel_enabled = False 

589 self.route_selected() 

590 

591 def enable_sig_ahead(self): 

592 self.sig_enabled = True 

593 self.route_selected() 

594 

595 def disable_sig_ahead(self): 

596 self.sig_enabled = False 

597 self.route_selected() 

598 

599 def set_values(self, mode:int): 

600 # The 'Mode' value represents the approach control mode that has been set 

601 # release_on_red=1, release_on_yel=2, released_on_red_home_ahead=3 

602 self.route.set_value(mode != 0) 

603 self.approach_mode = mode 

604 self.route_selected() 

605 

606 def get_values(self): 

607 # The 'Mode' value represents the approach control mode that has been set 

608 # release_on_red=1, release_on_yel=2, released_on_red_home_ahead=3 

609 return (self.selection.get()) 

610 

611 def approach_control_selected(self): 

612 return self.route.get_value() 

613 

614#------------------------------------------------------------------------------------ 

615# Class for a Approach Control route frame (comprising selections for each route) 

616# Public class instance methods provided by this class are:  

617# "enable_release_on_red" - disables/blanks the "Release on Red" radio button  

618# "disable_release_on_red" enables/loads the "Release on Red" radio button 

619# "enable_release_on_yel" - disables/blanks the "Release on yellow" radio button  

620# "disable_release_on_yel" enables/loads the "Release on yellow" radio button 

621# "enable_release_on_red_sig_ahead" - disables/blanks the "Release on sig ahead" radio button  

622# "disable_release_on_red_sig_ahead" enables/loads the "Release on sig ahead" radio button 

623# "set_values" - sets the initial values for the check boxes & radio buttons)  

624# "get_values" - get current last values for the check boxes & radio buttons 

625# "is_selected" - returns whether the signal has been configured for approach control 

626# Note that no route enable/disable functions are provided - External functions  

627# should call the individal route_enable/disable functions for each element 

628#------------------------------------------------------------------------------------ 

629 

630class approach_control_frame(): 

631 def __init__(self, parent_frame): 

632 # Create a label frame for the UI element 

633 self.frame = Tk.LabelFrame(parent_frame, text="Approach control selections") 

634 # Create a subframe for the context label 

635 self.subframe1 = Tk.Frame(self.frame) 

636 self.subframe1.pack(side=Tk.LEFT, padx=2, pady=2, fill='both') 

637 self.label = Tk.Label(self.frame, text="Routes\nsubject to\napproach\ncontrol", anchor='w') 

638 self.label.pack(side=Tk.LEFT) 

639 # Create a subframe for the route elements 

640 self.subframe2 = Tk.Frame(self.frame) 

641 self.subframe2.pack(side=Tk.LEFT, padx=2, pady=2, fill='x', expand=True) 

642 self.main=approach_control_route_element(self.subframe2, label="MAIN") 

643 self.lh1=approach_control_route_element(self.subframe2, label="LH1") 

644 self.lh2=approach_control_route_element(self.subframe2, label="LH2") 

645 self.rh1=approach_control_route_element(self.subframe2, label="RH1") 

646 self.rh2=approach_control_route_element(self.subframe2, label="RH2") 

647 

648 def enable_release_on_red(self): 

649 self.main.enable_red() 

650 self.lh1.enable_red() 

651 self.lh2.enable_red() 

652 self.rh1.enable_red() 

653 self.rh2.enable_red() 

654 

655 def disable_release_on_red(self): 

656 self.main.disable_red() 

657 self.lh1.disable_red() 

658 self.lh2.disable_red() 

659 self.rh1.disable_red() 

660 self.rh2.disable_red() 

661 

662 def enable_release_on_yel(self): 

663 self.main.enable_yel() 

664 self.lh1.enable_yel() 

665 self.lh2.enable_yel() 

666 self.rh1.enable_yel() 

667 self.rh2.enable_yel() 

668 

669 def disable_release_on_yel(self): 

670 self.main.disable_yel() 

671 self.lh1.disable_yel() 

672 self.lh2.disable_yel() 

673 self.rh1.disable_yel() 

674 self.rh2.disable_yel() 

675 

676 def enable_release_on_red_sig_ahead(self): 

677 self.main.enable_sig_ahead() 

678 self.lh1.enable_sig_ahead() 

679 self.lh2.enable_sig_ahead() 

680 self.rh1.enable_sig_ahead() 

681 self.rh2.enable_sig_ahead() 

682 

683 def disable_release_on_red_sig_ahead(self): 

684 self.main.disable_sig_ahead() 

685 self.lh1.disable_sig_ahead() 

686 self.lh2.disable_sig_ahead() 

687 self.rh1.disable_sig_ahead() 

688 self.rh2.disable_sig_ahead() 

689 

690 def set_values(self, approach_control:[int,]): 

691 # Approach_Control comprises a list of routes [MAIN, LH1, LH2, RH1, RH2] 

692 # Each element represents the approach control mode that has been set 

693 # release_on_red=1, release_on_yel=2, released_on_red_home_ahead=3 

694 self.main.set_values(approach_control[0]) 

695 self.lh1.set_values(approach_control[1]) 

696 self.lh2.set_values(approach_control[2]) 

697 self.rh1.set_values(approach_control[3]) 

698 self.rh2.set_values(approach_control[4]) 

699 

700 def get_values(self): 

701 # Approach_Control comprises a list of routes [MAIN, LH1, LH2, RH1, RH2] 

702 # Each element represents the approach control mode that has been set 

703 # release_on_red=1, release_on_yel=2, released_on_red_home_ahead=3 

704 return ( [ self.main.get_values(), 

705 self.lh1.get_values(), 

706 self.lh2.get_values(), 

707 self.rh1.get_values(), 

708 self.rh2.get_values() ] ) 

709 

710 def is_selected(self): 

711 return ( self.main.approach_control_selected() or 

712 self.lh1.approach_control_selected() or 

713 self.lh2.approach_control_selected() or 

714 self.rh1.approach_control_selected() or 

715 self.rh2.approach_control_selected() ) 

716 

717#------------------------------------------------------------------------------------ 

718# Top level Class for the Edit Signal Window Automation Tab 

719#------------------------------------------------------------------------------------ 

720 

721class signal_automation_tab(): 

722 def __init__(self, parent_tab, parent_object): 

723 # Create the signal sensor frame (always packed) 

724 self.gpio_sensors = signal_passed_sensor_frame(parent_tab, parent_object) 

725 self.gpio_sensors.frame.pack(padx=2, pady=2, fill='x') 

726 # Create a Frame for the track occupancy and general settings (always packed) 

727 self.frame1 = Tk.Frame(parent_tab) 

728 self.frame1.pack(padx=2, pady=2, fill='x') 

729 self.track_occupancy = track_occupancy_frame(self.frame1) 

730 self.track_occupancy.frame.pack(side=Tk.LEFT, padx=2, pady=2) 

731 self.general_settings = general_settings_frame(self.frame1) 

732 self.general_settings.frame.pack(side=Tk.LEFT, padx=2, pady=2, fill='both', expand=True) 

733 # Create a Frame for the timed signal configuration (packed according to signal type) 

734 self.timed_signal = timed_signal_frame(parent_tab, parent_object) 

735 # Create a Frame for the Signal Approach control (packed according to signal type) 

736 self.approach_control = approach_control_frame(parent_tab) 

737 

738######################################################################################