initial commit, first structure

This commit is contained in:
Georg Schlisio
2021-02-05 18:55:05 +01:00
commit 244ff34596
5 changed files with 265 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
__pycache__

BIN
ASCII Protocol V1.8.pdf Normal file

Binary file not shown.

182
MKS_ASCII_Protocol.py Normal file
View File

@@ -0,0 +1,182 @@
"""
Implementation of the MKS ASCII Protocol
as specified in "ASCII Protocol V1.8.pdf"
"""
__author__ = "Georg Schlisio <georg.schlisio@ipp.mpg.de"
__version__ = 0.1
__protocol_version__ = 1.8
#### imports
import time
import sys
import os
import logging
import socket
__softwarename__ = "MKS ASCII server on " + socket.gethostname()
from message import ParseMessage, ComposeMessage
logging.basicConfig(level=logging.INFO)
####
composer = ComposeMessage()
class MKS_ASCII_Protocol():
# 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
message_queue = []
# Mass spectrometer properties
filament = None # number of filament in use
def __init__(self, ip, port=10014, local_port=10000, target_state="connected"):
"""
Talk to an MKS mass spectrometer over the ethernet interface.
:param ip:
:param port:
:param local_port:
"""
self._remote_ip = ip
self._remote_port = port
self._local_port = local_port
self._target_state = target_state
def turn_on(self):
self._target_state = "connected"
def turn_off(self):
self._target_state = "disconnected"
def run(self):
"""
Main loop.
:return:
"""
while True:
logging.info(f"starting new loop")
start_time = time.time()
if self._target_state == "connected" and not self._connected:
self._connect()
if self._connected:
self._check_for_messages()
if self._target_state == "disconnected":
self._disconnect()
break
if self._target_state == "exit":
self._disconnect()
exit(0)
end_time = time.time()
if end_time - start_time < self.loop_min_time:
sleeptime = self.loop_min_time - (end_time - start_time)
logging.info(f"sleep for {sleeptime}s")
time.sleep(sleeptime)
def _send(self, msg, args=None):
"""
Wrapper for sending messages
:param msg:
:return:
"""
self._s.send(composer(msg, args=args))
def _connect(self):
"""
Initiate connection
:return:
"""
assert not self._connected, "Cannot connect "
self._s = socket.socket(family=socket.AF_INET)
self._host = socket.gethostname()
self._s.bind((self._host, self._local_port))
self._s.connect((self._remote_ip, self._remote_port))
self.connected = True
self._protocol_version_check()
# control message formatting
if self.format_with_tab:
self._send("FormatWithTab True")
# negotiate protocol version
self._send("AcceptProtocol 1.6") # TODO check if actual protocol needs to be signaled
def _protocol_version_check(self, tries = 10):
"""
Wait for first message and evaluate protocol compatibility
:return:
"""
while True:
self._check_for_messages()
if len(self.message_queue) > 0:
break
time.sleep(1)
tries -= 1
if tries == 0:
logging.error(f"Connection not successfully established: received no greeting. Exiting.")
exit(201)
message = self.message_queue.pop(0)
assert message.type == "MKSRGA", f"Unexpected message type: {message.type}"
assert message.parms[message.type] in ("Single", "Multi"), f"Unexpected connection type: {message.parms[message.type]}"
assert message.parms[message.type] == "Single", f"Connection to QMS Servers of type 'Multi' is not supported." # To support this, implement the commands "Sensors" and "Select"
assert "Protocol_Revision" in message.parms
assert "Min_Compatibility" in message.parms
self._remote_protocol_version = float(message.parms["Protocol_Revision"])
self._remote_min_protocol_version = float(message.parms["Min_Compatibility"])
if self._remote_min_protocol_version > self._protocol_version:
logging.error(f"Version mismatches: remote requires V{self._remote_min_protocol_version} but we have only V{self._protocol_version}, exiting")
exit(202)
def _disconnect(self):
"""
Terminate connection
:return:
"""
# send message "Release"
self._s.send(composer(mtype="Release"))
# TODO: replace sleep with actual check for "Release Ok"
time.sleep(2)
self._s.close()
self._connected = False
def _check_for_messages(self):
"""
Read socket buffer, parse messages and enqueue them into the message queue
:return:
"""
rawinput = self._s.recv(4096).decode("ASCII").split("\r\r")
if len(rawinput) > 1:
logging.info(f"found {len(rawinput) - 1} messages")
for i in range(len(rawinput) - 1):
self.message_queue.append(ParseMessage(rawinput[i]))
if __name__ == "__main__":
p = MKS_ASCII_Protocol(ip="127.0.0.1")
p.run()

