--- /dev/null
+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.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='Title', anchor=tk.CENTER, font=self.font)
+ self.bind('<Configure>', self.on_resize)
+
+ def title(self, text):
+ self.itemconfigure('Title', text=text)
+
+ def on_resize(self, event):
+ """Replot on window resize events"""
+ self.replot(True)
+
+ def adjust_axes(self):
+ """Resize the axes space for the current font."""
+ width = self.font.measure('0'*6)
+ height = self.font.metrics()['linespace']
+ self.yaxis_space = width
+ self.xaxis_space = height
+
+ def replot(self, resized=False):
+ """
+ Update the canvas graph lines from the cached data lists.
+ The lines are scaled to match the canvas size as the window may
+ be resized by the user.
+ """
+ self.adjust_axes()
+ width = self.winfo_width() - self.yaxis_space
+ height = self.winfo_height() - self.xaxis_space
+ if self.ilist and self.xlist:
+ ymin = min(self.ilist)
+ ymax = max(self.ilist)
+ yrange = ymax - ymin
+ npoints = len(self.xlist)
+ coords = []
+ for n in range(0, npoints):
+ x = (width * n) / npoints + self.yaxis_space
+ y = height - self.xaxis_space
+ if yrange != 0:
+ yval = ((self.ilist[n] - ymin) * height) / yrange
+ y -= yval
+ coords.append(x)
+ coords.append(y)
+ self.coords('Spectrum', *coords)
+ self.itemconfigure('XMin', text=f"{self.xlist[0]:.0f}")
+ self.itemconfigure('XMax', text=f"{self.xlist[-1]:.0f}")
+ self.itemconfigure('YMin', text=f"{ymin:.0f}")
+ self.itemconfigure('YMax', text=f"{ymax:.0f}")
+ if resized:
+ y = height - self.xaxis_space
+ 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]
+ 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('YMax', [self.yaxis_space, 0])
+ self.coords('Title', [width/2, 10])
--- /dev/null
+#!/usr/bin/env python3
+
+import sys
+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 spectrumviewer import SpectrumViewer
+
+APP_TITLE = "OPC Monitor"
+
+logging.basicConfig(level=logging.WARNING)
+_logger = logging.getLogger('asyncua')
+
+
+class SubscriptionHandler:
+ """
+ The SubscriptionHandler is used to handle the data that is received for the subscription.
+ """
+ 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 = id.split('.')[-1]
+ if id == 'xlist':
+ if val[0] > val[1]:
+ val.reverse()
+ self.reverse = True
+ else:
+ self.reverse = False
+ if id == 'ilist' and self.reverse:
+ val.reverse()
+ if id in ('xlist', 'ilist', 'index'):
+ 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()
+ self.app = app
+
+ def start(self):
+ self.event.clear()
+ self.thread.start()
+
+ def stop(self):
+ self.event.set()
+
+ def thread_main(self, self2, app):
+ print(f"thread_main: {self} {self2} {app}")
+ self.handler = SubscriptionHandler(app)
+ asyncio.run(self.main())
+
+ async def main(self):
+ client = Client(url='opc.tcp://localhost:4840/')
+ 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"])
+
+ subscription = await client.create_subscription(500, self.handler)
+ nodes = [xlist_node, ilist_node, index_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()])
+ # 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):
+ super(App, self).__init__(master, *args, **kwargs)
+ master.wm_withdraw()
+ master.wm_title(APP_TITLE)
+ self.create_ui()
+ 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.after(100, self.connect)
+
+ def on_destroy(self):
+ self.client.stop()
+ self.master.destroy()
+
+ def create_ui(self):
+ menu = tk.Menu(self)
+ self.winfo_toplevel().configure(menu=menu)
+ filemenu = tk.Menu(menu)
+ 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)
+ self.grid_rowconfigure(0, weight=1)
+ self.grid_columnconfigure(0, weight=1)
+
+ def connect(self):
+ self.client.start()
+
+ def on_opc_data_received(self, ev):
+ if not self.queue.empty():
+ dtype,data = self.queue.get()
+ if dtype == 'xlist':
+ self.canvas.xlist = data
+ elif dtype == 'ilist':
+ self.canvas.ilist = data
+ self.after_idle(self.canvas.replot)
+ elif dtype == 'index':
+ self.canvas.title(f"Acquisition #{data}")
+
+def load_idle(root):
+ """Load the IDLE shell"""
+ try:
+ import idlelib.pyshell as pyshell
+ sys.argv = [sys.argv[0], "-n"]
+ root.bind("<Control-F2>", lambda ev: pyshell.main())
+ root.bind("<Control-i>", lambda ev: pyshell.main())
+ except ModuleNotFoundError:
+ pass
+
+def main(args=None):
+ global app, root
+ root = tk.Tk()
+ app = App(root)
+ root.after(200, load_idle, root)
+ try:
+ root.mainloop()
+ except Exception as e:
+ tkmessagebox.showerror('Error', e)
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))