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__/
+.tox/
index c8e18d95477121c86bf865b142f606f709ffbb93..6ae7569611115af9c1518faac7e4a1d8cb117146 100644 (file)
@@ -1,24 +1,25 @@
 import tkinter as tk
 import tkinter.font as tkfont
 
+
 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.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)
-    
+
     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."""
-        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
@@ -63,12 +64,13 @@ class SpectrumViewer(tk.Canvas):
             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)
-            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('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('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
+import argparse
 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
 
+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)
@@ -24,15 +30,17 @@ class SubscriptionHandler:
     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)
-        _,id = str(node).split(';')
+        _, id = str(node).split(';')
         id = id.split('.')[-1]
+        if id.startswith('s='):
+            id = id.split('=')[-1].lower()
         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()
-        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:
-    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.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):
-        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.thread = None
+        self.event = None
 
     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")
-            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 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)
-        
+
 
 class App(ttk.Frame):
-    def __init__(self, master, *args, **kwargs):
+    def __init__(self, master, options, *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()
-        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.client = OpcClient(self)
+        self.client = OpcClient(self, self.options.uri)
         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)
-        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)
-        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()
 
+    def disconnect(self):
+        self.client.stop()
+
     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':
-                self.canvas.xlist = data
+                self.plot.xlist = data
             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':
-                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"""
@@ -141,10 +181,15 @@ def load_idle(root):
     except ModuleNotFoundError:
         pass
 
+
 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()
-    app = App(root)
+    app = App(root, options)
     root.after(200, load_idle, root)
     try:
         root.mainloop()
@@ -152,5 +197,6 @@ def main(args=None):
         tkmessagebox.showerror('Error', e)
     return 0
 
+
 if __name__ == '__main__':
     sys.exit(main(sys.argv[1:]))