├── .gitignore ├── LICENSE ├── README.md ├── estim2b ├── __init__.py ├── estim2b.py ├── estimsocket.py └── jolt.py ├── examples ├── example.py ├── get_status.py ├── server_client_motion_example │ ├── README.md │ ├── analyse.py │ ├── client.py │ ├── motion.py │ └── start_estim2b_server.py ├── server_client_passthru_example │ ├── README.md │ ├── client.py │ └── start_estim2b_server.py ├── set_output.py └── udp_motion_example │ ├── README.md │ ├── motion.py │ ├── recv_udp.py │ └── start_estim2b_server.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # dotenv 85 | .env 86 | 87 | # virtualenv 88 | .venv 89 | venv/ 90 | ENV/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 fredhatt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg) 2 | [![License](https://img.shields.io/badge/license-MIT%20License-brightgreen.svg)](https://opensource.org/licenses/MIT) 3 | 4 | # About estim2bapi 5 | This is an (unofficial) Python API for the E-Stim 2B. Note that this is alpha software, and 6 | thus comes with absolutely no warranty whatsoever. Use at your own risk. 7 | 8 | # Installation 9 | 10 | ## For users 11 | You can install using the pip command: 12 | 13 | pip install git+https://github.com/fredhatt/estim2bapi.git 14 | 15 | If you don't have pip in your Python distribution you can install it using, 16 | 17 | easy_install pip 18 | 19 | or (on Debian-based systems like Raspbian): 20 | 21 | apt-get install python-pip 22 | 23 | After installing, try the examples to check everything is working properly. 24 | 25 | ## For developers 26 | Clone this repository and append its path to your PYTHONPATH variable. In Linux you would do this: 27 | 28 | git clone https://github.com/fredhatt/estim2bapi 29 | echo "export PYTHONPATH=$PYTHONPATH:$(pwd)/estim2bapi" >> ~/.bashrc 30 | source ~/.bashrc 31 | 32 | # Usage 33 | # import the module and connect to 2B connected to ttyUSB0 (Linux)... 34 | import estim2b 35 | e2b = estim2b.Estim('/dev/ttyUSB0') 36 | # get status from 2B... 37 | e2b.status() 38 | 39 | For a simple usage example see example.py. 40 | -------------------------------------------------------------------------------- /estim2b/__init__.py: -------------------------------------------------------------------------------- 1 | from .estim2b import Estim 2 | from .estimsocket import EstimSocket 3 | from .jolt import Jolt 4 | -------------------------------------------------------------------------------- /estim2b/estim2b.py: -------------------------------------------------------------------------------- 1 | #! env python 2 | 3 | import serial 4 | import time 5 | import sys 6 | import platform 7 | 8 | ''' 9 | TODO: 10 | * add an enforce_consistency=boolean option to Estim, after 11 | sending a command get the status from the 2B and check it 12 | matches what EstimStatus thinks the status should be. 13 | ''' 14 | 15 | class EstimStatus: 16 | 17 | status = {'battery': None, 'A': None, 'B': None, 'C': None, 'D': None, 18 | 'mode': None, 'power': None, 'joined': None} 19 | 20 | keys = ['battery', 'A', 'B', 'C', 'D', 'mode', 'power', 'joined'] 21 | 22 | def parseReply(self, replyString): 23 | print(replyString) 24 | print(replyString.decode()) 25 | if not ":" in replyString: #check, if reply isn't empty 26 | print('Error communicating with E-stim 2B unit!') 27 | print(' check connection and power.') 28 | return False 29 | else: 30 | r = replyString.split(":") 31 | status_dict = {} 32 | for i, key in enumerate(self.keys): 33 | if i == 6: 34 | status_dict[key] = str(r[i]) 35 | else: 36 | status_dict[key] = int(r[i]) 37 | return status_dict 38 | 39 | #self.status.set(int(r[0]), int(r[1])/2, int(r[2])/2, int(r[3])/2, int(r[4])/2, 40 | # int(r[5]), str(r[6]), int(r[7])) 41 | 42 | def set(self, battery, A, B, C, D, mode, power, joined): 43 | stat = locals() 44 | stat.pop('self', None) 45 | self.status = stat 46 | 47 | def _set_kw(self, **kwargs): 48 | for k, v in kwargs.items(): 49 | self.status[k] = v 50 | 51 | def check(self, battery=None, A=None, B=None, C=None, D=None, mode=None, power=None, joined=None): 52 | ndiff = 0 53 | for k, v in locals(): 54 | if v is None: continue 55 | if v == self.status[k]: 56 | ndiff += 1 57 | return ndiff 58 | 59 | def _getstr(self): 60 | return "{}:{}:{}:{}:{}:{}:{}:00".format(self.status['battery'], self.status['A'], self.status['B'], 61 | self.status['C'], self.status['D'], self.status['mode'], self.status['power'], self.status['joined']) 62 | 63 | def _format_status(self): 64 | e2bstat = "==============================\n" 65 | for k, v in self.status.items(): 66 | space = " " 67 | for i in range(8 - len(k)): k += space 68 | e2bstat += " {}: {}\n".format(k, v) 69 | e2bstat += "==============================" 70 | return e2bstat 71 | 72 | def __call__(self, formatted=False, string=False): 73 | if string: 74 | return self._getstr() 75 | if formatted: 76 | return self._format_status() 77 | else: 78 | return self.status 79 | 80 | def update(self, command): 81 | if len(command) == 0: return True 82 | if command[0] == 'A': 83 | self._set_kw(A = int(command[1:])*2) 84 | return True 85 | if command[0] == 'B': 86 | self._set_kw(B = int(command[1:])*2) 87 | return True 88 | if command[0] == 'C': 89 | self._set_kw(C = int(command[1:])*2) 90 | return True 91 | if command[0] == 'D': 92 | self._set_kw(D = int(command[1:])*2) 93 | return True 94 | if command[0] == 'J': 95 | self._set_kw(joined=command[1:]) 96 | return True 97 | if command[0] == 'M': 98 | self._set_kw(mode=command[1:]) 99 | return True 100 | if command[0] == 'H' or command[0] == 'L': 101 | self._set_kw(power=command[0]) 102 | return True 103 | return False # unrecognised command 104 | 105 | 106 | 107 | 108 | 109 | class Estim: 110 | modekey = { 111 | "pulse":0, 112 | "bounce":1, 113 | "continuous":2, 114 | "asplit":3, 115 | "bsplit":4, 116 | "wave":5, 117 | "waterfall":6, 118 | "squeeze":7, 119 | "milk":8, 120 | "throb":9, 121 | "thrust":10, 122 | "random":11, 123 | "step":12, 124 | "training":13 125 | } 126 | 127 | ser = serial 128 | 129 | # device e.g. /dev/ttyUSB0 130 | def __init__(self, device='auto', baudrate=9600, timeout=0, verbose=True, dryrun=False, check_command=False, delay=0.05): 131 | if device == 'auto': 132 | if platform.system() == 'Darwin': device = '/dev/tty.usbserial-FTGD2KUC' 133 | if platform.system() == 'Linux': device = '/dev/ttyUSB0' 134 | if not dryrun: 135 | try: 136 | self.ser = serial.Serial( 137 | device, 138 | baudrate, 139 | timeout=timeout, 140 | bytesize=serial.EIGHTBITS, 141 | parity=serial.PARITY_NONE, 142 | stopbits=serial.STOPBITS_ONE) 143 | 144 | except Exception as e: 145 | print("Error opening serial device!") 146 | raise(e) 147 | 148 | if(self.ser.isOpen()): 149 | print("Opened serial device.") 150 | else: 151 | print('Running in dryrun mode.') 152 | 153 | self.status = EstimStatus() 154 | self.commErr = False 155 | self.verbose = verbose 156 | self.dryrun = dryrun 157 | self.check_command = check_command 158 | self.delay = delay 159 | 160 | #self.printStatus() 161 | 162 | def getStatus(self, formatted=True, check=None): 163 | ''' Gets the status from the 2B ''' 164 | if not self.dryrun: self.ser.flushInput() 165 | self.send("") 166 | replyString = self.recv() 167 | status_dict = self.status.parseReply(replyString) 168 | if self.verbose: 169 | print(replyString) 170 | if self.commErr or not status_dict: 171 | print('comm error', self.commErr, status_dict) 172 | sys.exit(1) 173 | return status_dict 174 | 175 | def recv(self): 176 | time.sleep(self.delay) 177 | if self.dryrun: 178 | replyString = "512:66:00:50:50:1:L:0:0" 179 | else: 180 | replyString = self.ser.readline() 181 | if self.verbose: 182 | print(replyString) 183 | return replyString 184 | 185 | def send(self, sendstring): 186 | self.status.update(sendstring) 187 | if self.verbose: 188 | print("send: {}".format(sendstring),) 189 | if self.dryrun: 190 | print 191 | else: 192 | command = sendstring+"\n\r" 193 | self.ser.write(command.encode()) 194 | print('(send complete).') 195 | time.sleep(self.delay) 196 | 197 | 198 | # Sets the output level [0, 99] of a channel. 199 | # serobj: the serial object to talk to 200 | # channel: one of A,B,C,D 201 | # level: the value between 0-99 to set. 202 | def setOutput(self, channel, level): 203 | print('setOutput: {} ({}): {} ({})'.format(channel, level, type(channel), type(level))) 204 | if channel in ['A', 'B']: 205 | if level < 0 or level > 100: 206 | print("Err: Invalid output level selected! A (or B) must be in range 0 to 100.") 207 | return False 208 | if channel in ['C', 'D']: 209 | if level < 2 or level > 100: 210 | print("Err: Invalid output level selected! C (or D) must be in range 2 to 100.") 211 | return False 212 | self.send(channel+str(level)) 213 | return True 214 | 215 | def setLow(self): 216 | self.send("L") 217 | 218 | def setHigh(self): 219 | self.send("H") 220 | 221 | def linkChannels(self): 222 | self.send("J1") 223 | 224 | def unlinkChannels(self): 225 | self.send("J0") 226 | 227 | def set(self, A=None, B=None, C=None, D=None): 228 | if A is not None: 229 | self.setOutput("A", A) 230 | if B is not None: 231 | self.setOutput("B", B) 232 | if C is not None: 233 | self.setOutput("C", C) 234 | if D is not None: 235 | self.setOutput("D", D) 236 | 237 | def setOutputs(self, A=None, B=None, kill_after=0): 238 | '''Sets levelA and levelB is they are specified above. 239 | Optionally sets the outputs to 0 after kill_after seconds.''' 240 | self.set(A=A, B=B) 241 | 242 | if kill_after > 0: 243 | time.sleep(kill_after) 244 | self.kill() 245 | 246 | def setFeelings(self, C=None, D=None): 247 | self.set(C=C, D=D) 248 | 249 | def kill(self): 250 | self.send("K") 251 | 252 | def reset(self): 253 | self.send("E") 254 | 255 | def setMode(self, modestring): 256 | modenum = self.modekey[modestring] 257 | if modenum < 0 or modenum > 13: 258 | print("Invalid mode") 259 | return False 260 | self.send("M"+str(modenum)) 261 | return True 262 | 263 | # Usage: 264 | #myestim = EStim('/dev/ttyUSB1') 265 | 266 | -------------------------------------------------------------------------------- /estim2b/estimsocket.py: -------------------------------------------------------------------------------- 1 | #! env python 2 | 3 | import socket 4 | import time 5 | 6 | class EstimSocket: 7 | 8 | def __init__(self, address="127.0.0.1", port=8089, verbose=True, udp=False): 9 | self._address = address 10 | self._port = port 11 | self._verbose = verbose 12 | self._udp = udp 13 | 14 | def open_socket(self): 15 | if self._udp: 16 | self.serversocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 17 | else: 18 | self.serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 19 | 20 | self.serversocket.bind(('', self._port)) 21 | if not self._udp: 22 | self.serversocket.listen(1) # become a server socket 23 | 24 | if self._udp: 25 | print('UDP Server started ({}:{})... waiting for data.'.format(self._address, self._port)) 26 | return None, None 27 | else: 28 | print('TCP Server started ({}:{})... waiting for client to connect.'.format(self._address, self._port)) 29 | conn, addr = self.serversocket.accept() 30 | print('New client {} connected.'.format(addr[0])) 31 | print('Running loop.') 32 | return conn, addr 33 | 34 | 35 | 36 | def start_server(self, max_incoming=1, callbacks=[], on_close=None, drop_packets=False): 37 | 38 | # handles the TCP vs UDP socket 39 | conn, addr = self.open_socket() 40 | 41 | while True: 42 | 43 | if self._udp: 44 | buf, addr = self.serversocket.recvfrom(4096) 45 | else: 46 | buf = conn.recv(4096) 47 | 48 | if len(buf) > 0: 49 | 50 | if self._verbose: 51 | print('Received {} from {}.'.format(buf, addr[0])) 52 | 53 | # at this point we've recv'd a buffer (buf) that contains data 54 | # it may contain multiple lines of data, if our server is processing 55 | # slower than the send rate of the client. To account for this we 56 | # split our buffer into lines, and run the callbacks sequentially 57 | # on those 58 | #print(type(buf)) 59 | #print(buf.decode('utf-8')) 60 | #print(str.splitlines(buf.decode('utf-8'))) 61 | buf = [buf.decode('utf-8')] 62 | if drop_packets: 63 | # only use the very last packet that was sent (faster) 64 | buf = buf[-1] 65 | 66 | for i, this_buf in enumerate(buf): 67 | # run all callbacks on each line in sequence 68 | 69 | for j, callback in enumerate(callbacks): 70 | # callbacks must accept two arguments: the buffer 71 | # that was sent, and the address of the device that 72 | # sent it. 73 | if self._verbose: 74 | print(' callback {} of {}...'.format(j, len(callbacks))) 75 | callback(this_buf, addr[0]) 76 | 77 | else: # len(buf) <= 0 78 | print('Client disconnected, will perform clean exit.') 79 | if on_close is not None: 80 | print('running cleanup...') 81 | on_close() 82 | break 83 | 84 | def client_connect(self): 85 | self.clientsocket = socket.socket( socket.AF_INET, socket.SOCK_STREAM ) 86 | self.clientsocket.connect( (self._address, self._port) ) 87 | 88 | def client_send(self, buf): 89 | if self._verbose: 90 | print('Sending {} to {}'.format(buf, self._address)) 91 | self.clientsocket.send(buf) 92 | 93 | 94 | if __name__ == "__main__": 95 | 96 | server = EstimSocket() 97 | server.start_server() 98 | 99 | -------------------------------------------------------------------------------- /estim2b/jolt.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | #from threading import Thread 4 | from queue import Queue 5 | from estim2b import Estim 6 | import _thread 7 | 8 | 9 | class Jolt: 10 | 11 | def __init__(self, e2b, verbose=False): 12 | # connection to the E-stim 2B unit 13 | self._e2b = e2b 14 | # print when jolting 15 | self._verbose = verbose 16 | # record of each jolt 17 | self._jolt_history = [] 18 | 19 | def test_grace_period(self, gtime): 20 | # returns true if we're still in the grace period 21 | if len(self._jolt_history) == 0: 22 | return False 23 | return self.time_since_last_jolt() < gtime 24 | 25 | def __call__(self, mode='throb', jtime=3.5, jpower=3, gtime=1): 26 | # first we see if we're within the grace time 27 | ltime = self.time_since_last_jolt() 28 | if self.test_grace_period(gtime):# + np.max([0, ltime-jtime])): 29 | if self._verbose: print('No jolt: within grace period') 30 | return False 31 | 32 | if mode is not None: 33 | self._e2b.setMode(mode) 34 | 35 | if self._verbose: print('Jolting.. ({} in last 60s)'.format(self.count_jolts(60))) 36 | #self._e2b.setOutputs(jpower, jpower, kill_after=jtime) 37 | _thread.start_new_thread(self._e2b.setOutputs, (jpower, jpower, jtime)) 38 | self._jolt_history += [time.time()] 39 | return True 40 | 41 | def time_since_last_jolt(self): 42 | if len(self._jolt_history) == 0: return np.inf 43 | return time.time() - self._jolt_history[-1] 44 | 45 | def count_jolts(self, t): 46 | needle = time.time() - t 47 | tarray = np.array(self._jolt_history) 48 | return len(tarray[tarray >= needle]) 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /examples/example.py: -------------------------------------------------------------------------------- 1 | import estim2b 2 | import time 3 | 4 | def jolt(e2b, jpower=None, jtime=None): 5 | if jpower is None: jpower = 2 6 | if jtime is None: jtime = 3 7 | print 'power={}; time={}'.format(jpower, jtime) 8 | e2b.setOutputs(jpower, jpower) 9 | time.sleep(jtime) 10 | e2b.kill() 11 | 12 | def jolt_simple(e2b, jpower=None, jtime=None): 13 | ''' Same as above, but simpler (uses kill_after keyword) ''' 14 | if jpower is None: jpower = 2 15 | if jtime is None: jtime = 3 16 | print 'power={}; time={}'.format(jpower, jtime) 17 | e2b.setOutputs(jpower, jpower, kill_after=jtime) 18 | 19 | # for Linux, device addr on Windows and Mac will be different. 20 | e2b = estim2b.Estim() 21 | #e2b.getStatus() 22 | e2b.setLow() 23 | 24 | # quick status update, tests connection etc 25 | print e2b.status() 26 | 27 | # change the mode and send a 2.5 second jolt 28 | e2b.setMode('throb') 29 | e2b.set(10, 10) 30 | time.sleep(3) 31 | e2b.set(0, 0) 32 | jolt(e2b, 3, 3) 33 | print e2b.status() 34 | 35 | e2b.setMode('bounce') 36 | e2b.setHigh() 37 | print e2b.status() 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/get_status.py: -------------------------------------------------------------------------------- 1 | import estim2b 2 | import time 3 | 4 | 5 | # for Linux, device addr on Windows and Mac will be different. 6 | e2b = estim2b.Estim() 7 | 8 | # quick status update, tests connection etc 9 | print e2b.status() 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/server_client_motion_example/README.md: -------------------------------------------------------------------------------- 1 | # Full working example of a motion sensor 2 | 3 | This is a fully-working example using a motion sensor with 2B. 4 | This uses two devices, a small client device which reads from an 5 | accelerometer and a server that is connected to the E-stim 2B powerbox. 6 | 7 | In my own set-up I have an ADXL345 attached to a Raspberry Pi Zero W. 8 | This is glued down to a small Li-ion battery, so that it can be a fully-remote 9 | sensor. On this device I run `client.py`, which just streams TCP packets to 10 | my server. 11 | 12 | The server may be a laptop, desktop or even another Raspberry Pi like device on the 13 | same network as the client. The server receives a stream of data from the client and 14 | processes this to figure out if the angle of the accelerometer has changed, or if 15 | there has been general movement. All the processing is done on the server, this makes 16 | it really easy to implement new clients (in fact your could use your mobile phone if 17 | you wrote an app to send TCP packets containing `"time,x,y,z"` to the server! 18 | 19 | # Usage 20 | 21 | First, on the server (the computer connected to the 2B) run, 22 | 23 | python ./start_estim2b_server.py 24 | 25 | Include any command line arguments you wish, for details see `python ./start_estim2b_server.py -h`. 26 | 27 | On the client (i.e. sensor) device run, 28 | 29 | client.py --address [IP address of the server] 30 | 31 | By default there is a short (10s) calibration period, this can be configured if necessary. 32 | 33 | -------------------------------------------------------------------------------- /examples/server_client_motion_example/analyse.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | import matplotlib.mlab as mlab 5 | import seaborn as sns; sns.set() 6 | 7 | df = pd.read_csv('xyz.log', header=None) 8 | 9 | sns.distplot(df[[2]]) 10 | 11 | mu = 0. 12 | #sigma = 0.0612143 13 | #sigma = 0.06822887 14 | sigma = 0.04287651 15 | x = np.linspace(mu - 3*sigma, mu + 3*sigma, 100) 16 | plt.plot(x, mlab.normpdf(x, mu, sigma)) 17 | 18 | plt.show() 19 | 20 | -------------------------------------------------------------------------------- /examples/server_client_motion_example/client.py: -------------------------------------------------------------------------------- 1 | from adxl345 import ADXL345 2 | import time 3 | import numpy as np 4 | from collections import deque 5 | from optparse import OptionParser 6 | from estim2b import EstimSocket 7 | 8 | 9 | parser = OptionParser() 10 | parser.add_option('--sleep', help='The time (s) to sleep between each send to the server.', 11 | dest='sleep', default=0.1, type=float) 12 | 13 | parser.add_option('--address', help='The IP address of the server', 14 | dest='address', default="192.168.0.5", type=str) 15 | 16 | parser.add_option('--port', help='The server port', 17 | dest='port', default=8089, type=int) 18 | 19 | opts, args = parser.parse_args() 20 | 21 | 22 | ''' 23 | By default the range is set to 2G, which is good for most applications. 24 | ''' 25 | adxl345 = ADXL345() 26 | 27 | 28 | def get_axes(): 29 | axes = adxl345.getAxes(gforce=True) 30 | ''' 31 | We send 4 pieces of information, the current time (which can be used to work out the lag) 32 | and the x, y, z readings from the accelerometer 33 | ''' 34 | x = "{},{},{},{}".format(time.time(), axes['x'], axes['y'], axes['z']) 35 | return x 36 | 37 | 38 | 39 | client = EstimSocket(address=opts.address, port=opts.port) 40 | client.client_connect() 41 | 42 | while True: 43 | 44 | time.sleep(opts.sleep) 45 | x = get_axes() 46 | client.client_send(x) 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/server_client_motion_example/motion.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from collections import deque 3 | 4 | class History: 5 | def __init__(self, max_length=100): 6 | self.counter = 0 7 | self.max_length = max_length 8 | self.hist = deque() 9 | 10 | def record(self, t, x, y, z): 11 | self.counter += 1 12 | self.hist.append( np.array([t, x, y, z]) ) 13 | if len(self.hist) > self.max_length: 14 | self.hist.popleft() 15 | 16 | def __len__(self): 17 | return len(self.hist) 18 | 19 | def get(self, pos): 20 | return self.hist[pos] 21 | 22 | def get_stats(self, low=0, high=None): 23 | means = np.mean(self.hist, axis=0) 24 | stds = np.std(self.hist, axis=0) 25 | return means[low:high], stds[low:high] 26 | 27 | def calc_velocities(self): 28 | d = np.diff(self.hist, axis=0) 29 | dt = d[:, 0] 30 | dt = np.reshape(dt, (-1, 1)) 31 | dxyz = d[:, 1:4] 32 | return dxyz/dt 33 | 34 | def calc_angles(self, pos=None): 35 | if pos is None: 36 | xyz = np.array(self.hist)[:, 1:4] 37 | else: 38 | xyz = np.array(self.hist)[pos, 1:4] 39 | xyz = np.reshape(xyz, (-1, 3)) # single entry 40 | pitch = np.arctan(xyz[:, 0] / np.sqrt(xyz[:, 1]**2 + xyz[:, 2]**2)) * 180. / np.pi 41 | roll = np.arctan(xyz[:, 1] / np.sqrt(xyz[:, 0]**2 + xyz[:, 2]**2)) * 180. / np.pi 42 | return pitch, roll 43 | 44 | def calc_velocity(self): 45 | txyz_this = self.get(-1) 46 | txyz_last = self.get(-2) 47 | d = txyz_this - txyz_last 48 | dt = d[0] 49 | dxyz = d[1:4] 50 | return dxyz/dt 51 | 52 | def calibrate_velocities(self, motionstd=None): 53 | vels = self.calc_velocities() 54 | self.vel_means, self.vel_stds = np.mean(vels, axis=0), np.std(vels, axis=0) 55 | if motionstd is not None: 56 | self.vel_stds = motionstd 57 | 58 | return self.vel_means, self.vel_stds 59 | 60 | def calibrate_angles(self, angstd=None): 61 | angles = self.calc_angles() 62 | self.angle_means, self.angle_stds = np.mean(angles, axis=1), np.std(angles, axis=1) 63 | if angstd is not None: 64 | self.angle_stds = np.array([angstd, angstd]) 65 | 66 | return self.angle_means, self.angle_stds 67 | 68 | def test_velocity_trigger(self, motion_tol): 69 | vel = self.calc_velocity() 70 | trigger, = np.where( np.abs(vel) - motion_tol*self.vel_stds > 0 ) 71 | if len(trigger) > 0: 72 | return True 73 | return False 74 | 75 | def test_angle_trigger(self, angle_tol): 76 | dangles = np.array(self.calc_angles(-1)).flatten() - self.angle_means 77 | trigger, = np.where( np.abs(dangles) > angle_tol*self.angle_stds ) 78 | if len(trigger) > 0: 79 | return True 80 | return False 81 | 82 | 83 | -------------------------------------------------------------------------------- /examples/server_client_motion_example/start_estim2b_server.py: -------------------------------------------------------------------------------- 1 | from optparse import OptionParser 2 | from estim2b import EstimSocket 3 | from estim2b import Estim 4 | from collections import deque 5 | import time 6 | import numpy as np 7 | from optparse import OptionParser 8 | from motion import History 9 | from estim2b import Jolt 10 | import sys 11 | 12 | parser = OptionParser() 13 | parser.add_option('--motion-tol', help='The tolerance on motion trigger events.', 14 | dest='motiontol', default=1.25, type=float) 15 | 16 | parser.add_option('--motion-std', help='Overides motion calibration.', 17 | dest='motionstd', type=float) 18 | 19 | parser.add_option('--angle-tol', help='The tolerance on angle trigger events.', 20 | dest='angtol', default=3.0, type=float) 21 | 22 | parser.add_option('--angle-std', help='Overides angle calibration.', 23 | dest='angstd', type=float) 24 | 25 | parser.add_option('--grace-time', help='Number of seconds to recover posture.', 26 | dest='gtime', default=3.0, type=float) 27 | 28 | parser.add_option('--jolt-time', help='Duration of the jolt', 29 | dest='jtime', default=3.5, type=float) 30 | 31 | parser.add_option('--jolt-power', help='Power of the jolt', 32 | dest='jpower', default=3, type=int) 33 | 34 | 35 | opts, args = parser.parse_args() 36 | 37 | if opts.angstd is not None and not opts.angtol == 1.0: 38 | print 39 | print '--angle-std has been set but --angle-tol is not 1' 40 | print 'Note that angle-std will be multiplied by angle-tol.' 41 | print '(when overiding angle-std like this it is usually best to set angle-tol to 1)' 42 | print 43 | 44 | print 'Read command-line arguments:' 45 | print opts 46 | print 47 | 48 | e2b = Estim('/dev/ttyUSB0') 49 | 50 | jolt = Jolt(e2b, verbose=True) 51 | 52 | hist = History() 53 | 54 | #outfile = open('xyz.log', 'w') 55 | 56 | def callback_history(buf, address): 57 | 58 | try: 59 | dat = buf.split(',') 60 | t = float(dat[0].strip()) 61 | x = float(dat[1].strip()) 62 | y = float(dat[2].strip()) 63 | z = float(dat[3].strip()) 64 | valid = True 65 | except: 66 | e = sys.exc_info() 67 | sys.stderr.write('received malformed buffer: {}\n'.format(buf)) 68 | sys.stderr.write(str(e)) 69 | valid = False 70 | 71 | if not valid: return False 72 | 73 | 74 | hist.record(t, x, y, z) 75 | 76 | 77 | def callback_velocity(buf, address): 78 | # Assumes callback_history ran before it 79 | 80 | if hist.counter == hist.max_length: 81 | vel_means, vel_stds = hist.calibrate_velocities(opts.motionstd) 82 | print 'Calibrated' 83 | print ' mean movement:', vel_means 84 | print ' stdd movement:', vel_stds 85 | print 86 | 87 | if hist.counter >= hist.max_length: 88 | if hist.test_velocity_trigger(opts.motiontol): 89 | print 'moved at step {}'.format(hist.counter) 90 | jolt(jtime=opts.jtime, jpower=opts.jpower, gtime=opts.gtime) 91 | 92 | return True 93 | 94 | 95 | 96 | def callback_level(buf, address): 97 | # Assumes callback_history ran before it 98 | 99 | if hist.counter == hist.max_length: 100 | angle_means, angle_stds = hist.calibrate_angles(opts.angstd) 101 | print 'Calibrated' 102 | print ' mean angles:', angle_means 103 | print ' stdd angles:', angle_stds 104 | print 105 | 106 | if hist.counter >= hist.max_length: 107 | if hist.test_angle_trigger(opts.angtol): 108 | print 'Unlevel at {}'.format(hist.counter) 109 | jolt(jtime=opts.jtime, jpower=opts.jpower, gtime=opts.gtime) 110 | 111 | return True 112 | 113 | 114 | 115 | def set_outputs_to_zero(): 116 | e2b.kill() 117 | 118 | server = EstimSocket(verbose=False) 119 | server.start_server(callbacks=[callback_history, callback_velocity, callback_level], on_close=e2b.kill) 120 | 121 | -------------------------------------------------------------------------------- /examples/server_client_passthru_example/README.md: -------------------------------------------------------------------------------- 1 | # Simple remote control of E-stim 2B using TCP sockets 2 | 3 | This demonstrates the EstimSocket module. If you just want to try it out first start the server 4 | on the computer that's physically connected to your E-stim 2B: 5 | 6 | python ./start_estim2b_server.py 7 | 8 | And then on another computer on the same network run the client: 9 | 10 | python ./client.py 11 | 12 | The client script will ask you for commands (which are documented in the E-stim 2B developers manual), 13 | the commands are sent to to the server using TCP sockets. 14 | 15 | # Doing custom things 16 | 17 | If you want create a custom client/server application, you shouldn't need to modify anything 18 | in the `EstimSocket` module. 19 | The `start_server` function takes two arguments, `callbacks` and `on_close`. 20 | 21 | - `callbacks`: a Python-list of functions that are run (in order) whenever the server received 22 | data from a client. Callback functions must take two arguments, the data sent (`buf`) and the address 23 | of the client that sent it (`address`) 24 | 25 | - `on_close`: a function that is run if the client disconnects for any reason. In most cases running 26 | the `kill()` function from `Estim` is a good idea to turn all outputs to zero. 27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/server_client_passthru_example/client.py: -------------------------------------------------------------------------------- 1 | import time 2 | from estim2b import EstimSocket 3 | 4 | client = EstimSocket() 5 | client.client_connect() 6 | 7 | while True: 8 | time.sleep(0.1) 9 | 10 | command = raw_input('Enter E-stim 2B compatible command: ') 11 | client.client_send('{}'.format(command)) 12 | 13 | -------------------------------------------------------------------------------- /examples/server_client_passthru_example/start_estim2b_server.py: -------------------------------------------------------------------------------- 1 | from optparse import OptionParser 2 | from estim2b import EstimSocket 3 | from estim2b import Estim 4 | 5 | e2b = Estim('/dev/ttyUSB0') 6 | 7 | def callback_command_passthru(buf, address): 8 | e2b.send(buf) 9 | 10 | def set_outputs_to_zero(): 11 | e2b.kill() 12 | 13 | server = EstimSocket() 14 | server.start_server(callbacks=[callback_command_passthru], on_close=e2b.kill) 15 | 16 | -------------------------------------------------------------------------------- /examples/set_output.py: -------------------------------------------------------------------------------- 1 | import estim2b 2 | import time 3 | 4 | 5 | # for Linux, device addr on Windows and Mac will be different. 6 | e2b = estim2b.Estim('/dev/ttyUSB0') 7 | e2b.unlinkChannels() 8 | e2b.kill() # start from zero 9 | 10 | ''' 11 | Set channel A to 2% for 5 seconds 12 | ''' 13 | e2b.setOutput(channel='A', level=2) 14 | time.sleep(5) 15 | e2b.kill() 16 | 17 | ''' 18 | Set channel B to 10% for 5 seconds 19 | ''' 20 | e2b.setOutputs(levelB=10) 21 | time.sleep(5) 22 | 23 | ''' 24 | Set both channel A and B to 20% for 5 seconds 25 | ''' 26 | e2b.setOutputs(20, 20) 27 | time.sleep(5) 28 | 29 | e2b.status() 30 | e2b.kill() 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/udp_motion_example/README.md: -------------------------------------------------------------------------------- 1 | # Full working example of a motion sensor using UDP 2 | 3 | If you want to get up and running quickly use the [Wireless IMU](https://play.google.com/store/apps/details?id=org.zwiener.wimu&hl=en_GB) Android app from the Google Play store. 4 | 5 | This is a fully-working example using a motion sensor with 2B. 6 | This uses two devices, a small client device which reads from an 7 | accelerometer and a server that is connected to the E-stim 2B powerbox. 8 | 9 | This has been tested using the Wireless IMU Android app. 10 | 11 | The server may be a laptop, desktop or even a Raspberry Pi device on the 12 | same network as the client. The server receives a stream of data from the client and 13 | processes this to figure out if the angle of the accelerometer has changed, or if 14 | there has been general movement. All the processing is done on the server, this makes 15 | it really easy to implement new clients. 16 | 17 | # Usage 18 | 19 | First, on the server (the computer connected to the 2B) run, 20 | 21 | python ./start_estim2b_server.py 22 | 23 | Include any command line arguments you wish, for details see `python ./start_estim2b_server.py -h`. 24 | 25 | On your Android mobile phone start the Wireless IMU app, point it to the IP address of your server device 26 | and set the port to 8089 (by default). You probably want to set the update rate to "medium". 27 | When you're ready press "Activate Wireless Stream" in the app. 28 | 29 | -------------------------------------------------------------------------------- /examples/udp_motion_example/motion.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from collections import deque 4 | 5 | class EMA: 6 | def __init__(self, alpha): 7 | self.alpha = alpha 8 | self.ema = None 9 | self.emv = 0.0 10 | 11 | def __call__(self, value): 12 | delta = value 13 | if self.ema is not None: delta -= self.ema 14 | 15 | self.emv = (1.-self.alpha) * (self.emv + self.alpha*delta**2) 16 | 17 | if self.ema is None: 18 | self.ema = float(value) 19 | else: 20 | #self.ema = self.alpha * self.ema + (1.-self.alpha) * value 21 | self.ema = self.ema + self.alpha * delta 22 | 23 | print(self.ema, self.emv) 24 | return self.ema, self.emv 25 | 26 | def get_ema(self): 27 | if self.ema is None: return 0 28 | return self.ema 29 | 30 | 31 | class History: 32 | def __init__(self, max_length=1000): 33 | self.counter = 0 34 | self.max_length = max_length 35 | self.hist = deque() 36 | self.vhist = deque() 37 | self.shist = deque() 38 | self.ahist = deque() 39 | 40 | self.speed_ema = EMA(alpha=0.9) 41 | 42 | def record(self, t, x, y, z): 43 | self.counter += 1 44 | self.hist.append( np.array([t, x, y, z]) ) 45 | 46 | v = self.calc_velocity() 47 | self.vhist.append( np.array([t, v[0], v[1], v[2]]) ) 48 | 49 | s = self.calc_speed() 50 | #s, sv = self.speed_ema(s) 51 | #self.speed_means = s 52 | #self.speed_stds = np.sqrt(sv) 53 | self.shist.append( np.array([t, s]) ) 54 | 55 | pitch, roll = self.calc_angles(-1) 56 | self.ahist.append( np.array([t, pitch[0], roll[0]]) ) 57 | 58 | if len(self.hist) > self.max_length: 59 | self.hist.popleft() 60 | if len(self.vhist) > self.max_length: 61 | self.vhist.popleft() 62 | if len(self.shist) > self.max_length: 63 | self.shist.popleft() 64 | if len(self.ahist) > self.max_length: 65 | self.ahist.popleft() 66 | 67 | def __len__(self): 68 | return len(self.hist) 69 | 70 | def get(self, pos): 71 | return self.hist[pos] 72 | 73 | def get_stats(self, low=0, high=None): 74 | means = np.mean(self.hist, axis=0) 75 | stds = np.std(self.hist, axis=0) 76 | return means[low:high], stds[low:high] 77 | 78 | def calc_velocities(self): 79 | d = np.diff(self.hist, axis=0) 80 | dt = d[:, 0] 81 | dt = np.reshape(dt, (-1, 1)) 82 | dxyz = d[:, 1:4] 83 | return dxyz/dt 84 | 85 | def calc_speeds(self): 86 | vels = self.calc_velocities() 87 | return np.sqrt(np.sum(vels**2, axis=-1)) 88 | 89 | def calc_angles(self, pos=None): 90 | if pos is None: 91 | xyz = np.array(self.hist)[:, 1:4] 92 | else: 93 | xyz = np.array(self.hist)[pos, 1:4] 94 | xyz = np.reshape(xyz, (-1, 3)) # single entry 95 | pitch = np.arctan(xyz[:, 0] / np.sqrt(xyz[:, 1]**2 + xyz[:, 2]**2)) * 180. / np.pi 96 | roll = np.arctan(xyz[:, 1] / np.sqrt(xyz[:, 0]**2 + xyz[:, 2]**2)) * 180. / np.pi 97 | return pitch, roll 98 | 99 | def calc_velocity(self): 100 | try: 101 | txyz_this = self.get(-1) 102 | txyz_last = self.get(-2) 103 | except IndexError: 104 | return np.zeros(3, dtype=float) 105 | d = txyz_this - txyz_last 106 | dt = d[0] 107 | dxyz = d[1:4] 108 | return dxyz/dt 109 | 110 | def calc_speed(self): 111 | vels = self.calc_velocity() 112 | return np.sqrt(np.sum(vels**2, axis=-1)) 113 | 114 | def calibrate_velocities(self, motionstd=None): 115 | vels = self.calc_velocities() 116 | self.vel_means, self.vel_stds = np.mean(vels, axis=0), np.std(vels, axis=0) 117 | if motionstd is not None: 118 | self.vel_stds = motionstd 119 | 120 | return self.vel_means, self.vel_stds 121 | 122 | def calibrate_speeds(self, motionstd=None): 123 | speeds = self.calc_speeds() 124 | ##df = pd.DataFrame(speeds, columns=['vel']) 125 | ##ema = pd.ewma(df, alpha=0.5) 126 | ##self.speed_means = ema.mean().values[-1] 127 | ##self.speed_stds = ema.std().values[-1] 128 | self.speed_means, self.speed_stds = np.mean(speeds, axis=0), np.std(speeds, axis=0) 129 | if self.speed_stds < 1.5: self.speed_stds = 10.0 130 | 131 | if motionstd is not None: 132 | self.speed_stds = motionstd 133 | 134 | return self.speed_means, self.speed_stds 135 | 136 | def calibrate_angles(self, angstd=None): 137 | angles = self.calc_angles() 138 | self.angle_means, self.angle_stds = np.mean(angles, axis=1), np.std(angles, axis=1) 139 | if self.angle_stds[0] < 2.0: self.angle_stds[0] = 0.75 140 | if self.angle_stds[1] < 2.0: self.angle_stds[1] = 0.75 141 | if angstd is not None: 142 | self.angle_stds = np.array([angstd, angstd]) 143 | 144 | return self.angle_means, self.angle_stds 145 | 146 | def test_velocity_trigger(self, motion_tol): 147 | vel = self.calc_velocity() 148 | trigger, = np.where( np.abs(vel) - motion_tol*self.vel_stds > 0 ) 149 | if len(trigger) > 0: 150 | return True 151 | return False 152 | 153 | def test_speed_trigger(self, motion_tol): 154 | speed = self.calc_speed() 155 | trigger, = np.where( np.abs(speed) - motion_tol*self.speed_stds > 0 ) 156 | if len(trigger) > 0: 157 | return True 158 | return False 159 | 160 | def test_angle_trigger(self, angle_tol): 161 | dangles = np.array(self.calc_angles(-1)).flatten() - self.angle_means 162 | trigger, = np.where( np.abs(dangles) > angle_tol*self.angle_stds ) 163 | if len(trigger) > 0: 164 | return True 165 | return False 166 | 167 | 168 | -------------------------------------------------------------------------------- /examples/udp_motion_example/recv_udp.py: -------------------------------------------------------------------------------- 1 | import socket, traceback 2 | from motion import History 3 | 4 | hist = History() 5 | 6 | 7 | def callback_history(buf, address): 8 | 9 | try: 10 | dat = buf.split(',') 11 | t = float(dat[0].strip()) 12 | x = float(dat[2].strip()) 13 | y = float(dat[3].strip()) 14 | z = float(dat[4].strip()) 15 | valid = True 16 | except: 17 | sys.stderr.write('received malformed buffer: {}'.format(buf)) 18 | valid = False 19 | 20 | if not valid: return False 21 | 22 | hist.record(t, x, y, z) 23 | print address[0], 24 | print hist.calc_velocity(), 25 | print hist.calc_angles(-1) 26 | 27 | 28 | host = '' 29 | port = 5555 30 | 31 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 32 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 33 | s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 34 | s.bind((host, port)) 35 | 36 | while 1: 37 | try: 38 | message, address = s.recvfrom(8192) 39 | #print message 40 | callback_history(message, address) 41 | except (KeyboardInterrupt, SystemExit): 42 | raise 43 | except: 44 | traceback.print_exc() 45 | -------------------------------------------------------------------------------- /examples/udp_motion_example/start_estim2b_server.py: -------------------------------------------------------------------------------- 1 | from optparse import OptionParser 2 | from estim2b import EstimSocket 3 | from estim2b import Estim 4 | from collections import deque 5 | import time 6 | import numpy as np 7 | from optparse import OptionParser 8 | from motion import History 9 | from estim2b import Jolt 10 | import sys 11 | 12 | parser = OptionParser() 13 | parser.add_option('--motion-tol', help='The tolerance on motion trigger events.', 14 | dest='motiontol', default=1.25, type=float) 15 | 16 | parser.add_option('--motion-std', help='Overides motion calibration.', 17 | dest='motionstd', type=float) 18 | 19 | parser.add_option('--angle-tol', help='The tolerance on angle trigger events.', 20 | dest='angtol', default=3.0, type=float) 21 | 22 | parser.add_option('--angle-std', help='Overides angle calibration.', 23 | dest='angstd', type=float) 24 | 25 | parser.add_option('--grace-time', help='Number of seconds to recover posture.', 26 | dest='gtime', default=3.0, type=float) 27 | 28 | parser.add_option('--jolt-time', help='Duration of the jolt', 29 | dest='jtime', default=3.5, type=float) 30 | 31 | parser.add_option('--jolt-power', help='Power of the jolt', 32 | dest='jpower', default=3, type=int) 33 | 34 | parser.add_option('--device', help='COM device of Estim 2B hardware', 35 | dest='device', default='auto', type=str) 36 | 37 | opts, args = parser.parse_args() 38 | 39 | if opts.angstd is not None and not opts.angtol == 1.0: 40 | print() 41 | print('--angle-std has been set but --angle-tol is not 1') 42 | print('Note that angle-std will be multiplied by angle-tol.') 43 | print('(when overiding angle-std like this it is usually best to set angle-tol to 1)') 44 | print() 45 | 46 | print('Read command-line arguments:') 47 | print(opts) 48 | print() 49 | 50 | e2b = Estim(opts.device) 51 | 52 | jolt = Jolt(e2b, verbose=True) 53 | 54 | hist = History() 55 | 56 | #outfile = open('xyz.log', 'w') 57 | 58 | def callback_history(buf, address): 59 | 60 | try: 61 | dat = buf.split(',') 62 | t = float(dat[0].strip()) 63 | x = float(dat[2].strip()) 64 | y = float(dat[3].strip()) 65 | z = float(dat[4].strip()) 66 | valid = True 67 | except: 68 | sys.stderr.write('received malformed buffer: {}'.format(buf)) 69 | valid = False 70 | 71 | if not valid: return False 72 | 73 | hist.record(t, x, y, z) 74 | 75 | 76 | def callback_velocity(buf, address): 77 | # Assumes callback_history ran before it 78 | 79 | if hist.counter == hist.max_length: 80 | vel_means, vel_stds = hist.calibrate_velocities(opts.motionstd) 81 | print('Calibrated') 82 | print(' mean movement:', vel_means) 83 | print(' stdd movement:', vel_stds) 84 | print() 85 | 86 | if hist.counter >= hist.max_length: 87 | if hist.test_velocity_trigger(opts.motiontol): 88 | print('moved at step {}'.format(hist.counter)) 89 | jolt(jtime=opts.jtime, jpower=opts.jpower, gtime=opts.gtime) 90 | 91 | return True 92 | 93 | 94 | 95 | def callback_level(buf, address): 96 | # Assumes callback_history ran before it 97 | 98 | if hist.counter == hist.max_length: 99 | angle_means, angle_stds = hist.calibrate_angles(opts.angstd) 100 | print('Calibrated') 101 | print(' mean angles:', angle_means) 102 | print(' stdd angles:', angle_stds) 103 | print() 104 | 105 | if hist.counter >= hist.max_length: 106 | if hist.test_angle_trigger(opts.angtol): 107 | print('Unlevel at {}'.format(hist.counter)) 108 | jolt(jtime=opts.jtime, jpower=opts.jpower, gtime=opts.gtime) 109 | 110 | return True 111 | 112 | 113 | 114 | server = EstimSocket(verbose=False, udp=True) 115 | server.start_server(callbacks=[callback_history, callback_velocity, callback_level], on_close=e2b.kill) 116 | 117 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from setuptools import find_packages 3 | 4 | 5 | setup(name='estim2b', 6 | version='0.2', 7 | description='Unofficial Python API for E-stim 2B', 8 | author='Fred Hatt', 9 | author_email='fred.r.hatt@gmail.com', 10 | url='https://github.com/fredhatt/estim2b', 11 | license='MIT', 12 | install_requires=['numpy', 'pyserial'], 13 | packages=find_packages()) 14 | --------------------------------------------------------------------------------