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

155 statements  

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

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

2# This module contains all the UI functions for configuring Track Sensor objects 

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

4# 

5# External API functions intended for use by other editor modules: 

6# edit_sensor - Open the edit object top level window 

7# 

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

9# objects.update_object(obj_id,new_obj) - Update the configuration on save 

10######################################################################################################### 

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

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

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

14######################################################################################################### 

15# objects.section_exists(id) - To see if the Track Section exists ################################### 

16# 

17# Accesses the following external editor objects directly: 

18# objects.schematic_objects - To load/save the object configuration 

19# 

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

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

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

23# track_sensors.track_sensor_exists(id) - To see if the track sensor exists 

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

25# points.point_exists(id) - To see if the point exists 

26# 

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

28# common.str_int_item_id_entry_box 

29# common.int_item_id_entry_box 

30# common.point_interlocking_entry 

31# common.object_id_selection 

32# common.window_controls 

33# 

34#------------------------------------------------------------------------------------ 

35 

36import copy 

37import tkinter as Tk 

38 

39from . import common 

40from . import objects 

41 

42from ..library import points 

43from ..library import gpio_sensors 

44from ..library import track_sensors 

45from ..library import track_sections 

46 

47#------------------------------------------------------------------------------------ 

48# We maintain a global dictionary of open edit windows (where the key is the UUID 

49# of the object being edited) to prevent duplicate windows being opened. If the user 

50# tries to edit an object which is already being edited, then we just bring the 

51# existing edit window to the front (expanding if necessary) and set focus on it 

52#------------------------------------------------------------------------------------ 

53 

54open_windows={} 

55 

56#------------------------------------------------------------------------------------ 

57# Class for a gpio_sensor_selection frame - based on the str_int_item_id_entry_box 

58# Public Class instance methods (inherited from the str_int_item_id_entry_box) are 

59# "get_value" - will return the last "valid" value (string) 

60# "set_value" - set the initial value of the entry_box (str) - Also sets the 

61# current track sensor item ID (int) for validation purposes 

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

63# "validate" - Must 'exist' (or subscribed to) and not already mapped 

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

65#------------------------------------------------------------------------------------ 

66 

67class gpio_sensor_selection(common.str_int_item_id_entry_box): 

68 def __init__(self, parent_frame): 

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

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

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

72 self.track_sensor_id = 0 

73 # Create a labelframe to hold the various UI elements 

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

75 # Create a subframe to centre the UI elements 

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

77 self.subframe.pack() 

78 self.label = Tk.Label(self.subframe, text=" Track Sensor 'passed' sensor:") 

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

80 # The 'exists' function will return true if the GPIO sensor exists 

81 exists_function = gpio_sensors.gpio_sensor_exists 

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

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

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

85 super().__init__(self.subframe, tool_tip=tool_tip, exists_function=exists_function) 

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

87 

88 def validate(self, update_validation_status=True): 

89 # Do the basic validation first - ID is valid and 'exists' 

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

91 # Validate it isn't already mapped to another Signal or Track Sensor event. Note that we use the 

92 # current_item_id variable (from the base str_int_item_id_entry_box class) for validation. 

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

94 gpio_sensor_id = self.entry.get() 

95 event_mappings = gpio_sensors.get_gpio_sensor_callback(gpio_sensor_id) 

96 if event_mappings[0] > 0: 96 ↛ 97line 96 didn't jump to line 97, because the condition on line 96 was never true

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

98 valid = False 

99 elif event_mappings[1] > 0: 99 ↛ 100line 99 didn't jump to line 100, because the condition on line 99 was never true

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

101 valid = False 

102 elif event_mappings[2] > 0 and event_mappings[2] != self.track_sensor_id: 102 ↛ 103line 102 didn't jump to line 103, because the condition on line 102 was never true

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

104 valid = False 

105 if update_validation_status: self.set_validation_status(valid) 

106 return(valid) 

107 

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

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

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

111 def set_value(self, value:str, track_sensor_id:int): 

112 self.track_sensor_id = track_sensor_id 

113 super().set_value(value) 

114 

115#------------------------------------------------------------------------------------ 

116# Class for a track_sensor_route_group (comprising 6 points, and a track section) 

117# Uses the common point_interlocking_entry class for each point entry 

118# Public class instance methods provided are: 

119# "validate" - validate the current entry box values and return True/false 

120# "set_route" - will set the route elements (Points & Track Section) 

121# "get_route" - returns the last "valid" values (Points & Track Section) 

122#------------------------------------------------------------------------------------ 

123 

124class track_sensor_route_group(): 

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

126 # Create a label frame for this UI element (and pack into the parent frame) 

127 self.frame = Tk.Frame(parent_frame) 

128 self.frame.pack() 

129 # Create the route group label and the route entry elements (always packed) 

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

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

132 tool_tip = "Specify the points that need to be configured for the route" 

133 self.p1 = common.point_interlocking_entry(self.frame, points.point_exists, tool_tip) 

134 self.p2 = common.point_interlocking_entry(self.frame, points.point_exists, tool_tip) 

