├── .gitignore ├── LICENSE ├── Makefile ├── NetFT └── __init__.py ├── README.md ├── bin └── NetFT ├── doc ├── conf.py └── index.rst └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | build 4 | dist 5 | *.bak 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Cameron Devine 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build upload clean doc 2 | 3 | build: 4 | python setup.py sdist bdist_wheel --universal 5 | 6 | upload: 7 | twine upload dist/* 8 | 9 | clean: 10 | rm -rf *.egg-info build dist doc/build 11 | 12 | doc: 13 | sphinx-build doc doc/build 14 | -------------------------------------------------------------------------------- /NetFT/__init__.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import struct 3 | from threading import Thread 4 | from time import sleep 5 | 6 | class Sensor: 7 | '''The class interface for an ATI Force/Torque sensor. 8 | 9 | This class contains all the functions necessary to communicate 10 | with an ATI Force/Torque sensor with a Net F/T interface 11 | using RDT. 12 | ''' 13 | def __init__(self, ip): 14 | '''Start the sensor interface 15 | 16 | This function initializes the class and opens the socket for the 17 | sensor. 18 | 19 | Args: 20 | ip (str): The IP address of the Net F/T box. 21 | ''' 22 | self.ip = ip 23 | self.port = 49152 24 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 25 | self.sock.connect((ip, self.port)) 26 | self.mean = [0] * 6 27 | self.stream = False 28 | 29 | def send(self, command, count = 0): 30 | '''Send a given command to the Net F/T box with specified sample count. 31 | 32 | This function sends the given RDT command to the Net F/T box, along with 33 | the specified sample count, if needed. 34 | 35 | Args: 36 | command (int): The RDT command. 37 | count (int, optional): The sample count to send. Defaults to 0. 38 | ''' 39 | header = 0x1234 40 | message = struct.pack('!HHI', header, command, count) 41 | self.sock.send(message) 42 | 43 | def receive(self): 44 | '''Receives and unpacks a response from the Net F/T box. 45 | 46 | This function receives and unpacks an RDT response from the Net F/T 47 | box and saves it to the data class attribute. 48 | 49 | Returns: 50 | list of float: The force and torque values received. The first three 51 | values are the forces recorded, and the last three are the measured 52 | torques. 53 | ''' 54 | rawdata = self.sock.recv(1024) 55 | data = struct.unpack('!IIIiiiiii', rawdata)[3:] 56 | self.data = [data[i] - self.mean[i] for i in range(6)] 57 | return self.data 58 | 59 | def tare(self, n = 10): 60 | '''Tare the sensor. 61 | 62 | This function takes a given number of readings from the sensor 63 | and averages them. This mean is then stored and subtracted from 64 | all future measurements. 65 | 66 | Args: 67 | n (int, optional): The number of samples to use in the mean. 68 | Defaults to 10. 69 | 70 | Returns: 71 | list of float: The mean values calculated. 72 | ''' 73 | self.mean = [0] * 6 74 | self.getMeasurements(n = n) 75 | mean = [0] * 6 76 | for i in range(n): 77 | self.receive() 78 | for i in range(6): 79 | mean[i] += self.measurement()[i] / float(n) 80 | self.mean = mean 81 | return mean 82 | 83 | def zero(self): 84 | '''Remove the mean found with `tare` to start receiving raw sensor values.''' 85 | self.mean = [0] * 6 86 | 87 | def receiveHandler(self): 88 | '''A handler to receive and store data.''' 89 | while self.stream: 90 | self.receive() 91 | 92 | def getMeasurement(self): 93 | '''Get a single measurement from the sensor 94 | 95 | Request a single measurement from the sensor and return it. If 96 | The sensor is currently streaming, started by running `startStreaming`, 97 | then this function will simply return the most recently returned value. 98 | 99 | Returns: 100 | list of float: The force and torque values received. The first three 101 | values are the forces recorded, and the last three are the measured 102 | torques. 103 | ''' 104 | self.getMeasurements(1) 105 | self.receive() 106 | return self.data 107 | 108 | def measurement(self): 109 | '''Get the most recent force/torque measurement 110 | 111 | Returns: 112 | list of float: The force and torque values received. The first three 113 | values are the forces recorded, and the last three are the measured 114 | torques. 115 | ''' 116 | return self.data 117 | 118 | def getForce(self): 119 | '''Get a single force measurement from the sensor 120 | 121 | Request a single measurement from the sensor and return it. 122 | 123 | Returns: 124 | list of float: The force values received. 125 | ''' 126 | return self.getMeasurement()[:3] 127 | 128 | def force(self): 129 | '''Get the most recent force measurement 130 | 131 | Returns: 132 | list of float: The force values received. 133 | ''' 134 | return self.measurement()[:3] 135 | 136 | def getTorque(self): 137 | '''Get a single torque measurement from the sensor 138 | 139 | Request a single measurement from the sensor and return it. 140 | 141 | Returns: 142 | list of float: The torque values received. 143 | ''' 144 | return self.getMeasurement()[3:] 145 | 146 | def torque(self): 147 | '''Get the most recent torque measurement 148 | 149 | Returns: 150 | list of float: The torque values received. 151 | ''' 152 | return self.measurement()[3:] 153 | 154 | def startStreaming(self, handler = True): 155 | '''Start streaming data continuously 156 | 157 | This function commands the Net F/T box to start sending data continuously. 158 | By default this also starts a new thread with a handler to save all data 159 | points coming in. These data points can still be accessed with `measurement`, 160 | `force`, and `torque`. This handler can also be disabled and measurements 161 | can be received manually using the `receive` function. 162 | 163 | Args: 164 | handler (bool, optional): If True start the handler which saves data to be 165 | used with `measurement`, `force`, and `torque`. If False the 166 | measurements must be received manually. Defaults to True. 167 | ''' 168 | self.getMeasurements(0) 169 | if handler: 170 | self.stream = True 171 | self.thread = Thread(target = self.receiveHandler) 172 | self.thread.daemon = True 173 | self.thread.start() 174 | 175 | def getMeasurements(self, n): 176 | '''Request a given number of samples from the sensor 177 | 178 | This function requests a given number of samples from the sensor. These 179 | measurements must be received manually using the `receive` function. 180 | 181 | Args: 182 | n (int): The number of samples to request. 183 | ''' 184 | self.send(2, count = n) 185 | 186 | def stopStreaming(self): 187 | '''Stop streaming data continuously 188 | 189 | This function stops the sensor from streaming continuously as started using 190 | `startStreaming`. 191 | ''' 192 | self.stream = False 193 | sleep(0.1) 194 | self.send(0) 195 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NetFT 2 | 3 | [![Documentation Status](https://readthedocs.org/projects/netft/badge/?version=latest)](https://netft.readthedocs.io/en/latest/?badge=latest) 4 | [![PyPI](https://img.shields.io/pypi/v/NetFT.svg)](https://pypi.org/project/NetFT/) 5 | 6 | ## Please Note: 7 | **This project has moved to [GitHub](https://github.com/CameronDevine/NetFT). All future work will be stored there.** 8 | 9 | ## Introduction 10 | 11 | This is a Python API for RDT communication with ATI Force/Torque sensors using Net-F/T interface boxes. This library supports requesting single measurements, streaming measurements, and taring the sensor. 12 | 13 | ## Installation 14 | 15 | This package can be installed using `pip install NetFT`. The current development version can be installed by cloning the git repository and running `python setup.py install`. 16 | 17 | ## Use 18 | 19 | A command line interface is included with the package. It can be run by simply typing `NetFT` in a terminal. The documentation for this can be viewed by running `NetFT --help`. 20 | -------------------------------------------------------------------------------- /bin/NetFT: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import print_function 4 | import NetFT 5 | import argparse 6 | 7 | parser = argparse.ArgumentParser(description = "Read data from ATI NetFT sensors.") 8 | parser.add_argument('ip', 9 | metavar = 'ip address', 10 | type = str, 11 | help = "The IP address of the sensor") 12 | parser.add_argument('-t', '--torque', 13 | dest = 'torque', 14 | action = 'store_true', 15 | help = "Show torque values") 16 | parser.add_argument('-f', '--force', 17 | dest = 'force', 18 | action = 'store_true', 19 | help = "Show force values") 20 | parser.add_argument('-s', '--samples', 21 | dest = 'samples', 22 | type = int, 23 | default = False, 24 | metavar = 'N', 25 | help = "The number of samples to print") 26 | parser.add_argument('-m', '--mean', 27 | dest = 'mean', 28 | type = int, 29 | nargs = '?', 30 | const = 10, 31 | default = False, 32 | metavar = 'N', 33 | help = "Tare the sensor with N datapoints before showing data (default 10)") 34 | parser.add_argument('-c', '--continuous', 35 | dest = 'continuous', 36 | action = 'store_true', 37 | help = "Print data continuously") 38 | args = parser.parse_args() 39 | 40 | args.force, args.torque = \ 41 | args.force or not args.force and not args.torque, \ 42 | args.torque or not args.force and not args.torque 43 | 44 | sensor = NetFT.Sensor(args.ip) 45 | 46 | if args.mean: 47 | sensor.tare(args.mean) 48 | 49 | if args.force and args.torque: 50 | data = sensor.measurement 51 | get = sensor.getMeasurement 52 | elif args.force: 53 | data = sensor.force 54 | get = sensor.getForce 55 | else: 56 | data = sensor.torque 57 | get = sensor.getTorque 58 | 59 | if args.samples: 60 | sensor.getMeasurements(args.samples) 61 | for i in range(args.samples): 62 | sensor.receive() 63 | print(data()) 64 | elif args.continuous: 65 | sensor.startStreaming(False) 66 | try: 67 | while True: 68 | sensor.receive() 69 | print(data()) 70 | except KeyboardInterrupt: 71 | print('Exiting') 72 | else: 73 | print(get()) 74 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Check if m2r is installed, if not install it ---------------------------- 10 | 11 | try: 12 | import m2r 13 | del m2r 14 | except ImportError: 15 | import pip 16 | pip.main(['install', 'm2r']) 17 | 18 | # -- Path setup -------------------------------------------------------------- 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | # 24 | # import os 25 | # import sys 26 | # sys.path.insert(0, os.path.abspath('.')) 27 | 28 | import os 29 | import sys 30 | sys.path.insert(0, os.path.abspath('..')) 31 | 32 | print sys.path 33 | 34 | 35 | # -- Project information ----------------------------------------------------- 36 | 37 | project = u'NetFT' 38 | copyright = u'2018, Cameron Devine' 39 | author = u'Cameron Devine' 40 | 41 | # The short X.Y version 42 | #version = u'' 43 | # The full version, including alpha/beta/rc tags 44 | #release = u'0.6' 45 | 46 | 47 | # -- General configuration --------------------------------------------------- 48 | 49 | # If your documentation needs a minimal Sphinx version, state it here. 50 | # 51 | # needs_sphinx = '1.0' 52 | 53 | # Add any Sphinx extension module names here, as strings. They can be 54 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 55 | # ones. 56 | extensions = [ 57 | 'sphinx.ext.autodoc', 58 | 'sphinx.ext.intersphinx', 59 | 'sphinx.ext.mathjax', 60 | 'sphinx.ext.viewcode', 61 | 'sphinx.ext.napoleon', 62 | 'm2r' 63 | ] 64 | 65 | # Add any paths that contain templates here, relative to this directory. 66 | #templates_path = ['_templates'] 67 | 68 | # The suffix(es) of source filenames. 69 | # You can specify multiple suffix as a list of string: 70 | # 71 | source_suffix = ['.rst', '.md'] 72 | 73 | #source_parsers = { 74 | # '.md': 'recommonmark.parser.CommonMarkParser', 75 | #} 76 | 77 | # The master toctree document. 78 | master_doc = 'index' 79 | 80 | # The language for content autogenerated by Sphinx. Refer to documentation 81 | # for a list of supported languages. 82 | # 83 | # This is also used if you do content translation via gettext catalogs. 84 | # Usually you set "language" from the command line for these cases. 85 | language = None 86 | 87 | # List of patterns, relative to source directory, that match files and 88 | # directories to ignore when looking for source files. 89 | # This pattern also affects html_static_path and html_extra_path. 90 | exclude_patterns = [] 91 | 92 | # The name of the Pygments (syntax highlighting) style to use. 93 | pygments_style = None 94 | 95 | 96 | # -- Options for HTML output ------------------------------------------------- 97 | 98 | # The theme to use for HTML and HTML Help pages. See the documentation for 99 | # a list of builtin themes. 100 | # 101 | #html_theme = 'alabaster' 102 | 103 | # Theme options are theme-specific and customize the look and feel of a theme 104 | # further. For a list of options available for each theme, see the 105 | # documentation. 106 | # 107 | # html_theme_options = {} 108 | 109 | # Add any paths that contain custom static files (such as style sheets) here, 110 | # relative to this directory. They are copied after the builtin static files, 111 | # so a file named "default.css" will overwrite the builtin "default.css". 112 | #html_static_path = ['_static'] 113 | 114 | # Custom sidebar templates, must be a dictionary that maps document names 115 | # to template names. 116 | # 117 | # The default sidebars (for documents that don't match any pattern) are 118 | # defined by theme itself. Builtin themes are using these templates by 119 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 120 | # 'searchbox.html']``. 121 | # 122 | # html_sidebars = {} 123 | 124 | 125 | # -- Options for HTMLHelp output --------------------------------------------- 126 | 127 | # Output file base name for HTML help builder. 128 | htmlhelp_basename = 'NetFTdoc' 129 | 130 | 131 | # -- Options for LaTeX output ------------------------------------------------ 132 | 133 | latex_elements = { 134 | # The paper size ('letterpaper' or 'a4paper'). 135 | # 136 | # 'papersize': 'letterpaper', 137 | 138 | # The font size ('10pt', '11pt' or '12pt'). 139 | # 140 | # 'pointsize': '10pt', 141 | 142 | # Additional stuff for the LaTeX preamble. 143 | # 144 | # 'preamble': '', 145 | 146 | # Latex figure (float) alignment 147 | # 148 | # 'figure_align': 'htbp', 149 | } 150 | 151 | # Grouping the document tree into LaTeX files. List of tuples 152 | # (source start file, target name, title, 153 | # author, documentclass [howto, manual, or own class]). 154 | latex_documents = [ 155 | (master_doc, 'NetFT.tex', u'NetFT Documentation', 156 | u'Cameron Devine', 'manual'), 157 | ] 158 | 159 | 160 | # -- Options for manual page output ------------------------------------------ 161 | 162 | # One entry per manual page. List of tuples 163 | # (source start file, name, description, authors, manual section). 164 | man_pages = [ 165 | (master_doc, 'netft', u'NetFT Documentation', 166 | [author], 1) 167 | ] 168 | 169 | 170 | # -- Options for Texinfo output ---------------------------------------------- 171 | 172 | # Grouping the document tree into Texinfo files. List of tuples 173 | # (source start file, target name, title, author, 174 | # dir menu entry, description, category) 175 | texinfo_documents = [ 176 | (master_doc, 'NetFT', u'NetFT Documentation', 177 | author, 'NetFT', 'One line description of project.', 178 | 'Miscellaneous'), 179 | ] 180 | 181 | 182 | # -- Options for Epub output ------------------------------------------------- 183 | 184 | # Bibliographic Dublin Core info. 185 | epub_title = project 186 | 187 | # The unique identifier of the text. This can be a ISBN number 188 | # or the project homepage. 189 | # 190 | # epub_identifier = '' 191 | 192 | # A unique identification for the text. 193 | # 194 | # epub_uid = '' 195 | 196 | # A list of files that should not be packed into the epub file. 197 | epub_exclude_files = ['search.html'] 198 | 199 | 200 | # -- Extension configuration ------------------------------------------------- 201 | 202 | # -- Options for intersphinx extension --------------------------------------- 203 | 204 | # Example configuration for intersphinx: refer to the Python standard library. 205 | intersphinx_mapping = {'https://docs.python.org/': None} 206 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 2 3 | :caption: Contents: 4 | 5 | .. mdinclude:: ../README.md 6 | 7 | Documentation 8 | ------------- 9 | 10 | .. automodule:: NetFT 11 | :members: 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open('README.md', 'r') as f: 4 | long_description = f.read() 5 | 6 | setup( 7 | name = "NetFT", 8 | version = "2.0.1", 9 | packages = find_packages(), 10 | author = "Cameron Devine", 11 | author_email = "camdev@uw.edu", 12 | description = "A Python library for reading data from ATI Force/Torque sensors with a Net F/T interface box.", 13 | long_description = long_description, 14 | long_description_content_type = 'text/markdown', 15 | license = "BSD", 16 | keywords = "Robotics Force Torque Sensor NetFT ATI Data Logging", 17 | url = "https://github.com/CameronDevine/NetFT", 18 | project_urls={"Documentation": "https://netft.readthedocs.io/en/latest/"}, 19 | scripts = ['bin/NetFT'] 20 | ) 21 | --------------------------------------------------------------------------------