Support uri argument and fix flake8 warnings master
authorPat Thoyts <pat.thoyts@gmail.com>
Tue, 4 Jul 2023 14:56:58 +0000 (15:56 +0100)
committerPat Thoyts <pat.thoyts@gmail.com>
Tue, 4 Jul 2023 14:59:49 +0000 (15:59 +0100)
Amended to expect the Monitor output tree.

.gitignore
spectrumviewer.py
tox.ini [new file with mode: 0644]
wdf-opc-monitor.py

index c18dd8d83ceed1806b50b0aaa46beb7e335fff13..de21d941aeb060422726aff1cd7440d9b791114d 100644 (file)
@@ -1 +1,2 @@
 __pycache__/
 __pycache__/
+.tox/
index c8e18d95477121c86bf865b142f606f709ffbb93..6ae7569611115af9c1518faac7e4a1d8cb117146 100644 (file)
@@ -1,24 +1,25 @@
 import tkinter as tk
 import tkinter.font as tkfont
 
 import tkinter as tk
 import tkinter.font as tkfont
 
+
 class SpectrumViewer(tk.Canvas):
     def __init__(self, master, *args, **kwargs):
         self.xlist = []
         self.ilist = []
 class SpectrumViewer(tk.Canvas):
     def __init__(self, master, *args, **kwargs):
         self.xlist = []
         self.ilist = []
-        self.font = tkfont.nametofont('TkDefaultFont', root=master)
+        self.font = tkfont.nametofont('TkDefaultFont')
         self.yaxis_space = 40
         self.xaxis_space = 10
         super(SpectrumViewer, self).__init__(master, *args, **kwargs)
         self.create_line((0, 0, 0, 0), tag="Spectrum", fill='red', width=1)
         self.create_line((0, 0, 0, 0), tag='XAxis', fill='black', width=1)
         self.create_line((0, 0, 0, 0), tag='YAxis', fill='black', width=1)
         self.yaxis_space = 40
         self.xaxis_space = 10
         super(SpectrumViewer, self).__init__(master, *args, **kwargs)
         self.create_line((0, 0, 0, 0), tag="Spectrum", fill='red', width=1)
         self.create_line((0, 0, 0, 0), tag='XAxis', fill='black', width=1)
         self.create_line((0, 0, 0, 0), tag='YAxis', fill='black', width=1)
-        self.create_text((0,0), tag='XMin', anchor=tk.NW, font=self.font)
-        self.create_text((0,0), tag='XMax', anchor=tk.NE, font=self.font)
-        self.create_text((0,0), tag='YMin', anchor=tk.SE, font=self.font)
-        self.create_text((0,0), tag='YMax', anchor=tk.NE, font=self.font)
+        self.create_text((0, 0), tag='XMin', anchor=tk.NW, font=self.font)
+        self.create_text((0, 0), tag='XMax', anchor=tk.NE, font=self.font)
+        self.create_text((0, 0), tag='YMin', anchor=tk.SE, font=self.font)
+        self.create_text((0, 0), tag='YMax', anchor=tk.NE, font=self.font)
         self.create_text((0, 0), tag='Title', anchor=tk.CENTER, font=self.font)
         self.bind('<Configure>', self.on_resize)
         self.create_text((0, 0), tag='Title', anchor=tk.CENTER, font=self.font)
         self.bind('<Configure>', self.on_resize)
-    
+
     def title(self, text):
         self.itemconfigure('Title', text=text)
 
     def title(self, text):
         self.itemconfigure('Title', text=text)
 
@@ -28,7 +29,7 @@ class SpectrumViewer(tk.Canvas):
 
     def adjust_axes(self):
         """Resize the axes space for the current font."""
 
     def adjust_axes(self):
         """Resize the axes space for the current font."""
-        width = self.font.measure('0'*6)
+        width = self.font.measure('0' * 6)
         height = self.font.metrics()['linespace']
         self.yaxis_space = width
         self.xaxis_space = height
         height = self.font.metrics()['linespace']
         self.yaxis_space = width
         self.xaxis_space = height
@@ -63,12 +64,13 @@ class SpectrumViewer(tk.Canvas):
             self.itemconfigure('YMax', text=f"{ymax:.0f}")
         if resized:
             y = height - self.xaxis_space
             self.itemconfigure('YMax', text=f"{ymax:.0f}")
         if resized:
             y = height - self.xaxis_space
-            coords = [self.yaxis_space, y, width+self.yaxis_space, y]
+            coords = [self.yaxis_space, y, width + self.yaxis_space, y]
             self.coords('XAxis', coords)
             self.coords('XAxis', coords)