135 self.p3 = common.point_interlocking_entry(self.frame, points.point_exists, tool_tip) 

136 self.p4 = common.point_interlocking_entry(self.frame, points.point_exists, tool_tip) 

137 self.p5 = common.point_interlocking_entry(self.frame, points.point_exists, tool_tip) 

138 self.p6 = common.point_interlocking_entry(self.frame, points.point_exists, tool_tip) 

139 # Create the Track Section element (always packed) 

140 self.label = Tk.Label(self.frame, text=" Section:") 

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

142 ######################################################################################################### 

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

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

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

146 ######################################################################################################### 

147 self.section = common.int_item_id_entry_box(self.frame, exists_function=objects.section_exists, 

148 tool_tip = "Specify the next track section on the specified route (or leave blank)") 

149 self.section.pack(side=Tk.LEFT) 

150 

151 def validate(self): 

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

153 valid = True 

154 if not self.p1.validate(): valid = False 

155 if not self.p2.validate(): valid = False 

156 if not self.p3.validate(): valid = False 

157 if not self.p4.validate(): valid = False 

158 if not self.p5.validate(): valid = False 

159 if not self.p6.validate(): valid = False 

160 if not self.section.validate(): valid = False 

161 return(valid) 

162 

163 def set_route(self, interlocking_route:[[int,bool],int]): 

164 # A route comprises: [[p1, p2, p3, p4, p5, p6, p7], section_id] 

165 # Each point element in the point list comprises [point_id, point_state] 

166 self.p1.set_value(interlocking_route[0][0]) 

167 self.p2.set_value(interlocking_route[0][1]) 

168 self.p3.set_value(interlocking_route[0][2]) 

169 self.p4.set_value(interlocking_route[0][3]) 

170 self.p5.set_value(interlocking_route[0][4]) 

171 self.p6.set_value(interlocking_route[0][5]) 

172 self.section.set_value(interlocking_route[1]) 

173 

174 def get_route(self): 

175 # A route comprises: [[p1, p2, p3, p4, p5, p6, p7], section_id] 

176 # Each point element in the point list comprises [point_id, point_state] 

177 route = [ [ self.p1.get_value(), 

178 self.p2.get_value(), 

179 self.p3.get_value(), 

180 self.p4.get_value(), 

181 self.p5.get_value(), 

182 self.p6.get_value() ], 

183 self.section.get_value() ] 

184 return (route) 

185 

186#------------------------------------------------------------------------------------ 

187# Class for a track_sensor_route_frame (uses the base track_sensor_route_group class) 

188# Public class instance methods provided are: 

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

190# "set_routes" - will set all UI elements with the specified values 

191# "get_routes" - retrieves and returns the last "valid" values 

192#------------------------------------------------------------------------------------ 

193 

194class track_sensor_route_frame(): 

195 def __init__(self, parent_window, label:str): 

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

197 self.frame = Tk.LabelFrame(parent_window, text= label) 

198 # Create an element for each route - these are packed in the class instances 

199 self.main = track_sensor_route_group(self.frame, " Main") 

200 self.lh1 = track_sensor_route_group(self.frame, " LH1") 

201 self.lh2 = track_sensor_route_group(self.frame, " LH2") 

202 self.rh1 = track_sensor_route_group(self.frame, " RH1") 

203 self.rh2 = track_sensor_route_group(self.frame, " RH2") 

204 

205 def validate(self): 

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

207 valid = True 

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

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

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

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

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

213 return(valid) 

214 

215 def set_routes(self, track_section_routes:[[[[int,bool],],int]]): 

216 # A track_section_routes table comprises a list of routes: [main, lh1, lh2, rh1, rh2] 

217 # Each route comprises: [[p1, p2, p3, p4, p5, p6, p7], section_id] 

218 # Each point element in the point list comprises [point_id, point_state] 

219 self.main.set_route(track_section_routes[0]) 

220 self.lh1.set_route(track_section_routes[1]) 

221 self.lh2.set_route(track_section_routes[2]) 

222 self.rh1.set_route(track_section_routes[3]) 

223 self.rh2.set_route(track_section_routes[4]) 

224 

225 def get_routes(self): 

226 # An track_section_routes table comprises a list of routes: [main, lh1, lh2, rh1, rh2] 

227 # Each route comprises: [[p1, p2, p3, p4, p5, p6, p7], section_id] 

228 # Each point element in the point list comprises [point_id, point_state] 

229 return ( [ self.main.get_route(), 

230 self.lh1.get_route(), 

231 self.lh2.get_route(), 

232 self.rh1.get_route(), 

233 self.rh2.get_route() ] ) 

234 

235##################################################################################### 

236# Top level Class for the Edit Track Sensor window 

237##################################################################################### 

238 

239class edit_track_sensor(): 

240 def __init__(self, root, object_id): 

241 global open_windows 

242 # If there is already a window open for this object then re-focus and exit 

243 if object_id in open_windows.keys(): 243 ↛ 244line 243 didn't jump to line 244, because the condition on line 243 was never true

244 open_windows[object_id].lift() 