4
README Normal file
View File

@@ -0,0 +1,4 @@
Control software for MKS mass spectrometers
This is a python implementation of the MKS ASCII protocol used for a multitude of MKS mass spectrometers.
The implementation is based on the included spec, which was obtained from MKS.

78
message.py Normal file
View File

@@ -0,0 +1,78 @@
import time
class ParseMessage():
parms = {}
raw=None
def __init__(self, raw):
self.time = time.time()
self.raw = raw
self.parse()
def parse(self):
"""
Parser for incoming messages.
:return:
"""
for i, line in enumerate(self.raw.split("\r\n")):
entries = line.split()
if entries == []:
continue
if i == 0:
self.type = entries[0]
if len(entries) < 2:
raise NotImplementedError()
elif len(entries) == 2:
self.parms[entries[0]] = entries[1]
else:
self.parms[entries[0]] = entries[1:]
class ComposeMessage():
def __init__(self):
pass
def __call__(self, mtype, args=None):
"""
Compose raw byte string to send over ethernet
TODO: check argument type
TODO: make sure all commands fit the scheme
:param mtype: command as static string
:param args: list of argument strings (optional, default None)
:return: raw byte string
"""
assert isinstance(mtype, str)
#assert isinstance(args, list)
raw = mtype
if args is not None:
raw += " "+" ".join(args)
raw += "\r\n\r\r"
return raw.encode("ASCII")
if __name__ == "__main__":
sample_messages = {
"SensorState": 'SensorState OK\r\n State InUse\r\n UserApplication "Process Eye Professional"\r\n UserVersion V5.2\r\n UserAddress 127.0.0.1\r\n',
"Info": 'Info OK\r\n SerialNumber LM70-00197021\r\n Name "Chamber A"\r\n State Ready\r\n UserApplication N/A' +\
'\r\n UserVersion N/A\r\n UserAddress N/A\r\n ProductID 70 MicroVision+\r\n RFConfiguration 0 "Smart Head"' +\
'\r\n DetectorType 0 Faraday\r\n SEMSupply 3000 3.0kV\r\n ExternalHardware 0 None\r\n TotalPressureGauge 0 ' +\
'"Not Fitted"\r\n Page 13 of 158\r\n FilamentType 0 Tungsten\r\n ControlUnitUse 4 "Standard RGA"\r\n' +\
' SensorType 1 "Standard Open Source"\r\n InletType 1 None\r\n Version V3.70\r\n NumEGains 3\r\n' +\
' NumDigitalPorts 2\r\n NumAnalogInputs 4\r\n NumAnalogOutputs 1\r\n NumSourceSettings 6\r\n ' +\
'NumInlets 1\r\n MaxMass 200\r\n ActiveFilament 1\r\n FullScaleADCAmps 0.000002\r\n FullScaleADCCount' +\
' 8388608\r\n PeakResolution 32\r\n ConfigurableIonSource Yes\r\n RolloverCompensation No\r\n',
"EGains": 'EGains OK\r\n 1\r\n 100\r\n 20000\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",
#"AcceptProtocol":
"initiate": "MKSRGA Single\r\n Protocol_Revision 1.1\r\n Min_Compatibility 1.1\r\n\r\n\r\r",
"Error": 'command ERROR\r\n Number 200\r\n Description "err description"\r\n\r\n',
}
cm = ComposeMessage()
#bmess = cm(mtype="Select", args=["LM70-00197021"])
#smess = bmess.decode("ASCII")
pm = ParseMessage(sample_messages["Error"])
print(pm.type, pm.parms, pm.time)