-            coords = [self.yaxis_space, 0, self.yaxis_space, height-self.xaxis_space]
+            coords = [self.yaxis_space, 0,
+                      self.yaxis_space, height-self.xaxis_space]
             self.coords('YAxis', coords)
             self.coords('YAxis', coords)
-            self.coords('XMin', [self.yaxis_space, height-self.xaxis_space])
-            self.coords('XMax', [width+self.yaxis_space, height-self.xaxis_space])
-            self.coords('YMin', [self.yaxis_space, height-self.xaxis_space])
+            self.coords('XMin', [self.yaxis_space, height - self.xaxis_space])
+            self.coords('XMax', [width + self.yaxis_space, height - self.xaxis_space])
+            self.coords('YMin', [self.yaxis_space, height - self.xaxis_space])
             self.coords('YMax', [self.yaxis_space, 0])
             self.coords('YMax', [self.yaxis_space, 0])
-            self.coords('Title', [width/2, 10])
+            self.coords('Title', [width / 2, 10])
diff --git a/tox.ini b/tox.ini
new file mode 100644 (file)
index 0000000..1c71622
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,20 @@
+[tox]
+envlist = py310, py38, lint
+skip_missing_interpreters = True
+
+[testenv]
+setenv =
+    PYTHONPATH = {toxinidir}
+deps =
+    pytest
+commands =
+    py.test
+
+[testenv:lint]
+deps =
+     flake8
+commands =
+    flake8
+
+[flake8]
+max-line-length = 120
index 061c42c0306f0dce7a768066027d191839c2d378..a8e3a30b5a8b8922b03bb583921b2b94f989781f 100755 (executable)
@@ -1,16 +1,22 @@
 #!/usr/bin/env python3
 
 import sys
 #!/usr/bin/env python3
 
 import sys
+import argparse
 import tkinter as tk
 import tkinter.ttk as ttk
 import tkinter.messagebox as tkmessagebox
 import asyncio
 import threading
 import logging
 import tkinter as tk
 import tkinter.ttk as ttk
 import tkinter.messagebox as tkmessagebox
 import asyncio
 import threading
 import logging
-from asyncua import Client, Node, ua
-from queue import Queue, Empty
+from asyncua import Client, Node
+from queue import Queue
 from spectrumviewer import SpectrumViewer
 
 from spectrumviewer import SpectrumViewer
 
+if sys.platform == 'win32':
+    # Fix for tiny menus on high dpi display
+    from ctypes import windll
+    windll.shcore.SetProcessDpiAwareness(1)
+
 APP_TITLE = "OPC Monitor"
 
 logging.basicConfig(level=logging.WARNING)
 APP_TITLE = "OPC Monitor"
 
 logging.basicConfig(level=logging.WARNING)
@@ -24,15 +30,17 @@ class SubscriptionHandler:
     def __init__(self, app):
         self.app = app
         self.reverse = False
     def __init__(self, app):
         self.app = app
         self.reverse = False
-    
+
     def datachange_notification(self, node: Node, val, data):
         """
         Callback for asyncua Subscription.
         This method will be called when the Client received a data change message from the Server.
         """
         _logger.info('datachange_notification %r', node)
     def datachange_notification(self, node: Node, val, data):
         """
         Callback for asyncua Subscription.
         This method will be called when the Client received a data change message from the Server.
         """
         _logger.info('datachange_notification %r', node)
-        _,id = str(node).split(';')
+        _, id = str(node).split(';')
         id = id.split('.')[-1]
         id = id.split('.')[-1]
+        if id.startswith('s='):
+            id = id.split('=')[-1].lower()
         if id == 'xlist':
             if val[0] > val[1]:
                 val.reverse()
         if id == 'xlist':
             if val[0] > val[1]:
                 val.reverse()
@@ -41,65 +49,78 @@ class SubscriptionHandler:
                 self.reverse = False
         if id == 'ilist' and self.reverse:
             val.reverse()
                 self.reverse = False
         if id == 'ilist' and self.reverse:
             val.reverse()
-        if id in ('xlist', 'ilist', 'index'):
+        if id in ('xlist', 'ilist', 'index', 'time'):
             self.app.queue.put((id, val))
             self.app.event_generate('<<OpcDataReceived>>')
 
 
 class OpcClient:
             self.app.queue.put((id, val))
             self.app.event_generate('<<OpcDataReceived>>')
 
 
 class OpcClient:
-    def __init__(self, app):
-        self.thread = threading.Thread(target=self.thread_main, args=(self, app))
-        self.thread.daemon = True
-        self.event = asyncio.Event()
+    def __init__(self, app, uri):
+        self.uri = uri
+        self.thread = None
+        self.event = None
         self.app = app
 
     def start(self):
         self.app = app
 
     def start(self):
-        self.event.clear()
-        self.thread.start()
-    
+        if self.event:
+            self.event.clear()
+        if not self.thread:
+            self.thread = threading.Thread(target=self.thread_main, args=(app,))
+            self.thread.daemon = True
+            self.thread.start()
+
     def stop(self):
     def stop(self):
-        self.event.set()
+        if self.event:
+            self.event.set()
 
 
-    def thread_main(self, self2, app):
-        print(f"thread_main: {self} {self2} {app}")
+    def thread_main(self, app):
         self.handler = SubscriptionHandler(app)
         asyncio.run(self.main())
         self.handler = SubscriptionHandler(app)
         asyncio.run(self.main())
+        self.thread = None
+        self.event = None
 
     async def main(self):
 
     async def main(self):
-        client = Client(url='opc.tcp://localhost:4840/')
+        if not self.event:
+            self.event = asyncio.Event()  # ensure this gets attached to THIS event loop.
+        client = Client(url=self.uri)
         async with client:
             idx = await client.get_namespace_index(uri="urn:renishaw.spd")
         async with client:
             idx = await client.get_namespace_index(uri="urn:renishaw.spd")
-            xlist_node = await client.nodes.objects.get_child([f"{idx}:Renishaw", f"{idx}:SPD", f"{idx}:Spectrum", f"{idx}:xlist"])
-            ilist_node = await client.nodes.objects.get_child([f"{idx}:Renishaw", f"{idx}:SPD", f"{idx}:Spectrum", f"{idx}:ilist"])
-            index_node = await client.nodes.objects.get_child([f"{idx}:Renishaw", f"{idx}:SPD", f"{idx}:Spectrum", f"{idx}:index"])
+            xlist_node = await client.nodes.objects.get_child([f"{idx}:Renishaw", f"{idx}:Spectrum", f"{idx}:XList"])
+            ilist_node = await client.nodes.objects.get_child([f"{idx}:Renishaw", f"{idx}:Spectrum", f"{idx}:IList"])
+            index_node = await client.nodes.objects.get_child([f"{idx}:Renishaw", f"{idx}:Spectrum", f"{idx}:Index"])
+            time_node = await client.nodes.objects.get_child([f"{idx}:Renishaw", f"{idx}:Spectrum", f"{idx}:Time"])
 
 
-            subscription = await client.create_subscription(500, self.handler)
-            nodes = [xlist_node, ilist_node, index_node]
+            subscription = await client.create_subscription(200, self.handler)
+            nodes = [xlist_node, ilist_node, index_node, time_node]
             # We subscribe to data changes for two nodes (variables).
             await subscription.subscribe_data_change(nodes)
             # We subscribe to data changes for two nodes (variables).
             await subscription.subscribe_data_change(nodes)
-            # We let the subscription run for ten seconds
-            await asyncio.wait([self.event.wait()])
+            # Run until we get signalled to stop
+            wait_task = asyncio.Task(self.event.wait())
+            await wait_task
             # We delete the subscription (this un-subscribes from the data changes of the two variables).
             # This is optional since closing the connection will also delete all subscriptions.
             await subscription.delete()
             # After one second we exit the Client context manager - this will close the connection.
             await asyncio.sleep(1)
             # We delete the subscription (this un-subscribes from the data changes of the two variables).
             # This is optional since closing the connection will also delete all subscriptions.
             await subscription.delete()
             # After one second we exit the Client context manager - this will close the connection.
             await asyncio.sleep(1)
-        
+
 
 class App(ttk.Frame):
 
 class App(ttk.Frame):
-    def __init__(self, master, *args, **kwargs):
+    def __init__(self, master, options, *args, **kwargs):
         super(App, self).__init__(master, *args, **kwargs)
         super(App, self).__init__(master, *args, **kwargs)
+        self.options = options
+        self.index = None
+        self.timestamp = None
         master.wm_withdraw()
         master.wm_title(APP_TITLE)
         self.create_ui()
         master.wm_withdraw()
         master.wm_title(APP_TITLE)
         self.create_ui()
