commit 244ff34596ac502880ddc5d28a5728fa074dd177 Author: Georg Schlisio Date: Fri Feb 5 18:55:05 2021 +0100 initial commit, first structure diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/ASCII Protocol V1.8.pdf b/ASCII Protocol V1.8.pdf new file mode 100644 index 0000000..04b8b9b Binary files /dev/null and b/ASCII Protocol V1.8.pdf differ diff --git a/MKS_ASCII_Protocol.py b/MKS_ASCII_Protocol.py new file mode 100644 index 0000000..323586f --- /dev/null +++ b/MKS_ASCII_Protocol.py @@ -0,0 +1,182 @@ +""" +Implementation of the MKS ASCII Protocol +as specified in "ASCII Protocol V1.8.pdf" + +""" + +__author__ = "Georg Schlisio 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() \ No newline at end of file diff --git a/README b/README new file mode 100644 index 0000000..91109c4 --- /dev/null +++ b/README @@ -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. diff --git a/message.py b/message.py new file mode 100644 index 0000000..7f34125 --- /dev/null +++ b/message.py @@ -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)