Coverage for /home/pi/Software/model-railway-signalling/model_railway_signals/library/common.py: 85%
59 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-05 16:40 +0100
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-05 16:40 +0100
1# -------------------------------------------------------------------------
2# This module contains all of the parameters, funcions and classes that
3# are used across multiple modules in the model_railway_signalling library
4#--------------------------------------------------------------------------
5#
6# External API - classes and functions (used by the Schematic Editor):
7#
8# set_root_window(root) - initialise the library with the root window reference
9#
10# shutdown() - perform an orderly shutdown of the library functions (clean up)
11#
12# External API - classes and functions (used by the other library modules):
13#
14# execute_function_in_tkinter_thread(callback_function) - Will 'pass' the function
15# into the main tkinter thread via a queue (and tkinter event) and then execute
16# the function - used for MQTT and GPIO events to keep everything threadsafe.
17#
18# -------------------------------------------------------------------------
20import math
21import queue
22import logging
23import time
24from . import mqtt_interface
25from . import pi_sprog_interface
26from . import gpio_sensors
28# -------------------------------------------------------------------------
29# Global variables used within the Library Modules
30# -------------------------------------------------------------------------
32# Global variables for how the signals/points/sections buttons appear
33# on the screen. This is to allow the appearance to be optimised for
34# particular window sizes/screen resolutions.
35fontsize = 8 # Used by the Signals, Points and sections modules
36xpadding = 2 # Used by the Signals, Points and sections modules
37ypadding = -1 # Used by the Signals, Points and sections modules
38bgraised = "grey85" # Used by the Signals and Points modules
39bgsunken = "white" # Used by the Signals and Points modules
41# Global Variable to hold a reference to the TkInter Root Window
42root_window = None
43# Event queue for passing "commands" back into the main tkinter thread
44event_queue = queue.Queue()
45# Global variable to signal (to other modules) that application is closing
46shutdown_initiated = False
48#-------------------------------------------------------------------------
49# Function to perfor an orderly shutdown of the library modules:
50# MQTT Networking - clean up the published topics and disconnect
51# SPROG interface - switch off the DCC power and close the serial port
52# GPIO Sensors - revert all GPIO pins to their default states
53# Finally - wait for all scheduled TKinter events to complete
54#-------------------------------------------------------------------------
56def shutdown():
57 global shutdown_initiated
58 if not shutdown_initiated: 58 ↛ 81line 58 didn't jump to line 81, because the condition on line 58 was never false
59 logging.info ("Initiating Application Shutdown")
60 shutdown_initiated = True
61 # Clear out any retained messages and disconnect from broker
62 mqtt_interface.mqtt_shutdown()
63 # Turn off the DCC bus power and close the comms port
64 pi_sprog_interface.sprog_shutdown()
65 # Return the GPIO ports to their original configuration
66 gpio_sensors.gpio_shutdown()
67 # Wait until all the tasks we have scheduled via the tkinter 'after' method have completed
68 # We need to put a timeout around this to deal with any scheduled Tkinter "after" events
69 # (although its unlikely the user would initiate a shut down until these have finished)
70 timeout_start = time.time()
71 while time.time() < timeout_start + 30: 71 ↛ 78line 71 didn't jump to line 78, because the condition on line 71 was never false
72 if root_window.tk.call('after','info') != "": 72 ↛ 73line 72 didn't jump to line 73, because the condition on line 72 was never true
73 root_window.update()
74 time.sleep(0.01)
75 else:
76 logging.info ("Exiting Application")
77 break
78 if time.time() >= timeout_start + 30: 78 ↛ 79line 78 didn't jump to line 79, because the condition on line 78 was never true
79 logging.warning ("Timeout waiting for scheduled tkinter events to complete - Exiting anyway")
80 root_window.destroy()
81 return()
83#-------------------------------------------------------------------------
84# Function to set the tkinter "root" window reference as this is used to
85# schedule callback events in the main tkinter event loop using the 'after'
86# method and also for feeding custom callback functions into the main tkinter
87# thread. We do this as all the information out there on the internet concludes
88# tkinter isn't fully thread safe and so all manipulation of tkinter drawing
89# objects should be done from within the main tkinter thread.
90#-------------------------------------------------------------------------
92def set_root_window (root):
93 global root_window
94 root_window = root
95 # bind the tkinter event for handling events raised in external threads
96 root_window.bind("<<ExtCallback>>", handle_callback_in_tkinter_thread)
97 return(root_window)
99#-------------------------------------------------------------------------
100# Functions to allow custom callback functions to be passed in (from an external
101# thread) and then handled in the main Tkinter thread (to keep everything threadsafe).
102# We use the tkinter event_generate method to generate a custom event in the main
103# tkinter event loop in conjunction with a (threadsafe) queue to pass the callback function
104# Use as follows: execute_function_in_tkinter_thread (lambda: my_function(arg1,arg2...))
105#-------------------------------------------------------------------------
107def handle_callback_in_tkinter_thread(*args):
108 while not event_queue.empty() and not shutdown_initiated:
109 callback = event_queue.get(False)
110 callback()
111 return()
113def execute_function_in_tkinter_thread(callback_function):
114 if not shutdown_initiated: 114 ↛ 121line 114 didn't jump to line 121, because the condition on line 114 was never false
115 if root_window is not None: 115 ↛ 119line 115 didn't jump to line 119, because the condition on line 115 was never false
116 event_queue.put(callback_function)
117 root_window.event_generate("<<ExtCallback>>", when="tail")
118 else:
119 logging.error ("Execute_function_in_tkinter_thread - root undefined - executing in current thread")
120 callback_function()
121 return()
123# -------------------------------------------------------------------------
124# Common functions to rotate offset coordinates around an origin
125# The angle should be passed into these functions in degrees.
126# -------------------------------------------------------------------------
128def rotate_point(ox,oy,px,py,angle):
129 angle = math.radians(angle)
130 qx = ox + math.cos(angle) * (px) - math.sin(angle) * (py)
131 qy = oy + math.sin(angle) * (px) + math.cos(angle) * (py)
132 return (qx,qy)
134def rotate_line(ox,oy,px1,py1,px2,py2,angle):
135 start_point = rotate_point(ox,oy,px1,py1,angle)
136 end_point = rotate_point(ox,oy,px2,py2,angle)
137 return (start_point, end_point)
139##################################################################################################