Coverage for /home/pi/Software/model-railway-signalling/model_railway_signals/editor/utilities.py: 9%
300 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-03-22 13:55 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2024-03-22 13:55 +0000
1#------------------------------------------------------------------------------------
2# This module contains all the functions for the menubar utilities
3#
4# Classes (pop up windows) called from the main editor module menubar selections
5# dcc_programming(root, dcc_programming_enabled_function, dcc_power_on_function, dcc_power_off_function)
6# dcc_mappings(root)
7#
8# Uses the following library functions:
9# pi_sprog_interface.service_mode_read_cv(cv_to_read)
10# pi_sprog_interface.service_mode_write_cv(cv_to_write,value_to_write)
11# pi_sprog_interface.send_accessory_short_event(address, state)
12# pi_sprog_interface.request_dcc_power_off()
13# pi_sprog_interface.request_dcc_power_on()
14# dcc_control.get_dcc_address_mappings()
15#
16# Uses the following common editor UI elements:
17# common.entry_box
18# common.integer_entry_box
19# common.scrollable_text_frame
20# common.CreateToolTip
21# common.dcc_entry_box
22#------------------------------------------------------------------------------------
24import tkinter as Tk
25import json
26import os
28from ..library import pi_sprog_interface
29from ..library import dcc_control
30from . import common
32#------------------------------------------------------------------------------------
33# Class for a CV Programming entry element
34#------------------------------------------------------------------------------------
36class cv_programming_entry():
37 def __init__(self, parent_frame, row):
38 self.configuration_variable = common.integer_entry_box(parent_frame, width=5, min_value=1,
39 max_value=1023, callback=self.cv_updated, allow_empty=True, empty_equals_zero=False,
40 tool_tip="Enter the number of the Configuration Variable (CV) to read or program")
41 self.configuration_variable.grid(column=0, row=row)
42 self.current_value = common.entry_box(parent_frame, width=5,
43 tool_tip="Last Read value of CV (select 'Read' to populate or refresh)")
44 self.current_value.configure(state="disabled", disabledforeground="black")
45 self.current_value.grid(column=1, row=row)
46 self.value_to_set = common.integer_entry_box(parent_frame, width=5, min_value=0, max_value=255,
47 allow_empty=True, empty_equals_zero=False, callback=self.value_updated,
48 tool_tip="Enter the new value to set (select 'write' to program)")
49 self.value_to_set.grid(column=2, row=row)
50 self.notes = common.entry_box(parent_frame, width=30,
51 tool_tip="Add notes for this CV / value")
52 self.notes.grid(column=3, row=row, sticky="ew")
54 def validate(self):
55 # No need to validate the current value as this is read only
56 return(self.configuration_variable.validate() and self.value_to_set.validate())
58 def cv_updated(self):
59 # If the CV entry has been updated set the current value back to unknown
60 # and set the colour of the value_to_set back to black (to be programmed)
61 self.current_value.set_value("")
62 self.value_to_set.config(fg="black")
64 def value_updated(self):
65 # If the CV value_to_set has been updated set the colour back to black
66 self.value_to_set.config(fg="black")
68#------------------------------------------------------------------------------------
69# Class for a grid of CV Programming entry elements (uxses class above)
70#------------------------------------------------------------------------------------
72class cv_programming_grid():
73 def __init__(self, parent_frame):
74 self.grid_frame = Tk.Frame(parent_frame)
75 self.grid_frame.pack(fill='x')
76 self.list_of_entries = []
77 number_of_columns = 2
78 number_of_rows = 15
79 # Create the columns of CV programming values
80 for column_index in range(number_of_columns):
81 # Enable the column to expand to fill the available space
82 self.grid_frame.columnconfigure(column_index, weight=1)
83 # Create a frame to hold the columns of values (allow it to expand)
84 # Also allow the 3rd column (holding the notes) to expand within it
85 self.frame = Tk.Frame(self.grid_frame)
86 self.frame.grid(row=0, column=column_index, padx=10, sticky="ew")
87 self.frame.columnconfigure(3, weight=1)
88 # Create the heading labels for the cv_programming_entry elements
89 # Pack the "Notes" heading so it can expand to fill the available space
90 self.label1 = Tk.Label(self.frame,text="CV")
91 self.label1.grid(row=0, column=0)
92 self.label2 = Tk.Label(self.frame,text="Value")
93 self.label2.grid(row=0, column=1)
94 self.label3 = Tk.Label(self.frame,text="New")
95 self.label3.grid(row=0, column=2)
96 self.label4 = Tk.Label(self.frame,text="Notes")
97 self.label4.grid(row=0, column=3, sticky="ew")
98 # Create the cv_programming_entry element
99 for row_index in range(number_of_rows):
100 self.list_of_entries.append(cv_programming_entry(self.frame,row=row_index+1))
102 def validate(self):
103 valid=True
104 for cv_entry_element in self.list_of_entries:
105 if not cv_entry_element.validate(): valid=False
106 return(valid)
108#------------------------------------------------------------------------------------
109# Class for the CV Programming UI Element (uses class above)
110#------------------------------------------------------------------------------------
112class cv_programming_element():
113 def __init__(self, root_window, parent_window, parent_frame, dcc_programming_enabled_function,
114 dcc_power_off_function, dcc_power_on_function):
115 self.dcc_programming_enabled_function = dcc_programming_enabled_function
116 self.dcc_power_off_function = dcc_power_off_function
117 self.dcc_power_on_function = dcc_power_on_function
118 self.root_window = root_window
119 self.parent_window = parent_window
120 # Default CV configuration filename
121 self.loaded_file = ""
122 # Create the warning text
123 self.label=Tk.Label(parent_frame,text="WARNING - Before programming CVs, ensure only the device to be "+
124 "programmed\nis connected to the DCC bus - all other devices should be disconnected", fg="red")
125 self.label.pack(padx=2, pady=2)
126 # Create the grid of CVs to programe
127 self.cv_grid = cv_programming_grid (parent_frame)
128 # Create the Read/Write Buttons and the status label in a subframe to center them
129 self.subframe1 = Tk.Frame(parent_frame)
130 self.subframe1.pack()
131 self.B1 = Tk.Button (self.subframe1, text = "Read CVs",command=self.read_all_cvs)
132 self.B1.pack(side=Tk.LEFT, padx=2, pady=2)
133 self.TT1 = common.CreateToolTip(self.B1, "Read all CVs to retrieve / refresh the current values")
134 self.B2 = Tk.Button (self.subframe1, text = "Write CVs",command=self.write_all_cvs)
135 self.B2.pack(side=Tk.LEFT, padx=2, pady=2)
136 self.TT2 = common.CreateToolTip(self.B2, "Write all CVs to set the new values")
137 self.status = Tk.Label(self.subframe1, width=45, borderwidth=1, relief="solid")
138 self.status.pack(side=Tk.LEFT, padx=2, pady=2, expand='y')
139 self.statusTT = common.CreateToolTip(self.status, "Displays the CV Read / Write progress and status")
140 # Create the notes/documentation text entry
141 self.notes = common.scrollable_text_frame(parent_frame, max_height=10, max_width=38,
142 min_height=5, min_width=38, editable=True, auto_resize=True)
143 self.notes.pack(padx=2, pady=2, fill='both', expand=True)
144 self.notes.set_value("Document your CV configuration here")
145 # Create the Save/load Buttons and the filename label in a subframe to center them
146 self.subframe2 = Tk.Frame(parent_frame)
147 self.subframe2.pack(fill='y')
148 self.B3 = Tk.Button (self.subframe2, text = "Open",command=self.load_config)
149 self.B3.pack(side=Tk.LEFT, padx=2, pady=2)
150 self.TT3 = common.CreateToolTip(self.B3, "Load a CV configuration from file")
151 self.B4 = Tk.Button (self.subframe2, text = "Save",command=lambda:self.save_config(save_as=False))
152 self.B4.pack(side=Tk.LEFT, padx=2, pady=2)
153 self.TT4 = common.CreateToolTip(self.B4, "Save the current CV configuration to file")
154 self.B5 = Tk.Button (self.subframe2, text = "Save as",command=lambda:self.save_config(save_as=True))
155 self.B5.pack(side=Tk.LEFT, padx=2, pady=2)
156 self.TT5 = common.CreateToolTip(self.B5, "Save the current CV configuration as a new file")
157 self.name=Tk.Label(self.subframe2, width=45, borderwidth=1, relief="solid")
158 self.name.pack(side=Tk.LEFT, padx=2, pady=2, expand='y')
159 self.nameTT = common.CreateToolTip(self.name, "Displays the name of the CV config file after save or load")
161 def read_all_cvs(self):
162 # Force a focus out event to "accept" all values before programming (if the focus out
163 # event is processed after programming it will be interpreted as the CV being updated
164 # which will then set the read value back to blank as the CV value may have been changed
165 self.B1.focus_set()
166 self.root_window.update()
167 # Check programmng is enabled (DCC power on - which implies SPROG is connected
168 if not self.dcc_programming_enabled_function():
169 self.status.config(text="Connect to SPROG and enable DCC power to read CVs", fg="red")
170 elif not self.cv_grid.validate():
171 self.status.config(text="Entries on form need correcting", fg="red")
172 else:
173 self.status.config(text="Reading CVs", fg="black")
174 for cv_entry_element in self.cv_grid.list_of_entries:
175 cv_entry_element.current_value.set_value("")
176 self.root_window.update_idletasks()
177 read_errors = False
178 for cv_entry_element in self.cv_grid.list_of_entries:
179 cv_to_read = cv_entry_element.configuration_variable.get_value()
180 if cv_to_read is not None:
181 cv_value = pi_sprog_interface.service_mode_read_cv(cv_to_read)
182 if cv_value is not None:
183 cv_entry_element.current_value.set_value(str(cv_value))
184 else:
185 cv_entry_element.current_value.set_value("---")
186 read_errors = True
187 self.root_window.update_idletasks()
188 if read_errors:
189 self.status.config(text="One or more CVs could not be read", fg="red")
190 else:
191 self.status.config(text="")
192 # Cycle the power to enable the changes (revert back to normal operation)
193 pi_sprog_interface.request_dcc_power_off()
194 pi_sprog_interface.request_dcc_power_on()
196 def write_all_cvs(self):
197 # Force a focus out event to "accept" all values before programming (if the focus out
198 # event is processed after programming it will be interpreted as the value being updated
199 # which will then set the colour of the value back to black as it may have been changed
200 self.B1.focus_set()
201 self.root_window.update()
202 # Check programmng is enabled (DCC power on - which implies SPROG is connected
203 if not self.dcc_programming_enabled_function():
204 self.status.config(text="Connect to SPROG and enable DCC power to write CVs", fg="red")
205 elif not self.cv_grid.validate():
206 self.status.config(text="Entries on form need correcting", fg="red")
207 else:
208 self.status.config(text="Writing CVs", fg="black")
209 for cv_entry_element in self.cv_grid.list_of_entries:
210 cv_entry_element.value_to_set.config(fg="black")
211 self.root_window.update_idletasks()
212 write_errors = False
213 for cv_entry_element in self.cv_grid.list_of_entries:
214 cv_to_write = cv_entry_element.configuration_variable.get_value()
215 value_to_write = cv_entry_element.value_to_set.get_value()
216 if cv_to_write is not None and value_to_write is not None:
217 write_success = pi_sprog_interface.service_mode_write_cv(cv_to_write,value_to_write)
218 if write_success:
219 cv_entry_element.value_to_set.config(fg="green")
220 else:
221 cv_entry_element.value_to_set.config(fg="red")
222 write_errors = True
223 self.root_window.update_idletasks()
224 if write_errors:
225 self.status.config(text="One or more CVs could not be written", fg="red")
226 else:
227 self.status.config(text="")
228 # Cycle the power to enable the changes (revert back to normal operation)
229 pi_sprog_interface.request_dcc_power_off()
230 pi_sprog_interface.request_dcc_power_on()
232 def save_config(self, save_as:bool):
233 self.B4.focus_set()
234 self.root_window.update()
235 if not self.cv_grid.validate():
236 self.status.config(text="Entries on form need correcting", fg="red")
237 else:
238 self.status.config(text="")
239 # Filename to save is the filename loaded - or ask the user
240 if self.loaded_file == "" or save_as:
241 initial_filename = os.path.split(self.loaded_file)[1]
242 filename_to_save=Tk.filedialog.asksaveasfilename(title='Save CV Configuration', parent=self.parent_window,
243 filetypes=(('CV configuration files','*.cvc'),('all files','*.*')),initialfile=initial_filename)
244 # Set the filename to blank if the user has cancelled out of (or closed) the dialogue
245 if filename_to_save == (): filename_to_save = ""
246 # If the filename is not blank enforce the '.cvc' extention
247 if filename_to_save != "" and not filename_to_save.endswith(".cvc"): filename_to_save.append(".cvc")
248 else:
249 filename_to_save = self.loaded_file
250 # Only continue (to save the file) if the filename is not blank
251 if filename_to_save != "":
252 # Create a json structure to save the data
253 data_to_save = {}
254 data_to_save["filename"] = filename_to_save
255 data_to_save["documentation"] = self.notes.get_value()
256 data_to_save["configuration"] = []
257 for cv_entry_element in self.cv_grid.list_of_entries:
258 cv_number = cv_entry_element.configuration_variable.get_value()
259 cv_value = cv_entry_element.value_to_set.get_value()
260 cv_notes = cv_entry_element.notes.get_value()
261 data_to_save["configuration"].append([cv_number,cv_value,cv_notes])
262 try:
263 file_contents = json.dumps(data_to_save,indent=3,sort_keys=False)
264 except Exception as exception:
265 Tk.messagebox.showerror(parent=self.parent_window,title="Data Error",message=str(exception))
266 else:
267 # write the json structure to file
268 try:
269 with open (filename_to_save,'w') as file: file.write(file_contents)
270 file.close
271 except Exception as exception:
272 Tk.messagebox.showerror(parent=self.parent_window,title="File Save Error",message=str(exception))
273 else:
274 self.loaded_file = filename_to_save
275 self.name.config(text="Configuration file: "+os.path.split(self.loaded_file)[1])
278 def load_config(self):
279 self.B4.focus_set()
280 self.root_window.update()
281 filename_to_load = Tk.filedialog.askopenfilename(parent=self.parent_window,title='Load CV configuration',
282 filetypes=(('cvc files','*.cvc'),('all files','*.*')),initialdir = '.')
283 # Set the filename to blank if the user has cancelled out of (or closed) the dialogue
284 if filename_to_load == (): filename_to_load = ""
285 # Only continue (to load the file) if the filename is not blank
286 if filename_to_load != "":
287 try:
288 with open (filename_to_load,'r') as file: loaded_data=file.read()
289 file.close
290 except Exception as exception:
291 Tk.messagebox.showerror(parent=self.parent_window,title="File Load Error", message=str(exception))
292 else:
293 try:
294 loaded_data = json.loads(loaded_data)
295 except Exception as exception:
296 Tk.messagebox.showerror(parent=self.parent_window,title="File Parse Error", message=str(exception))
297 else:
298 self.loaded_file = filename_to_load
299 self.name.config(text="Configuration file: "+os.path.split(self.loaded_file)[1])
300 self.notes.set_value(loaded_data["documentation"])
301 for index, cv_entry_element in enumerate(loaded_data["configuration"]):
302 self.cv_grid.list_of_entries[index].configuration_variable.set_value(cv_entry_element[0])
303 self.cv_grid.list_of_entries[index].value_to_set.set_value(cv_entry_element[1])
304 self.cv_grid.list_of_entries[index].notes.set_value(cv_entry_element[2])
305 self.cv_grid.list_of_entries[index].current_value.set_value("")
307#------------------------------------------------------------------------------------
308# Class for the "one touch" Programming UI Element (uses class above)
309#------------------------------------------------------------------------------------
311class one_touch_programming_element():
312 def __init__(self, parent_frame, dcc_programming_enabled_function):
313 self.dcc_programming_enabled_function = dcc_programming_enabled_function
314 # Create the Address and entry Buttons (in a subframe to center them)
315 self.subframe = Tk.Frame(parent_frame)
316 self.subframe.pack()
317 self.label = Tk.Label(self.subframe, text="Address to program")
318 self.label.pack(side=Tk.LEFT, padx=2, pady=2)
319 self.entry = common.dcc_entry_box(self.subframe)
320 self.entry.pack(side=Tk.LEFT, padx=2, pady=2)
321 self.B1 = Tk.Button (self.subframe, text = "On (fwd)",command=lambda:self.send_command(True))
322 self.B1.pack(side=Tk.LEFT, padx=2, pady=2)
323 self.TT1 = common.CreateToolTip(self.B1, "Send an ON command to the selected DCC address")
324 self.B2 = Tk.Button (self.subframe, text = "Off (rev)",command=lambda:self.send_command(False))
325 self.B2.pack(side=Tk.LEFT, padx=2, pady=2)
326 self.TT2 = common.CreateToolTip(self.B2, "Send an OFF command to the selected DCC address")
327 # Create the Status Label
328 self.status = Tk.Label(parent_frame, text="")
329 self.status.pack(padx=2, pady=2)
331 def send_command(self, command):
332 self.subframe.focus_set()
333 # Check programmng is enabled (DCC power on - which implies SPROG is connected
334 if not self.dcc_programming_enabled_function():
335 self.status.config(text="Connect to SPROG and enable DCC power to programme", fg="red")
336 elif not self.entry.validate() or self.entry.get_value() < 1:
337 self.status.config(text="Entered DCC address is invalid", fg="red")
338 else:
339 self.status.config(text="")
340 pi_sprog_interface.send_accessory_short_event(self.entry.get_value(), command)
342#------------------------------------------------------------------------------------
343# Class for the "DCC Programming" window - Uses the classes above
344# Note that if a window is already open then we just raise it and exit
345#------------------------------------------------------------------------------------
347dcc_programming_window = None
349class dcc_programming():
350 def __init__(self, root_window, dcc_programming_enabled_function, dcc_power_off_function, dcc_power_on_function):
351 global dcc_programming_window
352 # If there is already a dcc programming window open then we just make it jump to the top and exit
353 if dcc_programming_window is not None:
354 dcc_programming_window.lift()
355 dcc_programming_window.state('normal')
356 dcc_programming_window.focus_force()
357 else:
358 # Create the top level window for DCC Programming
359 self.window = Tk.Toplevel(root_window)
360 self.window.title("DCC Programming")
361 self.window.protocol("WM_DELETE_WINDOW", self.ok)
362 self.window.resizable(False, False)
363 dcc_programming_window = self.window
364 # Create the ok/close button and tooltip - pack first so it remains visible on re-sizing
365 self.B1 = Tk.Button (self.window, text = "Ok / Close", command=self.ok)
366 self.TT1 = common.CreateToolTip(self.B1, "Close window")
367 self.B1.pack(padx=5, pady=5, side=Tk.BOTTOM)
368 # Create an overall frame to pack everything else in
369 self.frame = Tk.Frame(self.window)
370 self.frame.pack(fill='both', expand=True)
371 # Create the labelframe for "one Touch" DCC Programming (this gets packed later)
372 self.labelframe1 = Tk.LabelFrame(self.frame, text="DCC One Touch Programming")
373 self.labelframe1.pack(padx=2, pady=2, fill='x')
374 self.one_touch_programming = one_touch_programming_element(self.labelframe1, dcc_programming_enabled_function)
375 # Create the labelframe for CV Programming (this gets packed later)
376 self.labelframe2 = Tk.LabelFrame(self.frame, text="DCC Configuration Variable (CV) Programming")
377 self.labelframe2.pack(padx=2, pady=2, fill='both', expand=True)
378 self.cv_programming = cv_programming_element(root_window, self.window, self.labelframe2,
379 dcc_programming_enabled_function, dcc_power_off_function, dcc_power_on_function)
381 def ok(self):
382 global dcc_programming_window
383 dcc_programming_window = None
384 self.window.destroy()
386#------------------------------------------------------------------------------------
387# Class for the "DCC Mappings" window
388# Note that if a window is already open then we just raise it and exit
389#------------------------------------------------------------------------------------
391dcc_mappings_window = None
393class dcc_mappings():
394 def __init__(self, root_window):
395 global dcc_mappings_window
396 # If there is already a window open then we just make it jump to the top and exit
397 if dcc_mappings_window is not None:
398 dcc_mappings_window.lift()
399 dcc_mappings_window.state('normal')
400 dcc_mappings_window.focus_force()
401 else:
402 # Create the top level window
403 self.window = Tk.Toplevel(root_window)
404 self.window.title("DCC Mappings")
405 self.window.protocol("WM_DELETE_WINDOW", self.ok)
406 self.window.resizable(False, False)
407 dcc_mappings_window = self.window
408 # Create the ok/close and refresh buttons - pack first so they remain visible on re-sizing
409 self.frame1 = Tk.Frame(self.window)
410 self.frame1.pack(fill='x', expand=True, side=Tk.BOTTOM)
411 # Create a subframe to center the buttons in
412 self.subframe = Tk.Frame(self.frame1)
413 self.subframe.pack()
414 self.B1 = Tk.Button (self.subframe, text = "Ok / Close",command=self.ok)
415 self.B1.pack(side=Tk.LEFT, padx=2, pady=2)
416 self.TT1 = common.CreateToolTip(self.B1, "Close window")
417 self.B2 = Tk.Button (self.subframe, text = "Refresh",command=self.refresh_display)
418 self.B2.pack(side=Tk.LEFT, padx=2, pady=2)
419 self.TT1 = common.CreateToolTip(self.B2, "Reload the current DCC address mappings")
420 # Create an overall frame to pack everything else in
421 self.mappings_frame = None
422 self.refresh_display()
424 def ok(self):
425 global dcc_mappings_window
426 dcc_mappings_window = None
427 self.window.destroy()
429 def refresh_display(self):
430 # Create a frame to hold the mappings (destroy the old one first if needed)
431 if self.mappings_frame is not None:
432 self.mappings_frame.destroy()
433 self.mappings_frame = Tk.Frame(self.window)
434 self.mappings_frame.pack(fill='both', expand=True)
435 #create a subframe
436 # Retrieve the sorted dictionary of DCC address mappings
437 dcc_address_mappings = dcc_control.get_dcc_address_mappings()
438 # If there are no mappings then just display a warning
439 if len(dcc_address_mappings) == 0:
440 label = Tk.Label(self.mappings_frame, text="No DCC address mappings defined")
441 label.pack(padx=20, pady=20)
442 else:
443 # Build the table of DCC mappings
444 row_index = 0
445 for dcc_address in dict(sorted(dcc_address_mappings.items())):
446 if row_index == 0:
447 column_frame = Tk.LabelFrame(self.mappings_frame, text="DCC addresses")
448 column_frame.pack(side=Tk.LEFT, pady=2, padx=2, fill='both', expand=True, anchor='n')
449 # Create a subframe for the row (pack in the column frame)
450 row_frame = Tk.Frame(column_frame)
451 row_frame.pack(fill='x')
452 # Create the labels with the DCC mapping text (pack in the row subframe)
453 mapping_text = u"\u2192"+" "+dcc_address_mappings[dcc_address][0]+" "+str(dcc_address_mappings[dcc_address][1])
454 label1 = Tk.Label(row_frame, width=5, text=dcc_address, anchor="e")
455 label1.pack(side=Tk.LEFT)
456 label2 = Tk.Label(row_frame, width=10, text=mapping_text, anchor="w")
457 label2.pack(side=Tk.LEFT)
458 row_index = row_index + 1
459 if row_index >= 20: row_index = 0
461#############################################################################################