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
« 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#------------------------------------------------------------------------------------
36import copy
37import tkinter as Tk
39from . import common
40from . import objects
42from ..library import points
43from ..library import gpio_sensors
44from ..library import track_sensors
45from ..library import track_sections
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#------------------------------------------------------------------------------------
54open_windows={}
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#------------------------------------------------------------------------------------
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)
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)
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)
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#------------------------------------------------------------------------------------
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)
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)
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])
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)
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#------------------------------------------------------------------------------------
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")
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)
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])
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() ] )
235#####################################################################################
236# Top level Class for the Edit Track Sensor window
237#####################################################################################
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()
278#------------------------------------------------------------------------------------
279# Functions for Load, Save and close window
280#------------------------------------------------------------------------------------
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()
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()
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]
331#############################################################################################