-        self.grid(sticky = "news")
+        self.grid(sticky="news")
         self.bind('<<OpcDataReceived>>', self.on_opc_data_received)
         master.wm_protocol("WM_DELETE_WINDOW", self.on_destroy)
         master.grid_rowconfigure(0, weight=1)
         master.grid_columnconfigure(0, weight=1)
         master.wm_deiconify()
         self.queue = Queue()
         self.bind('<<OpcDataReceived>>', self.on_opc_data_received)
         master.wm_protocol("WM_DELETE_WINDOW", self.on_destroy)
         master.grid_rowconfigure(0, weight=1)
         master.grid_columnconfigure(0, weight=1)
         master.wm_deiconify()
         self.queue = Queue()
-        self.client = OpcClient(self)
+        self.client = OpcClient(self, self.options.uri)
         self.after(100, self.connect)
 
     def on_destroy(self):
         self.after(100, self.connect)
 
     def on_destroy(self):
@@ -109,27 +130,46 @@ class App(ttk.Frame):
     def create_ui(self):
         menu = tk.Menu(self)
         self.winfo_toplevel().configure(menu=menu)
     def create_ui(self):
         menu = tk.Menu(self)
         self.winfo_toplevel().configure(menu=menu)
-        filemenu = tk.Menu(menu)
+        filemenu = tk.Menu(menu, tearoff=False)
         menu.add_cascade(label="File", menu=filemenu)
         filemenu.add_command(label="Connect", command=self.connect)
         menu.add_cascade(label="File", menu=filemenu)
         filemenu.add_command(label="Connect", command=self.connect)
-        self.canvas = canvas = SpectrumViewer(self, background='white') #tk.Canvas(self, background="white")
-        canvas.grid(row=0, column=0, sticky=tk.NSEW)
+        filemenu.add_command(label="Disconnect", command=self.disconnect)
+        filemenu.add_separator()
+        filemenu.add_command(label='Exit', command=self.on_destroy)
+        self.plot = SpectrumViewer(self, background='white')
+        self.plot.grid(row=0, column=0, sticky=tk.NSEW)
         self.grid_rowconfigure(0, weight=1)
         self.grid_columnconfigure(0, weight=1)
 
     def connect(self):
         self.client.start()
 
         self.grid_rowconfigure(0, weight=1)
         self.grid_columnconfigure(0, weight=1)
 
     def connect(self):
         self.client.start()
 
+    def disconnect(self):
+        self.client.stop()
+
     def on_opc_data_received(self, ev):
         if not self.queue.empty():
     def on_opc_data_received(self, ev):
         if not self.queue.empty():
-            dtype,data = self.queue.get()
+            dtype, data = self.queue.get()
             if dtype == 'xlist':
             if dtype == 'xlist':
-                self.canvas.xlist = data
+                self.plot.xlist = data
             elif dtype == 'ilist':
             elif dtype == 'ilist':
-                self.canvas.ilist = data
-                self.after_idle(self.canvas.replot)
+                self.plot.ilist = data
+                self.after_idle(self.plot.replot)
+            elif dtype == 'time':
+                self.timestamp = data
+                self.update_title()
             elif dtype == 'index':
             elif dtype == 'index':
-                self.canvas.title(f"Acquisition #{data}")
+                self.index = data
+                self.update_title()
+
+    def update_title(self):
+        if self.timestamp and self.index:
+            ndx = self.index
+            t = f"{self.timestamp:%H:%M:%S %d/%m/%Y}"
+            self.index = None
+            self.timestamp = None
+            self.plot.title(f"Acquisition #{ndx}   {t}")
+
 
 def load_idle(root):
     """Load the IDLE shell"""
 
 def load_idle(root):
     """Load the IDLE shell"""
@@ -141,10 +181,15 @@ def load_idle(root):
     except ModuleNotFoundError:
         pass
 
     except ModuleNotFoundError:
         pass
 
+
 def main(args=None):
     global app, root
 def main(args=None):
     global app, root
+    parser = argparse.ArgumentParser(description="Renishaw OPC spectrum server viewer")
+    parser.add_argument('-u', '--uri', type=str, default='opc.tcp://localhost:4840',
+                        help='Specify the OPC server URI.')
+    options = parser.parse_args((args))
     root = tk.Tk()
     root = tk.Tk()
-    app = App(root)
+    app = App(root, options)
     root.after(200, load_idle, root)
     try:
         root.mainloop()
     root.after(200, load_idle, root)
     try:
         root.mainloop()
@@ -152,5 +197,6 @@ def main(args=None):
         tkmessagebox.showerror('Error', e)
     return 0
 
         tkmessagebox.showerror('Error', e)
     return 0
 
+
 if __name__ == '__main__':
     sys.exit(main(sys.argv[1:]))
 if __name__ == '__main__':
     sys.exit(main(sys.argv[1:]))