245 open_windows[object_id].state('normal') 

246 open_windows[object_id].focus_force() 

247 else: 

248 # This is the UUID for the object being edited 

249 self.object_id = object_id 

250 # Creatre the basic Top Level window and store the reference 

251 self.window = Tk.Toplevel(root) 

252 self.window.protocol("WM_DELETE_WINDOW", self.close_window) 

253 self.window.resizable(False, False) 

254 open_windows[object_id] = self.window 

255 # Create a Frame to hold the Item ID and GPIO Sensor UI elements 

256 self.frame = Tk.Frame(self.window) 

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

258 # Create the UI Element for Item ID selection 

259 self.sensorid = common.object_id_selection(self.frame, "Track Sensor ID", 

260 exists_function = track_sensors.track_sensor_exists) 

261 self.sensorid.frame.pack(side=Tk.LEFT, padx=2, pady=2, fill='y') 

262 # Create the UI Element for the GPIO Sensor selection. 

263 self.gpiosensor = gpio_sensor_selection(self.frame) 

264 self.gpiosensor.frame.pack(padx=2, pady=2, fill='x') 

265 # Create the UI Elements for the track sensor route elements 

266 self.ahead = track_sensor_route_frame(self.window,label="Routes / Track Sections 'behind' Track Sensor") 

267 self.ahead.frame.pack(padx=2, pady=2, fill='x') 

268 self.behind = track_sensor_route_frame(self.window,label="Routes/ Track Sections 'ahead of' Track Sensor") 

269 self.behind.frame.pack(padx=2, pady=2, fill='x') 

270 # Create the common Apply/OK/Reset/Cancel buttons for the window 

271 self.controls = common.window_controls(self.window, self.load_state, self.save_state, self.close_window) 

272 self.controls.frame.pack(side=Tk.BOTTOM, padx=2, pady=2) 

273 # Create the Validation error message (this gets packed/unpacked on apply/save) 

274 self.validation_error = Tk.Label(self.window, text="Errors on Form need correcting", fg="red") 

275 # load the initial UI state 

276 self.load_state() 

277 

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

279# Functions for Load, Save and close window 

280#------------------------------------------------------------------------------------ 

281 

282 def load_state(self): 

283 # Check the object we are editing still exists (hasn't been deleted from the schematic) 

284 # If it no longer exists then we just destroy the window and exit without saving 

285 if self.object_id not in objects.schematic_objects.keys(): 285 ↛ 286line 285 didn't jump to line 286, because the condition on line 285 was never true

286 self.close_window() 

287 else: 

288 item_id = objects.schematic_objects[self.object_id]["itemid"] 

289 # Label the edit window with the Item ID 

290 self.window.title("Track Sensor "+str(item_id)) 

291 # Set the Initial UI state (note the gpiosensor element needs the track sensor id for validation) 

292 self.sensorid.set_value(item_id) 

293 self.gpiosensor.set_value(objects.schematic_objects[self.object_id]["passedsensor"], item_id) 

294 self.ahead.set_routes(objects.schematic_objects[self.object_id]["routeahead"]) 

295 self.behind.set_routes(objects.schematic_objects[self.object_id]["routebehind"]) 

296 # Hide the validation error message 

297 self.validation_error.pack_forget() 

298 return() 

299 

300 def save_state(self, close_window:bool): 

301 # Check the object we are editing still exists (hasn't been deleted from the schematic) 

302 # If it no longer exists then we just destroy the window and exit without saving 

303 if self.object_id not in objects.schematic_objects.keys(): 303 ↛ 304line 303 didn't jump to line 304, because the condition on line 303 was never true

304 self.close_window() 

305 # Validate all user entries prior to applying the changes. Each of these would have 

306 # been validated on entry, but changes to other objects may have been made since then 

307 elif ( self.sensorid.validate() and self.gpiosensor.validate() and 307 ↛ 323line 307 didn't jump to line 323, because the condition on line 307 was never false

308 self.ahead.validate() and self.behind.validate() ): 

309 # Copy the original object Configuration (elements get overwritten as required) 

310 new_object_configuration = copy.deepcopy(objects.schematic_objects[self.object_id]) 

311 # Update the object coniguration elements from the current user selections 

312 new_object_configuration["itemid"] = self.sensorid.get_value() 

313 new_object_configuration["passedsensor"] = self.gpiosensor.get_value() 

314 new_object_configuration["routeahead"] = self.ahead.get_routes() 

315 new_object_configuration["routebehind"] = self.behind.get_routes() 

316 # Save the updated configuration (and re-draw the object) 

317 objects.update_object(self.object_id, new_object_configuration) 

318 # Close window on "OK" or re-load UI for "apply" 

319 if close_window: self.close_window() 

320 else: self.load_state() 

321 else: 

322 # Display the validation error message 

323 self.validation_error.pack(side=Tk.BOTTOM, before=self.controls.frame) 

324 return() 

325 

326 def close_window(self): 

327 self.window.destroy() 

328 # Delete the reference to this instance from the global list of open windows 

329 del open_windows[self.object_id] 

330 

331#############################################################################################