Coverage for /home/pi/Software/model-railway-signalling/model_railway_signals/editor/menubar_windows.py: 12%
683 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-03-27 14:35 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2024-03-27 14:35 +0000
1#------------------------------------------------------------------------------------
2# This module contains all the functions for the menubar selection windows
3#
4# Classes (pop up windows) called from the main editor module menubar selections
5# display_help(root)
6# display_about(root)
7# edit_layout_info()
8# edit_mqtt_settings(root, mqtt_connect_callback, mqtt_update_callback)
9# edit_sprog_settings(root, sprog_connect_callback, sprog_update_callback)
10# edit_logging_settings(root, logging_update_callback)
11# edit_canvas_settings(root, canvas_update_callback)
12# edit_gpio_settings(root, gpio_update_callback)
13#
14# Makes the following external API calls to other editor modules:
15# settings.get_canvas() - Get the current canvas settings (for editing)
16# settings.set_canvas() - Save the new canvas settings (as specified)
17# settings.get_logging() - Get the current log level (for editing)
18# settings.set_logging(level) - Save the new log level (as specified)
19# settings.get_general() - Get the current settings (layout info, version for info/editing)
20# settings.set_general() - Save the new settings (only layout info can be edited/saved)
21# settings.get_sprog() - Get the current SPROG settings (for editing)
22# settings.set_sprog() - Save the new SPROG settings (as specified)
23# settings.get_mqtt() - Get the current MQTT settings (for editing)
24# settings.set_mqtt() - Save the new MQTT settings (as specified)
25# settings.get_gpio() - Get the current GPIO settings (for editing)
26# settings.set_gpio() - Save the new GPIO settings (as specified)
27# settings.get_sub_dcc_nodes() - get the list of subscribed dccc command feeds
28# settings.get_sub_signals() - get the list of subscribed items
29# settings.get_sub_sections() - get the list of subscribed items
30# settings.get_sub_instruments() - get the list of subscribed items
31# settings.get_sub_sensors() - get the list of subscribed items
32# settings.get_pub_dcc() - get the publish dcc command feed flag
33# settings.get_pub_signals() - get the list of items to publish
34# settings.get_pub_sections() - get the list of items to publish
35# settings.get_pub_instruments() - get the list of items to publish
36# settings.get_pub_sensors() - get the list of items to publish
37# settings.set_sub_dcc_nodes() - set the list of subscribed nodes
38# settings.set_sub_signals() - set the list of subscribed items
39# settings.set_sub_sections() - set the list of subscribed items
40# settings.set_sub_instruments() - set the list of subscribed items
41# settings.set_sub_sensors() - set the list of subscribed items
42# settings.set_pub_dcc() - set the publish dcc command feed flag
43# settings.set_pub_signals() - set the list of items to publish
44# settings.set_pub_sections() - set the list of items to publish
45# settings.set_pub_instruments() - set the list of items to publish
46# settings.set_pub_sensors() - set the list of items to publish
47#
48# Uses the following common editor UI elements:
49# common.selection_buttons
50# common.check_box
51# common.entry_box
52# common.integer_entry_box
53# common.window_controls
54# common.CreateToolTip
55# common.scrollable_text_box
56#
57# Uses the following library functions:
58# gpio_sensors.get_list_of_available_ports() - to get a list of supported ports
59# mqtt_interface.get_node_status() - to get a list of connected nodes and timestamps
60#------------------------------------------------------------------------------------
62import tkinter as Tk
63import webbrowser
65from tkinter import ttk
67import time
68import datetime
70from . import common
71from . import settings
72from ..library import gpio_sensors
73from ..library import mqtt_interface
75#------------------------------------------------------------------------------------
76# Class for the "Help" window - Uses the common.scrollable_text_box.
77# Note that if a window is already open then we just raise it and exit.
78#------------------------------------------------------------------------------------
80help_text = """
81Schematic editor functions (Edit Mode):
831) Use the buttons on the left to add objects to the schematic.
842) Left-click to select objects (shift-left-click will add/remove from the selection).
853) Left-click/release when over an object to drag/drop selected objects.
864) Left-click/release when not over an object to seleact an 'area'.
875) Left-click/release on the 'end' of a selected line to move the line end.
886) Double-left-click on a schematic object to open the object configuraton window
897) Right-click on an object or the canvas to bring up additional options
908) <r> will rotate all selected point and signal objects by 180 degrees
919) <s> will snap all selected objects to the grid ('snap-to-grid' enabled or disabled)
9210) <backspace> will delete all currently selected objects from the schematic
9311) <cntl-c> / <cntl-v> will copy/paste all currently selected objects
9412) <cntl-z> / <cntl-y> will undo/redo schematic and object configuration changes
9513) <cntl-s> will toggle 'snap-to-grid' on/off for moving objects in Edit Mode
9614) <cntl-r> will re-size the window to fit the canvas (following user re-sizing)
9715) <Esc> will deselect all objects (or cancel the move of selected objects)
9816) Arrow keys will 'nudge' selected objects (or scroll the canvas if nothing selected)
9917) <cntl-m> will toggle the schematic editor between Edit Mode and Run Mode
101Schematic editor functions (Run Mode):
1031) <cntl-a> will toggle the signal automation on / off when in Run Mode
1042) <cntl-r> will re-size the window to fit the canvas (following user re-sizing)
1053) <cntl-m> will toggle the schematic editor between Edit Mode and Run Mode
1064) Arrow keys will scroll the canvas area (if the canvas is bigger than the window)
108Menubar Options
1101) File - All the save/load/new functions you would expect
1112) Mode => Edit/Run/Reset - Select Edit or Run Mode (also Reset layout to default state)
1123) Automation => Enable/Disable - Toggle signal automation functions (in Run Mode)
1134) SPROG => Connect/Disconnect - Toggle the connection to the SPROG DCC Command Station
1145) DCC Power => Enable/Disable - Toggle the DCC bus supply (SPROG must be connected)
1156) MQTT => Connect/disconnect - Toggle connection to an external MQTT broker
1167) Utilities => DCC Programmming - One touch and CV programming of signals/points
1178) Settings => Canvas - Change the layout display size and grid configuration
1189) Settings => MQTT - Configure the MQTT broker and signalling networking
11910) Settings => SPROG - Configure the serial port and SPROG behavior
12011) Settings => Logging - Set the log level for running the layout
12112) Settings => Sensors - Define the Ri-Pi GPIO port to track sensor mappings
12213) Help => About - Application version and licence information
12313) Help => Info - Add user notes to document your layout configuration
125"""
127help_window = None
129class display_help():
130 def __init__(self, root_window):
131 global help_window
132 # If there is already a window open then we just make it jump to the top and exit
133 if help_window is not None:
134 help_window.lift()
135 help_window.state('normal')
136 help_window.focus_force()
137 else:
138 # Create the top level window for application help
139 self.window = Tk.Toplevel(root_window)
140 self.window.title("Application Help")
141 self.window.protocol("WM_DELETE_WINDOW", self.ok)
142 help_window = self.window
143 # Create the link to the Quickstart Guide
144 self.frame = Tk.Frame(self.window)
145 self.frame.pack(padx=5, pady=5)
146 self.label1=Tk.Label(self.frame, text="Application quickstart guide can be downloaded from: ")
147 self.label1.pack(side=Tk.LEFT, pady=5)
148 self.hyperlink = "https://www.model-railway-signalling.co.uk/"
149 self.label2 = Tk.Label(self.frame, text=self.hyperlink, fg="blue", cursor="hand2")
150 self.label2.pack(side=Tk.LEFT, pady=5)
151 self.label2.bind("<Button-1>", self.callback)
152 # Create the srollable textbox to display the help text. We only specify
153 # the max height (in case the help text grows in the future) leaving
154 # the width to auto-scale to the maximum width of the help text
155 self.text = common.scrollable_text_frame(self.window, max_height=25)
156 self.text.set_value(help_text)
157 # Create the ok/close button and tooltip
158 self.B1 = Tk.Button (self.window, text = "Ok / Close", command=self.ok)
159 self.TT1 = common.CreateToolTip(self.B1, "Close window")
160 # Pack the OK button First - so it remains visible on re-sizing
161 self.B1.pack(padx=5, pady=5, side=Tk.BOTTOM)
162 self.text.pack(padx=2, pady=2, fill=Tk.BOTH, expand=True)
164 def callback(self,event):
165 webbrowser.open_new_tab(self.hyperlink)
167 def ok(self):
168 global help_window
169 help_window = None
170 self.window.destroy()
172#------------------------------------------------------------------------------------
173# Class for the "About" window - uses a hyperlink to go to the github repo.
174# Note that if a window is already open then we just raise it and exit.
175#------------------------------------------------------------------------------------
177# The version is the third parameter provided by 'get_general'
178about_text = """
179Model Railway Signals ("""+settings.get_general()[2]+""")
181An application for designing and developing fully interlocked and automated model railway
182signalling systems with DCC control of signals and points via the SPROG Command Station.
184This software is released under the GNU General Public License Version 2, June 1991
185meaning you are free to use, share or adapt the software as you like
186but must ensure those same rights are passed on to all recipients.
188For more information visit: """
190about_window = None
192class display_about():
193 def __init__(self, root_window):
194 global about_window
195 # If there is already a window open then we just make it jump to the top and exit
196 if about_window is not None:
197 about_window.lift()
198 about_window.state('normal')
199 about_window.focus_force()
200 else:
201 # Create the (non-resizable) top level window for application about
202 self.window = Tk.Toplevel(root_window)
203 self.window.title("Application Info")
204 self.window.protocol("WM_DELETE_WINDOW", self.ok)
205 self.window.resizable(False, False)
206 about_window = self.window
207 # Create the Help text and hyperlink
208 self.label1 = Tk.Label(self.window, text=about_text)
209 self.label1.pack(padx=5, pady=5)
210 self.hyperlink = "https://www.model-railway-signalling.co.uk/"
211 self.label2 = Tk.Label(self.window, text=self.hyperlink, fg="blue", cursor="hand2")
212 self.label2.pack(padx=5, pady=5)
213 self.label2.bind("<Button-1>", self.callback)
214 # Create the close button and tooltip
215 self.B1 = Tk.Button (self.window, text = "Ok / Close",command=self.ok)
216 self.B1.pack(padx=2, pady=2)
217 self.TT1 = common.CreateToolTip(self.B1, "Close window")
219 def ok(self):
220 global about_window
221 about_window = None
222 self.window.destroy()
224 def callback(self,event):
225 webbrowser.open_new_tab(self.hyperlink)
227#------------------------------------------------------------------------------------
228# Class for the Edit Layout Information window - Uses the common.scrollable_text_box.
229# Note that if a window is already open then we just raise it and exit.
230#------------------------------------------------------------------------------------
232edit_layout_info_window = None
234class edit_layout_info():
235 def __init__(self, root_window):
236 global edit_layout_info_window
237 # If there is already a window open then we just make it jump to the top and exit
238 if edit_layout_info_window is not None:
239 edit_layout_info_window.lift()
240 edit_layout_info_window.state('normal')
241 edit_layout_info_window.focus_force()
242 else:
243 # Create the top level window for application help
244 self.window = Tk.Toplevel(root_window)
245 self.window.title("Layout Info")
246 self.window.protocol("WM_DELETE_WINDOW", self.close_window)
247 edit_layout_info_window = self.window
248 # Create the srollable textbox to display the text. We specify
249 # the max height/width (in case the text grows in the future) and also
250 # the min height/width (to give the user something to start with)
251 self.text = common.scrollable_text_frame(self.window, max_height=40,max_width=100,
252 min_height=10, min_width=40, editable=True, auto_resize=True)
253 # Create the common Apply/OK/Reset/Cancel buttons for the window
254 self.controls = common.window_controls(self.window, self.load_state, self.save_state, self.close_window)
255 # We need to pack the window buttons at the bottom and then pack the text
256 # frame - so the buttons remain visible if the user re-sizes the window
257 self.controls.frame.pack(side=Tk.BOTTOM, padx=2, pady=2)
258 self.text.pack(padx=2, pady=2, fill=Tk.BOTH, expand=True)
259 # Load the initial UI state
260 self.load_state()
262 def load_state(self):
263 # The version is the forth parameter provided by 'get_general'
264 self.text.set_value(settings.get_general()[3])
266 def save_state(self, close_window:bool):
267 settings.set_general(info=self.text.get_value())
268 # close the window (on OK)
269 if close_window: self.close_window()
271 def close_window(self):
272 global edit_layout_info_window
273 edit_layout_info_window = None
274 self.window.destroy()
276#------------------------------------------------------------------------------------
277# Class for the Canvas configuration toolbar window. Note the init function takes
278# in a callback so it can apply the updated settings in the main editor application.
279# Note also that if a window is already open then we just raise it and exit.
280#------------------------------------------------------------------------------------
282canvas_settings_window = None
284class edit_canvas_settings():
285 def __init__(self, root_window, update_function):
286 global canvas_settings_window
287 # If there is already a window open then we just make it jump to the top and exit
288 if canvas_settings_window is not None:
289 canvas_settings_window.lift()
290 canvas_settings_window.state('normal')
291 canvas_settings_window.focus_force()
292 else:
293 self.update_function = update_function
294 # Create the (non resizable) top level window for the canvas settings
295 self.window = Tk.Toplevel(root_window)
296 self.window.title("Canvas")
297 self.window.protocol("WM_DELETE_WINDOW", self.close_window)
298 self.window.resizable(False, False)
299 canvas_settings_window = self.window
300 # Create the entry box elements for the width, height and grid
301 # Pack the elements as a grid to get an aligned layout
302 self.frame = Tk.Frame(self.window)
303 self.frame.pack()
304 self.frame.grid_columnconfigure(0, weight=1)
305 self.frame.grid_columnconfigure(1, weight=1)
306 self.label1 = Tk.Label(self.frame, text="Canvas width:")
307 self.label1.grid(row=0, column=0)
308 self.width = common.integer_entry_box(self.frame, width=5, min_value=400, max_value=8000,
309 allow_empty=False, tool_tip="Enter width in pixels (400-8000)")
310 self.width.grid(row=0, column=1)
311 self.label2 = Tk.Label(self.frame, text="Canvas height:")
312 self.label2.grid(row=1, column=0)
313 self.height = common.integer_entry_box(self.frame, width=5, min_value=200, max_value=4000,
314 allow_empty=False, tool_tip="Enter height in pixels (200-4000)")
315 self.height.grid(row=1, column=1)
316 self.label3 = Tk.Label(self.frame, text="Canvas Grid:")
317 self.label3.grid(row=2, column=0)
318 self.grid = common.integer_entry_box(self.frame, width=5, min_value=5, max_value=25,
319 allow_empty=False, tool_tip="Enter grid size in pixels (5-25)")
320 self.grid.grid(row=2, column=1)
321 # Create the check box element for snap to grid
322 self.snap = common.check_box (self.window, label="Snap to Grid",
323 tool_tip="Enable/Disable 'Snap-to-Grid' for schematic editing")
324 self.snap.pack(padx=2, pady=2)
325 # Create the common Apply/OK/Reset/Cancel buttons for the window
326 self.controls = common.window_controls(self.window, self.load_state, self.save_state, self.close_window)
327 self.controls.frame.pack(padx=2, pady=2)
328 # Create the Validation error message (this gets packed/unpacked on apply/save)
329 self.validation_error = Tk.Label(self.window, text="Errors on Form need correcting", fg="red")
330 # Load the initial UI state
331 self.load_state()
333 def load_state(self):
334 self.validation_error.pack_forget()
335 width, height, grid, snap_to_grid = settings.get_canvas()
336 self.width.set_value(width)
337 self.height.set_value(height)
338 self.grid.set_value(grid)
339 self.snap.set_value(snap_to_grid)
341 def save_state(self, close_window:bool):
342 # Only allow the changes to be applied / window closed if both values are valid
343 if self.width.validate() and self.height.validate() and self.grid.validate():
344 self.validation_error.pack_forget()
345 width = self.width.get_value()
346 height = self.height.get_value()
347 grid = self.grid.get_value()
348 snap_to_grid = self.snap.get_value()
349 settings.set_canvas(width=width, height=height, grid=grid, snap_to_grid=snap_to_grid)
350 # Make the callback to apply the updated settings
351 self.update_function()
352 # close the window (on OK)
353 if close_window: self.close_window()
354 else:
355 # Display the validation error message
356 self.validation_error.pack(side=Tk.BOTTOM, before=self.controls.frame)
358 def close_window(self):
359 global canvas_settings_window
360 canvas_settings_window = None
361 self.window.destroy()
363#------------------------------------------------------------------------------------
364# Class for the SPROG settings selection toolbar window. Note the init function takes
365# in callbacks for connecting to the SPROG and for applying the updated settings.
366# Note also that if a window is already open then we just raise it and exit.
367#------------------------------------------------------------------------------------
369edit_sprog_settings_window = None
371class edit_sprog_settings():
372 def __init__(self, root_window, connect_function, update_function):
373 global edit_sprog_settings_window
374 # If there is already a window open then we just make it jump to the top and exit
375 if edit_sprog_settings_window is not None:
376 edit_sprog_settings_window.lift()
377 edit_sprog_settings_window.state('normal')
378 edit_sprog_settings_window.focus_force()
379 else:
380 self.connect_function = connect_function
381 self.update_function = update_function
382 # Create the (non resizable) top level window for the SPROG configuration
383 self.window = Tk.Toplevel(root_window)
384 self.window.title("SPROG DCC")
385 self.window.protocol("WM_DELETE_WINDOW", self.close_window)
386 self.window.resizable(False, False)
387 edit_sprog_settings_window = self.window
388 # Create the Serial Port and baud rate UI elements
389 self.frame1 = Tk.Frame(self.window)
390 self.frame1.pack()
391 self.label1 = Tk.Label(self.frame1, text="Port:")
392 self.label1.pack(side=Tk.LEFT, padx=2, pady=2)
393 self.port = common.entry_box(self.frame1, width=15,tool_tip="Specify "+
394 "the serial port to use for communicating with the SPROG")
395 self.port.pack(side=Tk.LEFT, padx=2, pady=2)
396 self.label2 = Tk.Label(self.frame1, text="Baud:")
397 self.label2.pack(side=Tk.LEFT, padx=2, pady=2)
398 self.options = ['115200','460800']
399 self.baud_selection = Tk.StringVar(self.window, "")
400 self.baud = Tk.OptionMenu(self.frame1, self.baud_selection, *self.options)
401 menu_width = len(max(self.options, key=len))
402 self.baud.config(width=menu_width)
403 common.CreateToolTip(self.baud, "Select the baud rate to use for the serial port "
404 +"(115200 for Pi-SPROG3 or 460800 for Pi-SPROG3 v2)")
405 self.baud.pack(side=Tk.LEFT, padx=2, pady=2)
406 # Create the remaining UI elements
407 self.debug = common.check_box(self.window, label="Enhanced SPROG debug logging", width=28,
408 tool_tip="Select to enable enhanced debug logging (Layout log level must also be set "+
409 "to 'debug')")
410 self.debug.pack(padx=2, pady=2)
411 self.startup = common.check_box(self.window, label="Initialise SPROG on layout load", width=28,
412 tool_tip="Select to configure serial port and initialise SPROG following layout load",
413 callback=self.selection_changed)
414 self.startup.pack(padx=2, pady=2)
415 self.power = common.check_box(self.window, label="Enable DCC power on layout load", width=28,
416 tool_tip="Select to enable DCC accessory bus power following layout load")
417 self.power.pack(padx=2, pady=2)
418 # Create the Button to test connectivity
419 self.B1 = Tk.Button (self.window, text="Test SPROG connectivity",command=self.test_connectivity)
420 self.B1.pack(padx=2, pady=2)
421 self.TT1 = common.CreateToolTip(self.B1, "Will configure/open the specified serial port and request "+
422 "the command station status to confirm a connection to the SPROG has been established")
423 # Create the Status Label
424 self.status = Tk.Label(self.window, text="")
425 self.status.pack(padx=2, pady=2)
426 # Create the common Apply/OK/Reset/Cancel buttons for the window
427 self.controls = common.window_controls(self.window, self.load_state, self.save_state, self.close_window)
428 self.controls.frame.pack(padx=2, pady=2)
429 # Load the initial UI state
430 self.load_state()
432 def selection_changed(self):
433 # If connect on startup is selected then enable the DCC power on startup selection
434 if self.startup.get_value(): self.power.enable()
435 else: self.power.disable()
437 def test_connectivity(self):
438 # Validate the port to "accept" the current value and focus out (onto the button)
439 self.port.validate()
440 self.B1.focus()
441 # Save the existing settings (as they haven't been "applied" yet)
442 s1, s2, s3, s4, s5 = settings.get_sprog()
443 # Apply the current settings (as thery currently appear in the UI)
444 baud = int(self.baud_selection.get())
445 port = self.port.get_value()
446 debug = self.debug.get_value()
447 startup = self.startup.get_value()
448 power = self.power.get_value()
449 settings.set_sprog(port=port, baud=baud, debug=debug, startup=startup, power=power)
450 # The Sprog Connect function will return True if successful
451 # It will also update the Menubar to reflect the SPROG connection status
452 if self.connect_function(show_popup=False):
453 self.status.config(text="SPROG successfully connected", fg="green")
454 else:
455 self.status.config(text="SPROG connection failure", fg="red")
456 # Now restore the existing settings (as they haven't been "applied" yet)
457 settings.set_sprog(s1, s2, s3, s4, s5)
459 def load_state(self):
460 # Reset the Test connectivity message
461 self.status.config(text="")
462 port, baud, debug, startup, power = settings.get_sprog()
463 self.port.set_value(port)
464 self.baud_selection.set(str(baud))
465 self.debug.set_value(debug)
466 self.startup.set_value(startup)
467 self.power.set_value(power)
468 self.selection_changed()
470 def save_state(self, close_window:bool):
471 # Validate the port to "accept" the current value
472 self.port.validate()
473 baud = int(self.baud_selection.get())
474 port = self.port.get_value()
475 debug = self.debug.get_value()
476 startup = self.startup.get_value()
477 power = self.power.get_value()
478 # Save the updated settings
479 settings.set_sprog(port=port, baud=baud, debug=debug, startup=startup, power=power)
480 # Make the callback to apply the updated settings
481 self.update_function()
482 # close the window (on OK)
483 if close_window: self.close_window()
485 def close_window(self):
486 global edit_sprog_settings_window
487 edit_sprog_settings_window = None
488 self.window.destroy()
490#------------------------------------------------------------------------------------
491# Class for the Logging Level selection toolbar window. Note the init function takes
492# in a callback so it can apply the updated settings in the main editor application.
493# Note also that if a window is already open then we just raise it and exit.
494#------------------------------------------------------------------------------------
496edit_logging_settings_window = None
498class edit_logging_settings():
499 def __init__(self, root_window, update_function):
500 global edit_logging_settings_window
501 # If there is already a window open then we just make it jump to the top and exit
502 if edit_logging_settings_window is not None:
503 edit_logging_settings_window.lift()
504 edit_logging_settings_window.state('normal')
505 edit_logging_settings_window.focus_force()
506 else:
507 self.update_function = update_function
508 # Create the (non resizable) top level window for the Logging Configuration
509 self.window = Tk.Toplevel(root_window)
510 self.window.title("Logging")
511 self.window.protocol("WM_DELETE_WINDOW", self.close_window)
512 self.window.resizable(False, False)
513 edit_logging_settings_window = self.window
514 # Create the logging Level selections element
515 self.log_level = common.selection_buttons (self.window, label="Layout Log Level",
516 b1="Error", b2="Warning", b3="Info", b4="Debug",
517 tool_tip="Set the logging level for running the layout")
518 self.log_level.frame.pack()
519 # Create the common Apply/OK/Reset/Cancel buttons for the window
520 self.controls = common.window_controls(self.window, self.load_state, self.save_state, self.close_window)
521 self.controls.frame.pack(padx=2, pady=2)
522 # Load the initial UI state
523 self.load_state()
525 def load_state(self):
526 self.log_level.set_value(settings.get_logging())
528 def save_state(self, close_window:bool):
529 log_level = self.log_level.get_value()
530 settings.set_logging(log_level)
531 # Make the callback to apply the updated settings
532 self.update_function()
533 # close the window (on OK )
534 if close_window: self.close_window()
536 def close_window(self):
537 global edit_logging_settings_window
538 edit_logging_settings_window = None
539 self.window.destroy()
541#------------------------------------------------------------------------------------
542# Class for the MQTT 'Broker configuration' Tab
543#------------------------------------------------------------------------------------
545class mqtt_configuration_tab():
546 def __init__(self, parent_tab, connect_function):
547 self.connect_function = connect_function
548 # Create a label frame for the Broker configuration
549 self.frame1 = Tk.LabelFrame(parent_tab, text="Broker configuration")
550 self.frame1.pack(padx=2, pady=2, fill='x')
551 # Create the Serial Port and baud rate UI elements
552 self.subframe1 = Tk.Frame(self.frame1)
553 self.subframe1.pack(padx=2, pady=2)
554 self.label1 = Tk.Label(self.subframe1, text="Address:")
555 self.label1.pack(side=Tk.LEFT, padx=2, pady=2)
556 self.url = common.entry_box(self.subframe1, width=32,tool_tip="Specify the URL or IP address of "+
557 "the MQTT broker (specify 'localhost' for a Broker running on the local machine)")
558 self.url.pack(side=Tk.LEFT, padx=2, pady=2)
559 self.subframe2 = Tk.Frame(self.frame1)
560 self.subframe2.pack(padx=2, pady=2)
561 self.label2 = Tk.Label(self.subframe2, text="Port:")
562 self.label2.pack(side=Tk.LEFT, padx=2, pady=2)
563 self.port = common.integer_entry_box(self.subframe2, width=6, min_value=1, max_value=65535,
564 allow_empty=False, tool_tip="Specify the TCP/IP Port to use for the Broker (default is usually 1883)")
565 self.port.pack(side=Tk.LEFT, padx=2, pady=2)
566 # Create the User Name and Password elements
567 self.subframe3 = Tk.Frame(self.frame1)
568 self.subframe3.pack(padx=2, pady=2)
569 self.label3 = Tk.Label(self.subframe3, text="Username:", width=10)
570 self.label3.pack(side=Tk.LEFT, padx=2, pady=2)
571 self.username = common.entry_box(self.subframe3, width=25,tool_tip=
572 "Specify the username for connecting to the broker")
573 self.username.pack(side=Tk.LEFT, padx=2, pady=2)
574 self.subframe4 = Tk.Frame(self.frame1)
575 self.subframe4.pack(padx=2, pady=2)
576 self.label4 = Tk.Label(self.subframe4, text="Password:", width=10)
577 self.label4.pack(side=Tk.LEFT, padx=2, pady=2)
578 self.password = common.entry_box(self.subframe4, width=25,tool_tip="Specify the password (WARNING DO NOT "+
579 "RE-USE AN EXISTING PASSWORD AS THIS IS SENT OVER THE NETWORK UNENCRYPTED)")
580 self.password.pack(side=Tk.LEFT, padx=2, pady=2)
581 # Create a label frame for the Broker configuration
582 self.frame2 = Tk.LabelFrame(parent_tab, text="Network configuration")
583 self.frame2.pack(padx=2, pady=2, fill='x')
584 # Create the Network name and node name elements
585 self.subframe5 = Tk.Frame(self.frame2)
586 self.subframe5.pack(padx=2, pady=2)
587 self.label5 = Tk.Label(self.subframe5, text="Network:")
588 self.label5.pack(side=Tk.LEFT, padx=2, pady=2)
589 self.network = common.entry_box(self.subframe5, width=15,tool_tip=
590 "Specify a name for this layout signalling network (common across all instances of the "+
591 "application being used to control the different signalling areas on the layout)")
592 self.network.pack(side=Tk.LEFT, padx=2, pady=2)
593 self.label4 = Tk.Label(self.subframe5, text="Node:")
594 self.label4.pack(side=Tk.LEFT, padx=2, pady=2)
595 self.node = common.entry_box(self.subframe5, width=5, tool_tip=
596 "Specify a unique identifier for this node (signalling area) on the network")
597 self.node.pack(side=Tk.LEFT, padx=2, pady=2)
598 # Create the remaining UI elements
599 self.debug = common.check_box(self.frame2, label="Enhanced MQTT debug logging", width=32,
600 tool_tip="Select to enable enhanced debug logging (Layout log level must also be set to 'debug')")
601 self.debug.pack(padx=2, pady=2)
602 self.startup = common.check_box(self.frame2, label="Connect to Broker on layout load", width=32,
603 tool_tip="Select to configure MQTT networking and connect to the broker following layout load")
604 self.startup.pack(padx=2, pady=2)
605 self.pubshutdown = common.check_box(self.frame2, label="Publish shutdown on application exit", width=32,
606 tool_tip="Select to publish a shutdown command to other network nodes when exiting this application")
607 self.pubshutdown.pack(padx=2, pady=2)
608 self.subshutdown = common.check_box(self.frame2, label="Quit application on reciept of shutdown", width=32,
609 tool_tip="Select to shutdown and exit this application on reciept of a shutdown command published by another node")
610 self.subshutdown.pack(padx=2, pady=2)
611 # Create the Button to test connectivity
612 self.B1 = Tk.Button (parent_tab, text="Test Broker connectivity",command=self.test_connectivity)
613 self.B1.pack(padx=2, pady=2)
614 self.TT1 = common.CreateToolTip(self.B1, "Will attempt to establish a connection to the broker")
615 # Create the Status Label
616 self.status = Tk.Label(parent_tab, text="")
617 self.status.pack(padx=2, pady=2)
619 def accept_all_entries(self):
620 # Validate the entry_boxes to "accept" the current values
621 # Note that this is not doing any validation as such (nothing really to validate)
622 self.url.validate()
623 self.port.validate()
624 self.network.validate()
625 self.node.validate()
626 self.username.validate()
627 self.password.validate()
629 def test_connectivity(self):
630 # Validate the entry_boxes to "accept" the current values and focus onto the button
631 self. accept_all_entries()
632 self.B1.focus()
633 # Save the existing settings (as they haven't been "applied" yet)
634 current_settings = (settings.get_mqtt())
635 # Apply the current settings (as they currently appear in the UI)
636 url = self.url.get_value()
637 port = self.port.get_value()
638 network = self.network.get_value()
639 node = self.node.get_value()
640 username = self.username.get_value()
641 password = self.password.get_value()
642 debug = self.debug.get_value()
643 startup = self.startup.get_value()
644 settings.set_mqtt(url=url, port=port, network=network, node=node,
645 username=username, password=password, debug=debug, startup=startup)
646 # The MQTT Connect function will return True if successful
647 # It will also update the Menubar to reflect the MQTT connection status
648 if self.connect_function(show_popup=False):
649 self.status.config(text="MQTT successfully connected", fg="green")
650 else:
651 self.status.config(text="MQTT connection failure", fg="red")
652 # Now restore the existing settings (as they haven't been "applied" yet)
653 settings.set_mqtt(*current_settings)
655#------------------------------------------------------------------------------------
656# Base Class for a dynamic str_entry_box_grid.
657#------------------------------------------------------------------------------------
659class entry_box_grid():
660 def __init__(self, parent_frame, base_class, width:int, tool_tip:str, columns:int=5):
661 self.parent_frame = parent_frame
662 self.base_class = base_class
663 self.tool_tip = tool_tip
664 self.columns = columns
665 self.width = width
666 # Create a frame (with padding) in which to pack everything
667 self.frame = Tk.Frame(self.parent_frame)
668 self.frame.pack(side=Tk.LEFT,padx=2,pady=2)
670 def create_row(self, pack_after=None):
671 # Create the Frame for the row
672 self.list_of_subframes.append(Tk.Frame(self.frame))
673 self.list_of_subframes[-1].pack(after=pack_after, padx=2, fill='x')
674 # Create the entry_boxes for the row
675 for value in range (self.columns):
676 self.list_of_entry_boxes.append(self.base_class(self.list_of_subframes[-1],
677 width=self.width, tool_tip=self.tool_tip))
678 self.list_of_entry_boxes[-1].pack(side=Tk.LEFT)
679 # Only set the value if we haven't reached the end of the values_to_setlist
680 if len(self.list_of_entry_boxes) <= len(self.values_to_set):
681 self.list_of_entry_boxes[-1].set_value(self.values_to_set[len(self.list_of_entry_boxes)-1])
682 # Create the button for inserting rows
683 this_subframe = self.list_of_subframes[-1]
684 self.list_of_buttons.append(Tk.Button(self.list_of_subframes[-1], text="+", height= 1, width=1,
685 padx=2, pady=0, font=('Courier',8,"normal"), command=lambda:self.create_row(this_subframe)))
686 self.list_of_buttons[-1].pack(side=Tk.LEFT, padx=5)
687 common.CreateToolTip(self.list_of_buttons[-1], "Insert new row (below)")
688 # Create the button for deleting rows (apart from the first row)
689 if len(self.list_of_subframes)>1:
690 self.list_of_buttons.append(Tk.Button(self.list_of_subframes[-1], text="-", height= 1, width=1,
691 padx=2, pady=0, font=('Courier',8,"normal"), command=lambda:self.delete_row(this_subframe)))
692 self.list_of_buttons[-1].pack(side=Tk.LEFT)
693 common.CreateToolTip(self.list_of_buttons[-1], "Delete row")
695 def delete_row(self, this_subframe):
696 this_subframe.destroy()
698 def set_values(self, values_to_set:list):
699 # Destroy and re-create the parent frame - this should also destroy all child widgets
700 self.frame.destroy()
701 self.frame = Tk.Frame(self.parent_frame)
702 self.frame.pack(side=Tk.LEFT,padx=2,pady=2)
703 self.list_of_subframes = []
704 self.list_of_entry_boxes = []
705 self.list_of_buttons = []
706 # Ensure at least one row is created - even if the list of values_to_set is empty
707 self.values_to_set = values_to_set
708 while len(self.list_of_entry_boxes) < len(values_to_set) or self.list_of_subframes == []:
709 self.create_row()
711 def get_values(self):
712 # Validate all the entries to accept the current (as entered) values
713 self.validate()
714 return_values = []
715 for entry_box in self.list_of_entry_boxes:
716 if entry_box.winfo_exists():
717 # Ignore all default entries - we need to handle int and str entry boxes types
718 if ( (type(entry_box.get_value())==str and entry_box.get_value() != "" ) or
719 (type(entry_box.get_value())==int and entry_box.get_value() != 0) ):
720 return_values.append(entry_box.get_value())
721 return(return_values)
723 def validate(self):
724 valid = True
725 for entry_box in self.list_of_entry_boxes:
726 if entry_box.winfo_exists():
727 if not entry_box.validate(): valid = False
728 return(valid)
730#------------------------------------------------------------------------------------
731# Class for the MQTT Configuration 'Subscribe' Tab
732#------------------------------------------------------------------------------------
734class mqtt_subscribe_tab():
735 def __init__(self, parent_tab):
736 # Create the Serial Port and baud rate UI elements
737 self.frame1 = Tk.LabelFrame(parent_tab, text="DCC command feed")
738 self.frame1.pack(padx=2, pady=2, fill='x')
739 self.dcc = entry_box_grid(self.frame1, base_class=common.entry_box, columns=4, width=8,
740 tool_tip="Specify the remote network nodes to take a DCC command feed from")
741 self.frame2 = Tk.LabelFrame(parent_tab, text="Signals")
742 self.frame2.pack(padx=2, pady=2, fill='x')
743 self.signals = entry_box_grid(self.frame2, base_class=common.str_item_id_entry_box, columns=4, width=8,
744 tool_tip="Enter the IDs of the remote signals to subscribe to (in the form 'node-ID')")
745 self.frame3 = Tk.LabelFrame(parent_tab, text="Track sections")
746 self.frame3.pack(padx=2, pady=2, fill='x')
747 self.sections = entry_box_grid(self.frame3, base_class=common.str_item_id_entry_box, columns=4, width=8,
748 tool_tip="Enter the IDs of the remote track sections to subscribe to (in the form 'node-ID')")
749 self.frame4 = Tk.LabelFrame(parent_tab, text="Block instruments")
750 self.frame4.pack(padx=2, pady=2, fill='x')
751 self.instruments = entry_box_grid(self.frame4, base_class=common.str_item_id_entry_box, columns=4, width=8,
752 tool_tip="Enter the IDs of the remote block instruments to subscribe to (in the form 'node-ID')")
753 self.frame5 = Tk.LabelFrame(parent_tab, text="Track sensors")
754 self.frame5.pack(padx=2, pady=2, fill='x')
755 self.sensors = entry_box_grid(self.frame5, base_class=common.str_item_id_entry_box, columns=4, width=8,
756 tool_tip="Enter the IDs of the remote track sensors (GPIO ports) to subscribe to (in the form 'node-ID')")
758 def validate(self):
759 return (self.dcc.validate() and self.signals.validate() and self.sections.validate()
760 and self.instruments.validate() and self.sensors.validate())
762#------------------------------------------------------------------------------------
763# Class for the MQTT Configuration 'Publish' Tab
764#------------------------------------------------------------------------------------
766class mqtt_publish_tab():
767 def __init__(self, parent_tab):
768 # Create the Serial Port and baud rate UI elements
769 self.frame1 = Tk.LabelFrame(parent_tab, text="DCC command feed")
770 self.frame1.pack(padx=2, pady=2, fill='x')
771 self.dcc = common.check_box(self.frame1, label="Publish DCC command feed",
772 tool_tip="Select to publish all DCC commands from this node via the "+
773 "MQTT Network (so the feed can be picked up by the node hosting "+
774 "the Pi-SPROG DCC interface) and sent out to the layout")
775 self.dcc.pack(padx=2, pady=2)
776 self.frame2 = Tk.LabelFrame(parent_tab, text="Signals")
777 self.frame2.pack(padx=2, pady=2, fill='x')
778 self.signals = entry_box_grid(self.frame2, base_class=common.int_item_id_entry_box, columns=9, width=3,
779 tool_tip="Enter the IDs of the signals (on the local schematic) to publish via the MQTT network")
780 self.frame3 = Tk.LabelFrame(parent_tab, text="Track sections")
781 self.frame3.pack(padx=2, pady=2, fill='x')
782 self.sections = entry_box_grid(self.frame3, base_class=common.int_item_id_entry_box, columns=9, width=3,
783 tool_tip="Enter the IDs of the track sections (on the local schematic) to publish via the MQTT network")
784 self.frame4 = Tk.LabelFrame(parent_tab, text="Block instruments")
785 self.frame4.pack(padx=2, pady=2, fill='x')
786 self.instruments = entry_box_grid(self.frame4, base_class=common.int_item_id_entry_box, columns=9, width=3,
787 tool_tip="Enter the IDs of the block instruments (on the local schematic) to publish via the MQTT network")
788 self.frame5 = Tk.LabelFrame(parent_tab, text="Track sensors")
789 self.frame5.pack(padx=2, pady=2, fill='x')
790 self.sensors = entry_box_grid(self.frame5, base_class=common.int_item_id_entry_box, columns=9, width=3,
791 tool_tip="Enter the IDs of the track sensors (GPIO port) to publish via the MQTT network")
793 def validate(self):
794 return (self.signals.validate() and self.sections.validate()
795 and self.instruments.validate() and self.sensors.validate())
798#------------------------------------------------------------------------------------
799# Class for the MQTT Configuration 'status' Tab showing a list of connected nodes
800#------------------------------------------------------------------------------------
802class mqtt_status_tab():
803 def __init__(self, parent_tab):
804 # Create the list of connected nodes
805 self.frame1 = Tk.LabelFrame(parent_tab, text="Node Status")
806 self.frame1.pack(padx=2, pady=2, fill='x')
807 self.frame2 = None
808 self.button = Tk.Button(parent_tab, text="Refresh display", command=self.refresh)
809 self.button.pack(padx=2, pady=2,)
810 self.refresh()
812 def refresh(self):
813 # Get the list of currently connected nodes
814 node_status = mqtt_interface.get_node_status()
815 # Destroy the current frame (with all the entries) and re-create
816 if self.frame2 is not None: self.frame2.destroy()
817 self.frame2 = Tk.Frame(self.frame1)
818 self.frame2.pack()
819 # Populate the list of all nodes seen since application start
820 for node_id in node_status.keys():
821 subframe = Tk.Frame(self.frame2)
822 subframe.pack(padx=2, pady=2, fill='x')
823 # User defined Node identifier
824 node = Tk.Label(subframe,text=node_id)
825 node.pack(side=Tk.LEFT)
826 # Ip address (received in the heartbeat message)
827 ip_address = node_status[node_id][0]
828 label1 = Tk.Label(subframe, text=" - ip:")
829 label1.pack(side=Tk.LEFT)
830 ip_add = Tk.Label(subframe, text=ip_address)
831 ip_add.pack(side=Tk.LEFT)
832 # Timestamp (when the last heartbeat message was received)
833 time_stamp = node_status[node_id][1]
834 time_to_display = datetime.datetime.fromtimestamp(time_stamp).strftime('%H:%M:%S')
835 label2 = Tk.Label(subframe, text="- Last seen: ")
836 label2.pack(side=Tk.LEFT)
837 time_to_display = datetime.datetime.fromtimestamp(time_stamp).strftime('%H:%M:%S')
838 last_time = Tk.Label(subframe, text=time_to_display)
839 last_time.pack(side=Tk.LEFT)
840 # Set the colour of the timestamp according to how long ago it was
841 if time.time() - time_stamp > 10: last_time.config(fg="red")
842 else: last_time.config(fg="green")
843 if node_status == {}:
844 label = Tk.Label(self.frame2, text="No nodes seen since application start")
845 label.pack(side=Tk.LEFT)
847#------------------------------------------------------------------------------------
848# Class for the MQTT Settings window (uses the classes above for each tab). Note that init
849# takes in callbacks for connecting to the broker and for applying the updated settings.
850# Note also that if a window is already open then we just raise it and exit.
851#------------------------------------------------------------------------------------
853edit_mqtt_settings_window = None
855class edit_mqtt_settings():
856 def __init__(self, root_window, connect_function, update_function):
857 global edit_mqtt_settings_window
858 # If there is already a window open then we just make it jump to the top and exit
859 if edit_mqtt_settings_window is not None:
860 edit_mqtt_settings_window.lift()
861 edit_mqtt_settings_window.state('normal')
862 edit_mqtt_settings_window.focus_force()
863 else:
864 self.connect_function = connect_function
865 self.update_function = update_function
866 # Create the top level window for editing MQTT settings
867 self.window = Tk.Toplevel(root_window)
868 self.window.title("MQTT Networking")
869 self.window.protocol("WM_DELETE_WINDOW", self.close_window)
870 self.window.resizable(False, False)
871 edit_mqtt_settings_window = self.window
872 # Create the common Apply/OK/Reset/Cancel buttons for the window (packed first to remain visible)
873 self.controls = common.window_controls(self.window, self.load_state, self.save_state, self.close_window)
874 self.controls.frame.pack(side=Tk.BOTTOM, padx=2, pady=2)
875 # Create the Validation error message (this gets packed/unpacked on apply/save)
876 self.validation_error = Tk.Label(self.window, text="Errors on Form need correcting", fg="red")
877 # Create the Notebook (for the tabs)
878 self.tabs = ttk.Notebook(self.window)
879 # Create the Window tabs
880 self.tab1 = Tk.Frame(self.tabs)
881 self.tabs.add(self.tab1, text="Network")
882 self.tab2 = Tk.Frame(self.tabs)
883 self.tabs.add(self.tab2, text="Subscribe")
884 self.tab3 = Tk.Frame(self.tabs)
885 self.tabs.add(self.tab3, text="Publish")
886 self.tab4 = Tk.Frame(self.tabs)
887 self.tabs.add(self.tab4, text="Status")
888 self.tabs.pack()
889 # Create the tabs themselves:
890 self.config = mqtt_configuration_tab(self.tab1, self.connect_function)
891 self.subscribe = mqtt_subscribe_tab(self.tab2)
892 self.publish = mqtt_publish_tab(self.tab3)
893 self.status = mqtt_status_tab(self.tab4)
894 # Load the initial UI state
895 self.load_state()
897 def load_state(self):
898 # Hide the validation error and connection test messages
899 self.config.status.config(text="")
900 self.validation_error.pack_forget()
901 # Populate the network configuration tab
902 url, port, network, node, username, password, debug, startup, pubshut, subshut = settings.get_mqtt()
903 self.config.url.set_value(url)
904 self.config.port.set_value(port)
905 self.config.network.set_value(network)
906 self.config.node.set_value(node)
907 self.config.username.set_value(username)
908 self.config.password.set_value(password)
909 self.config.debug.set_value(debug)
910 self.config.startup.set_value(startup)
911 self.config.pubshutdown.set_value(pubshut)
912 self.config.subshutdown.set_value(subshut)
913 # Populate the subscribe tab
914 self.subscribe.dcc.set_values(settings.get_sub_dcc_nodes())
915 self.subscribe.signals.set_values(settings.get_sub_signals())
916 self.subscribe.sections.set_values(settings.get_sub_sections())
917 self.subscribe.instruments.set_values(settings.get_sub_instruments())
918 self.subscribe.sensors.set_values(settings.get_sub_sensors())
919 # Populate the publish tab
920 self.publish.dcc.set_value(settings.get_pub_dcc())
921 self.publish.signals.set_values(settings.get_pub_signals())
922 self.publish.sections.set_values(settings.get_pub_sections())
923 self.publish.instruments.set_values(settings.get_pub_instruments())
924 self.publish.sensors.set_values(settings.get_pub_sensors())
926 def save_state(self, close_window:bool):
927 # Validate the entries to "accept" the current values before reading
928 self.config.accept_all_entries()
929 # Only allow close if valid
930 if self.subscribe.validate() and self.publish.validate():
931 self.validation_error.pack_forget()
932 url = self.config.url.get_value()
933 port = self.config.port.get_value()
934 network = self.config.network.get_value()
935 node = self.config.node.get_value()
936 username = self.config.username.get_value()
937 password = self.config.password.get_value()
938 debug = self.config.debug.get_value()
939 startup = self.config.startup.get_value()
940 pubshut = self.config.pubshutdown.get_value()
941 subshut = self.config.subshutdown.get_value()
942 # Save the updated settings
943 settings.set_mqtt(url=url, port=port, network=network, node=node,
944 username=username, password=password, debug=debug, startup=startup,
945 publish_shutdown=pubshut, subscribe_shutdown=subshut)
946 # Save the Subscribe settings
947 settings.set_sub_dcc_nodes(self.subscribe.dcc.get_values())
948 settings.set_sub_signals(self.subscribe.signals.get_values())
949 settings.set_sub_sections(self.subscribe.sections.get_values())
950 settings.set_sub_instruments(self.subscribe.instruments.get_values())
951 settings.set_sub_sensors(self.subscribe.sensors.get_values())
952 # Save the publish settings
953 settings.set_pub_dcc(self.publish.dcc.get_value())
954 settings.set_pub_signals(self.publish.signals.get_values())
955 settings.set_pub_sections(self.publish.sections.get_values())
956 settings.set_pub_instruments(self.publish.instruments.get_values())
957 settings.set_pub_sensors(self.publish.sensors.get_values())
958 # Make the callback to apply the updated settings
959 self.update_function()
960 # close the window (on OK)
961 if close_window: self.close_window()
962 else:
963 # Display the validation error message
964 self.validation_error.pack(side=Tk.BOTTOM, before=self.controls.frame)
966 def close_window(self):
967 global edit_mqtt_settings_window
968 edit_mqtt_settings_window = None
969 self.window.destroy()
971#------------------------------------------------------------------------------------
972# Classes for the GPIO (Track Sensors) configuration window
973#------------------------------------------------------------------------------------
975class gpio_port_entry_box(common.int_item_id_entry_box):
976 def __init__(self, parent_frame, label:str, tool_tip:str, callback):
977 # create a frame to hold the label and entry box
978 self.frame = Tk.Frame(parent_frame)
979 self.frame.pack()
980 # Create the label and call the parent init function to create the EB
981 self.label = Tk.Label(self.frame, width=8, text=label)
982 self.label.pack(side=Tk.LEFT)
983 super().__init__(self.frame, tool_tip=tool_tip, callback=callback)
984 super().pack(side=Tk.LEFT)
985 # Create the Signal/Track sensor 'mapping' label
986 self.mapping = Tk.Label(self.frame, width=16, anchor='w')
987 self.mapping.pack(side=Tk.LEFT,padx=5)
989class gpio_port_entry_frame():
990 def __init__(self, parent_frame):
991 # Create the Label frame for the GPIO port assignments
992 self.frame = Tk.LabelFrame(parent_frame, text="GPIO port to GPIO Sensor mappings")
993 self.frame.pack(padx=2, pady=2, fill='x')
994 self.list_of_subframes = []
995 self.list_of_entry_boxes = []
996 self.list_of_available_gpio_ports = gpio_sensors.get_list_of_available_ports()
997 while len(self.list_of_entry_boxes) < len(self.list_of_available_gpio_ports):
998 # Create the Frame for the row
999 self.list_of_subframes.append(Tk.Frame(self.frame))
1000 self.list_of_subframes[-1].pack(side=Tk.LEFT, padx=2, fill='x')
1001 # Create the entry_boxes for the row
1002 for value in range (10):
1003 if len(self.list_of_entry_boxes) == len(self.list_of_available_gpio_ports): break
1004 label = "GPIO-"+str(self.list_of_available_gpio_ports[len(self.list_of_entry_boxes)])
1005 tool_tip = "Enter a GPIO Sensor ID to be associated with this GPIO port (or leave blank)"
1006 self.list_of_entry_boxes.append(gpio_port_entry_box(self.list_of_subframes[-1],
1007 label=label, tool_tip=tool_tip, callback=self.validate))
1009 def validate(self):
1010 valid = True
1011 # First do the basic validation on all entry boxes - we do this every time to
1012 # clear any duplicate entry validation errors that may now have been corrected
1013 for entry_box in self.list_of_entry_boxes:
1014 if not entry_box.validate(): valid = False
1015 # Then check for duplicate entries
1016 for entry_box1 in self.list_of_entry_boxes:
1017 value1 = entry_box1.get_value()
1018 for entry_box2 in self.list_of_entry_boxes:
1019 if entry_box1 != entry_box2 and value1 == entry_box2.get_value() and value1 != 0:
1020 entry_box1.TT.text = ("Duplicate ID - sensor is already assigned to another GPIO port")
1021 entry_box1.set_validation_status(False)
1022 valid = False
1023 return (valid)
1025 def get_values(self):
1026 list_of_mappings = []
1027 for index, gpio_port in enumerate(self.list_of_available_gpio_ports):
1028 sensor_id = self.list_of_entry_boxes[index].get_value()
1029 if sensor_id > 0: list_of_mappings.append([sensor_id, gpio_port])
1030 return (list_of_mappings)
1032 def set_values(self,list_of_mappings:[[int,int],]):
1033 # Clear down all entry boxes first before re-populating as we only
1034 # populate those where a mapping has been defined
1035 for index, gpio_port in enumerate(self.list_of_available_gpio_ports):
1036 self.list_of_entry_boxes[index].set_value(None)
1037 # Mappings is a variable length list of sensor to gpio mappings [sensor,gpio]
1038 for index, gpio_port in enumerate(self.list_of_available_gpio_ports):
1039 self.list_of_entry_boxes[index].mapping.config(text="-------------------------")
1040 for gpio_mapping in list_of_mappings:
1041 if gpio_port == gpio_mapping[1]:
1042 event_mappings = gpio_sensors.get_gpio_sensor_callback(gpio_mapping[0])
1043 if event_mappings[0] > 0: mapping_text = u"\u2192"+" Signal "+str(event_mappings[0])
1044 elif event_mappings[1] > 0: mapping_text = u"\u2192"+" Signal "+str(event_mappings[1])
1045 elif event_mappings[2] > 0: mapping_text = u"\u2192"+" Track Sensor "+str(event_mappings[2])
1046 else: mapping_text="-------------------------"
1047 self.list_of_entry_boxes[index].set_value(gpio_mapping[0])
1048 self.list_of_entry_boxes[index].mapping.config(text=mapping_text)
1050#------------------------------------------------------------------------------------
1051# Class for the "Sensors" window - Uses the classes above. Note the init function takes
1052# in a callback so it can apply the updated settings in the main editor application.
1053# Note also that if a window is already open then we just raise it and exit.
1054#------------------------------------------------------------------------------------
1056edit_gpio_settings_window = None
1058class edit_gpio_settings():
1059 def __init__(self, root_window, update_function):
1060 global edit_gpio_settings_window
1061 # If there is already a window open then we just make it jump to the top and exit
1062 if edit_gpio_settings_window is not None:
1063 edit_gpio_settings_window.lift()
1064 edit_gpio_settings_window.state('normal')
1065 edit_gpio_settings_window.focus_force()
1066 else:
1067 self.update_function = update_function
1068 # Create the (non resizable) top level window for editing MQTT settings
1069 self.window = Tk.Toplevel(root_window)
1070 self.window.title("GPIO Sensors")
1071 self.window.protocol("WM_DELETE_WINDOW", self.close_window)
1072 self.window.resizable(False, False)
1073 edit_gpio_settings_window = self.window
1074 # Create an overall frame to pack everything in
1075 self.frame = Tk.Frame(self.window)
1076 self.frame.pack()
1077 # Create the labelframe for the general GPIO settings
1078 self.subframe1 = Tk.LabelFrame(self.frame, text="GPIO Port Settings")
1079 self.subframe1.pack(padx=2, pady=2, fill='x')
1080 # Put the elements in a subframe to center them
1081 self.subframe2 = Tk.Frame(self.subframe1)
1082 self.subframe2.pack()
1083 self.label1 = Tk.Label(self.subframe2, text="Delay (ms):")
1084 self.label1.pack(side=Tk.LEFT, padx=2, pady=2, fill='x')
1085 self.trigger = common.integer_entry_box(self.subframe2, width=5, min_value=0, max_value=1000, allow_empty=False,
1086 tool_tip="Enter the delay period (before GPIO sensor events will be triggered) in milliseconds (0-1000)")
1087 self.trigger.pack(side=Tk.LEFT, padx=2, pady=2, fill='x')
1088 self.label2 = Tk.Label(self.subframe2, text="Timeout (ms):")
1089 self.label2.pack(side=Tk.LEFT, padx=2, pady=2, fill='x')
1090 self.timeout = common.integer_entry_box(self.subframe2, width=5, min_value=0, max_value=5000, allow_empty=False,
1091 tool_tip="Enter the timeout period (during which further triggers will be ignored) in milliseconds (0-5000)")
1092 self.timeout.pack(side=Tk.LEFT, padx=2, pady=2, fill='x')
1093 # Create the Label frame for the GPIO port assignments
1094 self.gpio = gpio_port_entry_frame(self.frame)
1095 # Create the common Apply/OK/Reset/Cancel buttons for the window
1096 self.controls = common.window_controls(self.window, self.load_state, self.save_state, self.close_window)
1097 self.controls.frame.pack(side=Tk.BOTTOM, padx=2, pady=2)
1098 # Create the Validation error message (this gets packed/unpacked on apply/save)
1099 self.validation_error = Tk.Label(self.window, text="Errors on Form need correcting", fg="red")
1100 # Load the initial UI state
1101 self.load_state()
1103 def load_state(self):
1104 self.validation_error.pack_forget()
1105 trigger, timeout, mappings = settings.get_gpio()
1106 self.gpio.set_values(mappings)
1107 self.trigger.set_value(int(trigger*1000))
1108 self.timeout.set_value(int(timeout*1000))
1110 def save_state(self, close_window:bool):
1111 # Only allow close if valid
1112 if self.gpio.validate() and self.trigger.validate() and self.timeout.validate():
1113 self.validation_error.pack_forget()
1114 mappings = self.gpio.get_values()
1115 trigger = float(self.trigger.get_value())/1000
1116 timeout = float(self.timeout.get_value())/1000
1117 settings.set_gpio(trigger, timeout, mappings)
1118 # Make the callback to apply the updated settings
1119 self.update_function()
1120 # Close the window (on OK) or refresh the display (on APPLY)
1121 if close_window: self.close_window()
1122 else: self.load_state()
1123 else:
1124 # Display the validation error message
1125 self.validation_error.pack(side=Tk.BOTTOM, before=self.controls.frame)
1127 def close_window(self):
1128 global edit_gpio_settings_window
1129 edit_gpio_settings_window = None
1130 self.window.destroy()
1132#------------------------------------------------------------------------------------
1133# Class for the General Settings toolbar window. Note the init function takes
1134# in a callback so it can apply the updated settings in the main editor application.
1135# Note also that if a window is already open then we just raise it and exit.
1136#------------------------------------------------------------------------------------
1138edit_general_settings_window = None
1140class edit_general_settings():
1141 def __init__(self, root_window, update_function):
1142 global edit_general_settings_window
1143 # If there is already a window open then we just make it jump to the top and exit
1144 if edit_general_settings_window is not None:
1145 edit_general_settings_window.lift()
1146 edit_general_settings_window.state('normal')
1147 edit_general_settings_window.focus_force()
1148 else:
1149 self.update_function = update_function
1150 # Create the (non resizable) top level window for the General Settings
1151 self.window = Tk.Toplevel(root_window)
1152 self.window.title("General")
1153 self.window.protocol("WM_DELETE_WINDOW", self.close_window)
1154 self.window.resizable(False, False)
1155 edit_general_settings_window = self.window
1156 # Create the "SPAD Popups" selection element
1157 self.spad = common.check_box(self.window, label="Enable Signal Passed at Danger popup warnings",
1158 tool_tip="Select to Enable popup Signal Passed at Danger (SPAD) and other track occupancy warnings")
1159 self.spad.pack(padx=2, pady=2)
1160 # Create the common Apply/OK/Reset/Cancel buttons for the window
1161 self.controls = common.window_controls(self.window, self.load_state, self.save_state, self.close_window)
1162 self.controls.frame.pack(padx=2, pady=2)
1163 # Load the initial UI state
1164 self.load_state()
1166 def load_state(self):
1167 # Spad Popups flag is the 6th parameter returned from get_general
1168 self.spad.set_value(settings.get_general()[5])
1170 def save_state(self, close_window:bool):
1171 settings.set_general(spad=self.spad.get_value())
1172 # Make the callback to apply the updated settings
1173 self.update_function()
1174 # close the window (on OK )
1175 if close_window: self.close_window()
1177 def close_window(self):
1178 global edit_general_settings_window
1179 edit_general_settings_window = None
1180 self.window.destroy()
1182#############################################################################################