greater rewrite of some time ago, commited for reference
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
Implementation of the MKS ASCII Protocol
|
Implementation of the MKS ASCII Protocol
|
||||||
as specified in "ASCII Protocol V1.8.pdf"
|
as specified in "ASCII Protocol V1.8.pdf"
|
||||||
|
|
||||||
|
The
|
||||||
"""
|
"""
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
@@ -17,39 +18,20 @@ import sys
|
|||||||
import os
|
import os
|
||||||
#import numpy as np
|
#import numpy as np
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
import queue
|
import queue
|
||||||
import socket
|
import socket
|
||||||
__softwarename__ = "MKS ASCII server on " + socket.gethostname()
|
__softwarename__ = "MKS ASCII server on " + socket.gethostname()
|
||||||
from message import ParseMessage, ComposeMessage
|
from message import ParseMessage, ComposeMessage
|
||||||
|
|
||||||
|
## basic config
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
assert (sys.version_info[0] + sys.version_info[1]*0.1 ) >= 3.7, "Minimum python requirement not met: "+str(sys.version_info[0] + sys.version_info[1]*0.1)+". We need 3.7 or above for fstrings and queue features."
|
||||||
|
|
||||||
####
|
####
|
||||||
composer = ComposeMessage()
|
composer = ComposeMessage()
|
||||||
|
|
||||||
class MKSListener():
|
class MKSListener():
|
||||||
# networking
|
|
||||||
_remote_port = 10014 # port on remote device
|
|
||||||
_local_port = 10000 # local port
|
|
||||||
_s = None # socket object
|
|
||||||
|
|
||||||
# connection state
|
|
||||||
_connected = False # connection state
|
|
||||||
_target_state = "disconnected" # target connection state
|
|
||||||
device_sn = None # device SN currently connected to
|
|
||||||
|
|
||||||
# protocol versioning and formatting
|
|
||||||
_remote_min_protocol_version = None # remote requirement of min protocol, retrieved from remote device
|
|
||||||
_min_protocol_version = 1.6 # assumed minimum protocol version
|
|
||||||
_protocol_version = __protocol_version__
|
|
||||||
format_with_tab = False # whether to request FormatWithTab (reduces networkload slighlty)
|
|
||||||
|
|
||||||
# program flow control
|
|
||||||
loop_min_time = 0.01 # minimum duration of a listener loop in seconds
|
|
||||||
|
|
||||||
# Mass spectrometer properties
|
|
||||||
filament = None # number of filament in use
|
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, ip, up_queue, down_queue, port=10014, local_port=10000, target_state="connected"):
|
def __init__(self, ip, up_queue, down_queue, port=10014, local_port=10000, target_state="connected"):
|
||||||
"""
|
"""
|
||||||
@@ -63,10 +45,31 @@ class MKSListener():
|
|||||||
"""
|
"""
|
||||||
self.up_queue = up_queue # queue of messages towards the RGA device
|
self.up_queue = up_queue # queue of messages towards the RGA device
|
||||||
self.down_queue = down_queue # queue of messages from the RGA device
|
self.down_queue = down_queue # queue of messages from the RGA device
|
||||||
|
self._target_state = target_state
|
||||||
|
|
||||||
|
# networking
|
||||||
self._remote_ip = ip #
|
self._remote_ip = ip #
|
||||||
self._remote_port = port
|
self._remote_port = port
|
||||||
self._local_port = local_port
|
self._local_port = local_port
|
||||||
self._target_state = target_state
|
self._s = None # socket object
|
||||||
|
|
||||||
|
# connection state
|
||||||
|
self._connected = False # connection state
|
||||||
|
self._target_state = "disconnected" # target connection state
|
||||||
|
self.device_sn = None # device SN currently connected to
|
||||||
|
|
||||||
|
# protocol versioning and formatting
|
||||||
|
self._remote_min_protocol_version = None # remote requirement of min protocol, retrieved from remote device
|
||||||
|
self._min_protocol_version = 1.6 # assumed minimum protocol version
|
||||||
|
self._protocol_version = __protocol_version__
|
||||||
|
self.format_with_tab = False # whether to request FormatWithTab (reduces networkload slighlty)
|
||||||
|
|
||||||
|
# program flow control
|
||||||
|
self.loop_min_time = 0.01 # minimum duration of a listener loop in seconds
|
||||||
|
|
||||||
|
# Mass spectrometer properties
|
||||||
|
self.filament = None # number of filament in use
|
||||||
|
|
||||||
|
|
||||||
def turn_on(self):
|
def turn_on(self):
|
||||||
self._target_state = "connected"
|
self._target_state = "connected"
|
||||||
@@ -184,8 +187,6 @@ class MKSListener():
|
|||||||
self.down_queue.put(ParseMessage(rawinput[i]))
|
self.down_queue.put(ParseMessage(rawinput[i]))
|
||||||
|
|
||||||
class MKSOperator():
|
class MKSOperator():
|
||||||
_target_state = "run"
|
|
||||||
_current_protocol_version = 1
|
|
||||||
_commands = {
|
_commands = {
|
||||||
# controlling message formatting
|
# controlling message formatting
|
||||||
"FormatWithTab": {"type": "Controlling message formatting", "description": "", "action": NotImplementedError, "expected_response": "", "from_version": 1},
|
"FormatWithTab": {"type": "Controlling message formatting", "description": "", "action": NotImplementedError, "expected_response": "", "from_version": 1},
|
||||||
@@ -340,8 +341,12 @@ class MKSOperator():
|
|||||||
"DiagnosticInput": {"type": "Asynchronous Sensor Notifications", "description": "", "action": NotImplementedError, "expected_response": "", "from_version": 1.6},
|
"DiagnosticInput": {"type": "Asynchronous Sensor Notifications", "description": "", "action": NotImplementedError, "expected_response": "", "from_version": 1.6},
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, up_queue:queue.SimpleQueue, down_queue:queue.SimpleQueue, command_actions:dict = {}):
|
def __init__(self, up_queue:queue.SimpleQueue, down_queue:queue.SimpleQueue, command_actions:dict={}, target_state="run"):
|
||||||
#raise NotImplementedError()
|
#raise NotImplementedError()
|
||||||
|
|
||||||
|
self._target_state = target_state
|
||||||
|
self._current_protocol_version = 1
|
||||||
|
|
||||||
self.up_queue = up_queue
|
self.up_queue = up_queue
|
||||||
self.down_queue = down_queue
|
self.down_queue = down_queue
|
||||||
# default pre-set actions
|
# default pre-set actions
|
||||||
@@ -370,7 +375,7 @@ class MKSOperator():
|
|||||||
if self._commands[command]["from_version"] > self._current_protocol_version:
|
if self._commands[command]["from_version"] > self._current_protocol_version:
|
||||||
logging.warning(f"Unsupported command {command} for our version {self._current_protocol_version}. Ignoring.")
|
logging.warning(f"Unsupported command {command} for our version {self._current_protocol_version}. Ignoring.")
|
||||||
continue
|
continue
|
||||||
assert hasattr(command, "__call__")
|
assert hasattr(self._commands[command]['action'], "__call__"), f"{command} callable has not __call__"
|
||||||
self._commands[command]["action"] = actions[command]
|
self._commands[command]["action"] = actions[command]
|
||||||
|
|
||||||
def _digest_command(self, message):
|
def _digest_command(self, message):
|
||||||
@@ -401,19 +406,34 @@ class MKSOperator():
|
|||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def _message_tab_formatting(self, message):
|
def _message_tab_formatting(self, message):
|
||||||
|
# no action needed - all code is invariant to tabs vs spaces
|
||||||
|
# using spaces saves a few bits of bandwith due to less padding spaces, so use it anyways
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _available_sensors(self, message):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ProtocolVersionError(Exception):
|
class ProtocolVersionError(Exception):
|
||||||
raise NotImplementedError("TODO") # TODO
|
def __init__(self, version, message="Version mismatch: "):
|
||||||
|
self.message = message + str(version)
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
||||||
|
def run(obj):
|
||||||
|
obj.run()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print(sys.version_info)
|
|
||||||
# use https://docs.python.org/3/library/queue.html#queue.SimpleQueue
|
|
||||||
q = queue.SimpleQueue()
|
|
||||||
# producer: MKS_ASCII_Protocol instance
|
|
||||||
# consumer: yet-to-write
|
|
||||||
# launch both in separate threads (https://docs.python.org/3/library/threading.html) and have them communicating via queue
|
|
||||||
|
|
||||||
p = MKSListener(ip="127.0.0.1")
|
# communicating queues: up -> towards MKS instrument, down -> from MKS intrument
|
||||||
|
upq = queue.Queue()
|
||||||
p.run()
|
downq = queue.Queue()
|
||||||
|
# create instances of listener and operator
|
||||||
|
l = MKSListener(ip="127.0.0.1", up_queue=upq, down_queue=downq)
|
||||||
|
o = MKSOperator(up_queue=upq, down_queue=downq)
|
||||||
|
# create thread for each
|
||||||
|
listenerThread = threading.Thread(target=run, args=(l,))
|
||||||
|
operatorThread = threading.Thread(target=run, args=(o,))
|
||||||
|
# start threads
|
||||||
|
operatorThread.start()
|
||||||
|
listenerThread.start()
|
||||||
84
message.py
84
message.py
@@ -1,33 +1,72 @@
|
|||||||
import time
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
class ParseMessage():
|
class ParseMessage():
|
||||||
parms = {}
|
|
||||||
raw=None
|
table_commands = ["Sensors", "EGains", "InletInfo", "DetectorInfo", "AnalogInputInfo",
|
||||||
|
"AnalogOutputInfo", "DigitalInfo", "SourceAlignmentInfo", "SourceResolutionInfo",
|
||||||
|
"DiagnosticInputInfo", "RunDiagnostics", ""]
|
||||||
|
verbose_commands = ["DiagnosticInput", "DigitalPortChange", "AnalogInput", "MassReading", "ZeroReading",
|
||||||
|
"StartingScan", "FilamentStatus", ""] # commands that feel special and don't submit to the usual pattern of command - status
|
||||||
|
|
||||||
def __init__(self, raw):
|
def __init__(self, raw):
|
||||||
|
"""
|
||||||
|
Parse incoming binary message
|
||||||
|
:param raw: raw string
|
||||||
|
"""
|
||||||
self.time = time.time()
|
self.time = time.time()
|
||||||
|
self.is_table = False
|
||||||
|
self.type = None
|
||||||
|
self.status = None
|
||||||
|
self.parms = {}
|
||||||
|
self.table_head = None
|
||||||
|
self.table_body = None
|
||||||
self.raw = raw
|
self.raw = raw
|
||||||
self.parse()
|
self.parse(raw)
|
||||||
|
|
||||||
def parse(self):
|
def parse(self, raw):
|
||||||
"""
|
"""
|
||||||
Parser for incoming messages.
|
Parser for incoming messages.
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
for i, line in enumerate(self.raw.split("\r\n")):
|
# split message at newline indicators and get rid of empty entries
|
||||||
entries = line.split()
|
linebyline = list(filter(None, raw.split("\r\n")))
|
||||||
if entries == []:
|
|
||||||
continue
|
# first line contains command and status or more
|
||||||
if i == 0:
|
line1 = linebyline[0].split()
|
||||||
self.type = entries[0]
|
self.type = line1[0]
|
||||||
if len(entries) < 2:
|
if self.type in self.table_commands:
|
||||||
raise NotImplementedError()
|
self.is_table = True
|
||||||
elif len(entries) == 2:
|
if len(line1) == 1:
|
||||||
self.parms[entries[0]] = entries[1]
|
pass
|
||||||
|
elif len(line1) == 2:
|
||||||
|
self.type = line1[0]
|
||||||
|
self.status = line1[1]
|
||||||
else:
|
else:
|
||||||
self.parms[entries[0]] = entries[1:]
|
assert self.type in self.verbose_commands
|
||||||
|
self.parms[self.type] = line1[1:]
|
||||||
|
|
||||||
|
# terminate here for one-line commands
|
||||||
|
if len(linebyline) == 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
# following lines
|
||||||
|
if self.is_table:
|
||||||
|
# for table commands we split table head and body
|
||||||
|
thead = linebyline[1].split()
|
||||||
|
try:
|
||||||
|
tbody = [linebyline[i].split() for i in range(len(linebyline[2:]))]
|
||||||
|
except IndexError:
|
||||||
|
logging.warning(f"Digesting message {self.type}: no table body found")
|
||||||
|
tbody = []
|
||||||
|
self.table_head = thead
|
||||||
|
self.table_body = tbody
|
||||||
|
else:
|
||||||
|
# for other commands we make a dictionary with key [values, ..]
|
||||||
|
try:
|
||||||
|
self.parms.update({line.split()[0]: line.split()[1:] for line in linebyline[1:]})
|
||||||
|
except IndexError:
|
||||||
|
raise NotImplementedError("this should not happen")
|
||||||
|
|
||||||
class ComposeMessage():
|
class ComposeMessage():
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -65,14 +104,19 @@ if __name__ == "__main__":
|
|||||||
"InletInfo": 'InletInfo OK\r\n Automatic Yes \r\nActiveInlet 0\r\n Factor Fixed CanCalibrate DefaultFactor TypeName\r\n 1 Yes No 1 "Process Chamber direct"\r\n',
|
"InletInfo": 'InletInfo OK\r\n Automatic Yes \r\nActiveInlet 0\r\n Factor Fixed CanCalibrate DefaultFactor TypeName\r\n 1 Yes No 1 "Process Chamber direct"\r\n',
|
||||||
"Release": "Release Ok\r\n",
|
"Release": "Release Ok\r\n",
|
||||||
#"AcceptProtocol":
|
#"AcceptProtocol":
|
||||||
"initiate": "MKSRGA Single\r\n Protocol_Revision 1.1\r\n Min_Compatibility 1.1\r\n\r\n\r\r",
|
"initiate": "MKSRGA Single\r\n Protocol_Revision 1.1\r\n Min_Compatibility 1.1\r\n\r\n",
|
||||||
"Error": 'command ERROR\r\n Number 200\r\n Description "err description"\r\n\r\n',
|
"Error": 'command ERROR\r\n Number 200\r\n Description "err description"\r\n\r\n',
|
||||||
|
"Sensors": "Sensors OK\r\n State SerialNumber Name\r\n Ready LM70-00197021 “Chamber A”\r\n Ready LM70-00198021 “Chamber B”\r\n\r\n",
|
||||||
|
"DiagnosticInput": "DiagnosticInput 0 3.2745\r\n\r\n",
|
||||||
|
"FilamentStatus": "FilamentStatus 1 OFF\r\n Trip None\r\n Drive Off\r\n EmissionTripState OK\r\n ExternalTripState OK\r\n RVCTripState OK",
|
||||||
}
|
}
|
||||||
cm = ComposeMessage()
|
cm = ComposeMessage()
|
||||||
#bmess = cm(mtype="Select", args=["LM70-00197021"])
|
#bmess = cm(mtype="Select", args=["LM70-00197021"])
|
||||||
|
|
||||||
#smess = bmess.decode("ASCII")
|
#smess = bmess.decode("ASCII")
|
||||||
|
|
||||||
pm = ParseMessage(sample_messages["InletInfo"])
|
for m in sample_messages:
|
||||||
|
pm = ParseMessage(sample_messages[m])
|
||||||
|
print(pm.type, pm.status, pm.is_table)
|
||||||
|
|
||||||
print(pm.type, pm.parms, pm.time)
|
#print(pm.type, pm.parms, pm.time)
|
||||||
|
|||||||
Reference in New Issue
Block a user