diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6f93ca1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*egg* +*/*/__pycache__/* +*/*_pycache_* \ No newline at end of file diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..50ed3bc --- /dev/null +++ b/license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Robert Guggenberger + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/localite/client.py b/localite/client.py new file mode 100644 index 0000000..daa9e04 --- /dev/null +++ b/localite/client.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +""" +A client to communicate with the Localite to control the magventure. + +@author: Robert Guggenberger +""" +import socket +import json +# %% + + +class Client(object): + """ + A LocaliteJSON socket client used to communicate with a LocaliteJSON socket server. + + example + ------- + host = '127.0.0.1' + port = 6666 + client = Client(True) + client.connect(host, port).send(data) + response = client.recv() + client.close() + + example + ------- + response = Client().connect(host, port).send(data).recv_close() + """ + + socket = None + + def __del__(self): + self.close() + + def __init__(self, host, port=6666): + self.host = host + self.port = port + + def connect(self): + 'connect wth the remote server' + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.connect((self.host, self.port)) + self.socket.settimeout(3) + + def close(self): + 'closes the connection' + self.socket.shutdown(1) + self.socket.close() + + def write(self, data): + self.socket.sendall(data.encode('ascii')) + return self + + def read(self): + 'parse the message until it is a valid json' + msg = bytearray(b' ') + while True: + try: + prt = self.socket.recv(1) + msg += prt + key, val = self.decode(msg.decode('ascii')) # because the first byte is b' ' + return key, val + except json.decoder.JSONDecodeError: + pass + except Exception as e: + raise e + + def decode(self, msg:str, index=0): + msg = json.loads(msg) + key = list(msg.keys())[index] + val = msg[key] + return key, val + + def send(self, msg:str): + self.connect() + self.write(msg) + self.close() + + def request(self, msg='{"get":"coil_0_amplitude"}'): + self.connect() + self.write(msg) + key = val = '' + _, expected = self.decode(msg) + while key != expected: + key, val = self.read() + self.close() + return key, val + +# c = Client(host=host)) +# %timeit -n 1 -r 1000 c.request() +# 5.38 ms ± 244 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) +# %% +# %timeit -n 1 -r 1000 c.send('{"coil_0_amplitude":45}') +# 451 µs ± 12.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) diff --git a/localite/mockserver.py b/localite/mockserver.py new file mode 100644 index 0000000..8a39037 --- /dev/null +++ b/localite/mockserver.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Mock LocaliteJSON Server for testing and debugging + +@author: Robert Guggenberger +""" +import socket +import sys +import datetime + +class MockServer(object): + """ + A LocaliteJSON socket server used to communicate with a LocaliteJSON socket client. + for testing purposes - replies with the message send + + example + ------- + host = 127.0.0.1 + port = 6666 + server = Server(host, port) + server.loop() + """ + + backlog = 5 + client = None + + def __init__(self, host, port, verbose): + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.socket.bind((host, port)) + self.socket.listen(self.backlog) + self.verbose = verbose + print('Creating TestServer at', host, ':', port) + + def __del__(self): + self.close() + + def accept(self): + # if a client is already connected, disconnect it + if self.client: + self.client.close() + self.client, self.client_addr = self.socket.accept() + return self + + def send(self, data): + if not self.client: + raise Exception('Cannot send data, no client is connected') + _send(self.client, data) + return self + + def recv(self): + if not self.client: + raise Exception('Cannot receive data, no client is connected') + return _recv(self.client) + + def loop(self): + try: + while True: + self.accept() + data = self.recv() + if self.verbose: + print('Received', data, 'from', self.client_addr, 'at', datetime.datetime.now()) + self.send(data) + except Exception as e: + raise e + finally: + self.close() + + def close(self): + if self.client: + self.client.close() + self.client = None + if self.socket: + self.socket.close() + self.socket = None + + + +# helper functions # +def _send(socket, data): + try: + serialized = data.encode('ASCII') + except (TypeError, ValueError): + raise Exception('Message is not serializable') + socket.sendall(serialized) + + +def _recv(socket): + # read ASCII letter by letter until we reach a zero count of +{-} + def parse(counter, buffer): + if counter is None: + counter = 0 + char = socket.recv(1).decode('ASCII') + buffer.append(char) + if char is '{': + counter += 1 + if char is '}': + counter -= 1 + return counter, buffer + + buffer = [] + counter = None + while counter is not 0: + counter, buffer = parse(counter, buffer) + # print(counter, buffer[-1]) + + buffer = ''.join(buffer) + return buffer + +def myip(): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + s.close() + return ip + +def parse_message(): + try: + data = sys.argv[sys.argv.index('-m')+1] + if data[0] is '-': + raise IndexError + except IndexError: + data = '{"missing":"message"}' + return data + +def single_command_only(): + print('Choose either -m for message or -t for TestServer') + quit() + +def show_help(): + print(''' + Specify arguments + ----------------- + + -m for message + -t Start a test-server + -p followed by port, defaults to 6666 + -h followed by host, defaults to ip adress + -v turn verbose on + + Example + ------- + python LocaliteJSON.py -h 127.0.0.1 -p 6666 -m '{"test":"message"}' + + Example + ------- + python LocaliteJSON.py -h 127.0.0.1 -p 6666 -t + + ''') + quit() + +class Messages(): + gci = "{'get':'current_instrument'}" +# %% +if __name__ == '__main__': + if len(sys.argv) < 2 or '-help' in sys.argv: + show_help() + + # set defaults + host = myip() + port = 6666 + verbose = False + + if '-h' in sys.argv: + host = sys.argv[sys.argv.index('-h')+1] + if '-p' in sys.argv: + port = int(sys.argv[sys.argv.index('-p')+1]) + if '-v' in sys.argv: + verbose = True + if '-t' in sys.argv and '-m' in sys.argv: + single_command_only() + quit() + + # start test server + if '-t' in sys.argv: + server = MockServer(host, port, verbose) + server.loop() + # start client socket diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..49568c3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +json +socket \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..3ff4486 --- /dev/null +++ b/setup.py @@ -0,0 +1,31 @@ +from distutils.core import setup + + +setup( + name='dev-localite', + version='0.0.1', + description='Control Magventure with LocaliteJSON', + long_description='Toolbox to control a Magventure TMS with localites JSON-TCP-IP Interface', + author='Robert Guggenberger', + author_email='robert.guggenberger@uni-tuebingen.de', + url='https://github.com/stim-devices/dev-localite.git', + download_url='https://github.com/stim-devices/dev-localite.git', + license='MIT', + packages=['localite'], + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'Intended Audience :: Education', + 'Intended Audience :: Healthcare Industry', + 'Intended Audience :: Science/Research', + 'Intended Audience :: Information Technology', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Topic :: Scientific/Engineering :: Human Machine Interfaces', + 'Topic :: Scientific/Engineering :: Medical Science Apps.', + 'Topic :: Software Development :: Libraries', + ] +)