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)
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
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])
#!/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)
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()
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):
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"""
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()
tkmessagebox.showerror('Error', e)
return 0
+
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))