├── pyproject.toml ├── circuits ├── 5X_ReSkin.Zip ├── 5X_ReSkin_BOM.xlsx └── README.md ├── parts ├── middle_mold.STL ├── top_holder.STL └── bottom_holder.STL ├── reskin_sensor ├── __init__.py ├── sensor.py └── sensor_proc.py ├── visualizations ├── images │ └── 3D.PNG ├── pygame_demo.py └── heatmap.py ├── .gitmodules ├── setup.py ├── LICENSE ├── arduino ├── README.md ├── 5X_binary_burst_stream │ └── 5X_binary_burst_stream.ino └── 5X_burst_stream │ └── 5X_burst_stream.ino ├── tests ├── sensor_test.py └── sensor_proc_test.py ├── README.md └── .gitignore /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /circuits/5X_ReSkin.Zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raunaqbhirangi/reskin_sensor/HEAD/circuits/5X_ReSkin.Zip -------------------------------------------------------------------------------- /parts/middle_mold.STL: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raunaqbhirangi/reskin_sensor/HEAD/parts/middle_mold.STL -------------------------------------------------------------------------------- /parts/top_holder.STL: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raunaqbhirangi/reskin_sensor/HEAD/parts/top_holder.STL -------------------------------------------------------------------------------- /parts/bottom_holder.STL: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raunaqbhirangi/reskin_sensor/HEAD/parts/bottom_holder.STL -------------------------------------------------------------------------------- /circuits/5X_ReSkin_BOM.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raunaqbhirangi/reskin_sensor/HEAD/circuits/5X_ReSkin_BOM.xlsx -------------------------------------------------------------------------------- /reskin_sensor/__init__.py: -------------------------------------------------------------------------------- 1 | from .sensor import ReSkinBase, ReSkinDummy 2 | from .sensor_proc import ReSkinProcess 3 | -------------------------------------------------------------------------------- /visualizations/images/3D.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raunaqbhirangi/reskin_sensor/HEAD/visualizations/images/3D.PNG -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "arduino/arduino-MLX90393"] 2 | path = arduino/arduino-MLX90393 3 | url = https://github.com/tesshellebrekers/arduino-MLX90393.git 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from setuptools import setup, find_packages 4 | 5 | 6 | def read(fname): 7 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 8 | 9 | 10 | setup( 11 | name="reskin_sensor", 12 | version="2.0.2", 13 | author="Raunaq Bhirangi", 14 | author_email="rbhirang@andrew.cmu.edu", 15 | description="Data acquisition library for a ReSkin sensor", 16 | long_description=read('README.md'), 17 | packages=find_packages(), 18 | install_requires=["numpy>=1.21.3", "pyserial>=3.5"], 19 | python_requires=">=3.6", 20 | url="https://github.com/raunaqbhirangi/reskin_sensor.git", 21 | ) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Raunaq Bhirangi 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 | -------------------------------------------------------------------------------- /arduino/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Arduino for ReSkin 2 | 3 | First, install Arduino IDE and the relevant board packages in order to upload code to your microcontroller. 4 | 5 | We tested the following on [Trinket M0](https://www.adafruit.com/product/3500) and [QT Py](https://www.adafruit.com/product/4600) from Adafruit. 6 | They have excellent tutorials for setting up these boards with your Arduino IDE. Follow [this one for the Trinket M0](https://learn.adafruit.com/adafruit-trinket-m0-circuitpython-arduino/arduino-ide-setup) and [this one for the QT Py](https://learn.adafruit.com/adafruit-qt-py/arduino-ide-setup). 7 | 8 | ## Parts 9 | 10 | We generally recommend the QT Py as it has an on-board Qwiic/STEMMA connector and USB-C connector, and does not require any soldering. Parts list for these two boards is as follows: 11 | 12 | ### Trinket M0 13 | - [Qwiic/STEMMA to breadboard cable](https://www.adafruit.com/product/4209) 14 | - micro-USB to USB-A cable 15 | - ReSkin circuit board 16 | 17 | ### Qt Py 18 | - [Qwiic/STEMMA cable](https://www.adafruit.com/product/4401) 19 | - USB-C to USB-A cable 20 | - ReSkin circuit board 21 | 22 | ## Installing Arduino Library (submodule) 23 | 24 | The library for the magnetometers is included as a submodule of this repo. Install the submodule 25 | ``` 26 | git submodule update --init 27 | ``` 28 | Move this library into your local libraries folder for your Arduino installation. 29 | 30 | ## Finding your port (Linux) 31 | 32 | Use the port in the tests/ as input to the -p argument. One of the simplest methods is to list all connected USB devices 33 | ``` 34 | lsusb 35 | ``` 36 | Then, connect your microcontroller and rerun the command. The new device between the two lists is your port. 37 | 38 | -------------------------------------------------------------------------------- /tests/sensor_test.py: -------------------------------------------------------------------------------- 1 | import serial 2 | 3 | import argparse 4 | from reskin_sensor import ReSkinBase, ReSkinDummy 5 | 6 | if __name__ == "__main__": 7 | # fmt: off 8 | parser = argparse.ArgumentParser( 9 | description="Test code to query ReSkin for a fixed number of data samples" 10 | ) 11 | parser.add_argument("-p", "--port", type=str, help="port to which the microcontroller is connected", required=True,) 12 | parser.add_argument("-b", "--baudrate", type=str, help="baudrate at which the microcontroller is streaming data", default=115200,) 13 | parser.add_argument("-n", "--num_mags", type=int, help="number of magnetometers on the sensor board", default=5,) 14 | # fmt: on 15 | args = parser.parse_args() 16 | 17 | try: 18 | test_sensor = ReSkinBase( 19 | num_mags=args.num_mags, 20 | port=args.port, 21 | baudrate=args.baudrate, 22 | burst_mode=True, 23 | device_id=1, 24 | ) 25 | except serial.serialutil.SerialException as e: 26 | print("ERROR: ", e) 27 | print("Using dummy sensor") 28 | test_sensor = ReSkinDummy( 29 | num_mags=args.num_mags, 30 | port=args.port, 31 | baudrate=args.baudrate, 32 | burst_mode=True, 33 | device_id=1, 34 | ) 35 | 36 | # Get 5 samples from sensor 37 | test_samples = test_sensor.get_data(num_samples=5) 38 | 39 | print( 40 | "Columns: ", 41 | ", \t".join( 42 | [ 43 | "T{0}, \tBx{0}, \tBy{0}, \tBz{0}".format(ind) 44 | for ind in range(test_sensor.num_mags) 45 | ] 46 | ), 47 | ) 48 | for sid, sample in enumerate(test_samples): 49 | print( 50 | "Sample {}: ".format(sid + 1) 51 | + str(["{:.2f}".format(d) for d in sample.data]) 52 | ) 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reskin Sensor Library 2 | This is a python library to interface with [ReSkin](https://openreview.net/forum?id=87_OJU4sw3V) sensors. We provide two classes for interfacing with [ReSkin](https://openreview.net/forum?id=87_OJU4sw3V). The `ReSkinBase` class is good for standalone data collection: it blocks code execution while data is being collected. The `ReSkinProcess` class can be used for non-blocking background data collection. Data can be buffered in the background while you run the rest of your code. 3 | 4 | Latest stable release is v2.0.0 5 | 6 | ## Installation 7 | 8 | This package can be installed using pip: 9 | ``` 10 | pip install reskin_sensor 11 | ``` 12 | Alternatively, if you would like the latest (potentially unstable) version, 13 | 1. Clone this repository using 14 | ``` 15 | $ git clone https://github.com/raunaqbhirangi/reskin_sensor.git --recursive 16 | ``` 17 | 2. Install this package using 18 | ``` 19 | $ pip install -e . 20 | ``` 21 | ## Usage 22 | 23 | 1. Connect the 5X board to the microcontroller. 24 | 25 | 2. Connect the microcontroller (we recommend the Adafruit Trinket M0 or the Adafruit QT PY) to the computer using a suitable USB cable 26 | 27 | 3. Use the [Arduino IDE](https://www.arduino.cc/en/software) to upload code to a microcontroller. The code as well as upload instructions can be found in the [arduino](./arduino) folder. 28 | If you get a `can't open device "": Permission denied` error, modify permissions to allow read and write on that port. On Linux, this would look like 29 | ``` 30 | $ sudo chmod a+rw 31 | ``` 32 | 33 | 4. Run test code on the computer 34 | ``` 35 | $ python tests/sensor_proc_test.py -p 36 | ``` 37 | ## Credits 38 | This package is maintained by [Raunaq Bhirangi](https://www.cs.cmu.edu/~rbhirang/). We would also like to cite the [pyForceDAQ](https://github.com/lindemann09/pyForceDAQ) library which was used as a reference in structuring this package. 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 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 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /tests/sensor_proc_test.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import time 3 | 4 | from reskin_sensor import ReSkinProcess 5 | 6 | if __name__ == "__main__": 7 | parser = argparse.ArgumentParser( 8 | description="Test code to run a ReSkin streaming process in the background. Allows data to be collected without code blocking" 9 | ) 10 | # fmt: off 11 | parser.add_argument("-p", "--port", type=str, help="port to which the microcontroller is connected", required=True,) 12 | parser.add_argument("-b", "--baudrate", type=str, help="baudrate at which the microcontroller is streaming data", default=115200,) 13 | parser.add_argument("-n", "--num_mags", type=int, help="number of magnetometers on the sensor board", default=5,) 14 | parser.add_argument("-tf", "--temp_filtered", action="store_true", help="flag to filter temperature from sensor output",) 15 | # fmt: on 16 | args = parser.parse_args() 17 | 18 | # Create sensor stream 19 | sensor_stream = ReSkinProcess( 20 | num_mags=args.num_mags, 21 | port=args.port, 22 | baudrate=args.baudrate, 23 | burst_mode=True, 24 | device_id=1, 25 | temp_filtered=args.temp_filtered, 26 | ) 27 | 28 | # Start sensor stream 29 | sensor_stream.start() 30 | time.sleep(0.1) 31 | 32 | # Buffer data for two seconds and return buffer 33 | if sensor_stream.is_alive(): 34 | sensor_stream.start_buffering() 35 | buffer_start = time.time() 36 | time.sleep(2.0) 37 | 38 | sensor_stream.pause_buffering() 39 | buffer_stop = time.time() 40 | 41 | # Get buffered data 42 | buffered_data = sensor_stream.get_buffer() 43 | 44 | if buffered_data is not None: 45 | print( 46 | "Time elapsed: {}, Number of datapoints: {}".format( 47 | buffer_stop - buffer_start, len(buffered_data) 48 | ) 49 | ) 50 | 51 | # Get a specified number of samples 52 | test_samples = sensor_stream.get_data(num_samples=5) 53 | print( 54 | "Columns: ", 55 | ", \t".join( 56 | [ 57 | ((not args.temp_filtered)*"T{0}, \t" + "Bx{0}, \tBy{0}, \tBz{0}").format(ind) 58 | for ind in range(args.num_mags) 59 | ] 60 | ), 61 | ) 62 | for sid, sample in enumerate(test_samples): 63 | print( 64 | "Sample {}: ".format(sid + 1) 65 | + str(["{:.2f}".format(d) for d in sample.data]) 66 | ) 67 | 68 | # Pause sensor stream 69 | sensor_stream.pause_streaming() 70 | 71 | sensor_stream.join() 72 | -------------------------------------------------------------------------------- /arduino/5X_binary_burst_stream/5X_binary_burst_stream.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 5X ReSkin Board Example Code 3 | By: Tess Hellebrekers 4 | Date: October 22, 2021 5 | License: This code is public domain but you buy me a beer if you use this and we meet someday (Beerware license). 6 | 7 | Library: Heavily based on original MLX90393 library from Theodore Yapo (https://github.com/tedyapo/arduino-MLX90393) 8 | Use this fork (https://github.com/tesshellebrekers/arduino-MLX90393) to access additional burst mode commands 9 | 10 | Read the XYZ magnetic flux fields and temperature across all five chips on the 5X ReSkin board 11 | Print binary data over serial port 12 | */ 13 | 14 | #include 15 | #include 16 | 17 | #define Serial SERIAL_PORT_USBVIRTUAL 18 | 19 | MLX90393 mlx0; 20 | MLX90393 mlx1; 21 | MLX90393 mlx2; 22 | MLX90393 mlx3; 23 | MLX90393 mlx4; 24 | 25 | MLX90393::txyz data0 = {0,0,0,0}; //Create a structure, called data, of four floats (t, x, y, and z) 26 | MLX90393::txyz data1 = {0,0,0,0}; 27 | MLX90393::txyz data2 = {0,0,0,0}; 28 | MLX90393::txyz data3 = {0,0,0,0}; 29 | MLX90393::txyz data4 = {0,0,0,0}; 30 | 31 | uint8_t mlx0_i2c = 0x0C; // these are the I2C addresses of the five chips that share one I2C bus 32 | uint8_t mlx1_i2c = 0x13; 33 | uint8_t mlx2_i2c = 0x12; 34 | uint8_t mlx3_i2c = 0x10; 35 | uint8_t mlx4_i2c = 0x11; 36 | 37 | void setup() 38 | { 39 | //Start serial port and wait until user opens it 40 | Serial.begin(115200); 41 | while (!Serial) { 42 | delay(5); 43 | } 44 | 45 | //Start default I2C bus for your board, set to fast mode (400kHz) 46 | Wire.begin(); 47 | Wire.setClock(400000); 48 | delay(10); 49 | 50 | //start chips given address, -1 for no DRDY pin, and I2C bus object to use 51 | byte status = mlx0.begin(mlx0_i2c, -1, Wire); 52 | status = mlx1.begin(mlx1_i2c, -1, Wire); 53 | status = mlx2.begin(mlx2_i2c, -1, Wire); 54 | status = mlx3.begin(mlx3_i2c, -1, Wire); 55 | status = mlx4.begin(mlx4_i2c, -1, Wire); 56 | 57 | //default gain and digital filtering set up in the begin() function of library. Adjust here is you want to change them 58 | //mlx0.setGain(5); //accepts [0,7] 59 | //mlx0.setDigitalFiltering(5); // accepts [2,7]. refer to datasheet for hall configurations 60 | 61 | //Start burst mode for temp, x, y, and z for all chips 62 | //Burst mode: continuously sample temp, x, y, and z, at regular intervals without polling 63 | mlx0.startBurst(0xF); 64 | mlx1.startBurst(0xF); 65 | mlx2.startBurst(0xF); 66 | mlx3.startBurst(0xF); 67 | mlx4.startBurst(0xF); 68 | } 69 | 70 | void loop() 71 | { 72 | //continuously read the most recent data from the data registers and save to data 73 | mlx0.readBurstData(data0); //Read the values from the sensor 74 | mlx1.readBurstData(data1); 75 | mlx2.readBurstData(data2); 76 | mlx3.readBurstData(data3); 77 | mlx4.readBurstData(data4); 78 | 79 | //write binary data over serial 80 | Serial.write((byte*)&data0, sizeof(data0)); 81 | Serial.write((byte*)&data1, sizeof(data1)); 82 | Serial.write((byte*)&data2, sizeof(data2)); 83 | Serial.write((byte*)&data3, sizeof(data3)); 84 | Serial.write((byte*)&data4, sizeof(data4)); 85 | Serial.println(); 86 | 87 | //adjust delay to achieve desired sampling rate 88 | delayMicroseconds(500); 89 | 90 | } 91 | -------------------------------------------------------------------------------- /visualizations/pygame_demo.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import sys 3 | from pygame.locals import * 4 | import math 5 | import time 6 | import numpy as np 7 | from reskin_sensor import ReSkinBase 8 | 9 | def init_pygame(): 10 | time.sleep(1) 11 | pygame.init() # initialize pygame 12 | clock = pygame.time.Clock() 13 | screen = pygame.display.set_mode((420,600)) 14 | bg = pygame.image.load("./images/3D.PNG") 15 | pygame.mouse.set_visible(1) 16 | 17 | pygame.display.set_caption('5X Board Visual') 18 | return clock, screen, bg 19 | 20 | def get_baseline(sens, num_samples): 21 | print("Leave board resting on table") 22 | time.sleep(2.) 23 | 24 | baseline_samples = sens.get_data(num_samples) 25 | baseline = [s.data for s in baseline_samples] 26 | baseline = np.array(baseline) 27 | baseline = np.mean(baseline, axis=0) 28 | print("Resting data collected.") 29 | 30 | return baseline 31 | 32 | 33 | if __name__ == '__main__': 34 | 35 | WHITE = pygame.Color(255, 255, 255) 36 | RED = pygame.Color(255, 0, 0) 37 | BLACK = pygame.Color(0,0,0) 38 | 39 | viz_sensor = ReSkinBase(num_mags=5, port='/dev/ttyACM0', baudrate=115200) 40 | scale = 100 41 | 42 | temp_mask = np.ones((20,),dtype=bool) 43 | temp_mask[::4] = False 44 | 45 | clock, screen, bg = init_pygame() 46 | 47 | # read first 100 samples as baseline 48 | numBaselineSamples = 100 49 | 50 | baseline = get_baseline(viz_sensor, numBaselineSamples) 51 | 52 | # chip locations in pixels on the game board 53 | # in order of center, top, right, bottom, left to match incoming data stream 54 | chip_locations = np.array([[211,204], [211, 60],[357, 206],[211, 353],[67, 204]]) 55 | 56 | while True: 57 | 58 | raw_data = viz_sensor.get_data(1) 59 | input_data = raw_data[0].data - baseline 60 | #rotation of chip axes to pygame coordinate system 61 | 62 | input_data = input_data[temp_mask] 63 | input_data[0:3] = [input_data[1], -1*input_data[0], -1*input_data[2]] 64 | input_data[3:6] = [input_data[4], -1*input_data[3], -1*input_data[5]] 65 | input_data[6:9] = [input_data[6], input_data[7], -1*input_data[8]] 66 | input_data[9:12] = [-1*input_data[9], -1*input_data[10], -1*input_data[11]] 67 | input_data[12:15] = [-1*input_data[13], input_data[12], -1*input_data[14]] 68 | 69 | screen.blit(bg, (0,0)) 70 | for idx in range(5): 71 | center_arrow = chip_locations[idx] 72 | angle = math.atan2(input_data[3*idx+1],input_data[3*idx]) 73 | z = math.sqrt((input_data[3*idx]*input_data[3*idx]+input_data[3*idx+1]*input_data[3*idx+1])) 74 | x = center_arrow[0] + math.sin(angle)*z 75 | y = center_arrow[1] + math.cos(angle)*z 76 | r = abs(input_data[3*idx+2])/scale 77 | pygame.draw.line(screen, (0,0,0), center_arrow, (x,y), 5) 78 | pygame.draw.circle(screen, (0,0,1), center_arrow, r, 1) 79 | 80 | for event in pygame.event.get(): 81 | if event.type == pygame.QUIT: 82 | sys.exit() 83 | elif event.type == KEYDOWN: 84 | if event.key == ord('b'): 85 | baseline = get_baseline(viz_sensor, numBaselineSamples) 86 | 87 | pygame.display.update() -------------------------------------------------------------------------------- /circuits/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The zip file contains all the information necessary to fabricate the 5X ReSkin circuit board. The BOM, Bill of Materials, details the components needed to populate the corresponding circuit design. You may choose to manufacture by yourself, if you have access to a PCB mill, and solder yourself, with a reflow oven. However, we recommend ordering your boards from a 3rd party vendor, such as PCBWay or OSHPark, as they are relatively inexpensive, quick, and high-quality. 4 | 5 | ### Note: The shipping is often the biggest bottleneck in price and time - consider placing large, shared orders if you are able. 6 | 7 | ### Note: The main magnetometer chip, MLX90393, is currently very low-stock. This is out of our control, but we hope it is resolved soon. We recommend purchasing from reputable distributers such as Digikey or Mouser. 8 | 9 | # Ordering circuit boards from PCBWay (One Example) 10 | 11 | 1. Navigate to [PCBWay](https://www.pcbway.com/) and select ["PCB Instant Quote"](https://www.pcbway.com/orderonline.aspx) from the main menu bar. 12 | 2. The default option is to start a rigid boards. Select [FPC/Rigid Flex](https://www.pcbway.com/flexible.aspx) to start a flexible PCB order. 13 | 3. For rigid boards, select [quick-order PCB](https://www.pcbway.com/QuickOrderOnline.aspx) and upload the zip file here to "+Add Gerber File". 14 | 4. RIGID BOARDS ONLY: The board size will be automatically detected as 20x29mm. We recommend the below settings for the board, in sequential line-by-line order. The bold bullet points are different from the default options. 15 | - Board type: Single Pieces 16 | - Different Design in Panel: 1 17 | - Size: (auto-populated) 20x29mm 18 | - **Quantity: [your choice]** 19 | - Layers: 2 Layers 20 | - Material: FR-4 21 | - FR4-TG: TG130-140 22 | - **Thickness: 0.6** 23 | - Min Track/Spacing: 6/6mil 24 | - Min Hole Size: 0.3mm 25 | - Solder Mask: Green 26 | - Silkscreen: White 27 | - Edge Connector: No 28 | - Surface Finish: HASL with Lead or **Immersion gold (ENIG)** 29 | - Via Process: Tenting Vias 30 | - Finished Copper: 1oz Cu 31 | - Extra pcb product number: [your choice] Note: It is a 3$ fee to avoid PCBWay extra text over the design. Purely aesthetic, no functional difference. 32 | - Additional options: None 33 | 5. FLEX BOARDS ONLY: There is no option for quick upload. They will prompt for the zip file after adding to cart for approval. The bold bullet points are different from the default options. 34 | - PCB Type: Flexible PCB 35 | - Different Design in Panel: 1 36 | - **Layers: 2 Layers** 37 | - Board type: Single Pieces 38 | - Size: 20x29mm 39 | - **Quantity: [your choice]** 40 | - Polyimide base material: Polyimide Flex 41 | - FPC Thickness: 0.1 42 | - Min Track/Spacing: 0.06mm 43 | - Min Hole size/pad size: 0.15/0.35mm 44 | - Solder Mask: Yellow Coverlay or **Black Coverlay** 45 | - Silkscreen: White or **Black** 46 | - Edge Connector: No 47 | - Stiffener: without 48 | - Surface Finish: Immersion Gold (ENIG) 49 | - Thickness of Immersion Gold: 1U" 50 | - Finished Copper: 1 oz Cu (35um) 51 | - E-test: 100% 52 | - 3M/Tesa tape: without 53 | - EMI shielding film: without 54 | - Other Special Request: None 55 | 6. Assembly service 56 | - Turnkey 57 | - Board type: single pieces 58 | - Quantity: [number of boards you want assembled, can be equal to or less than number of boards manufactured above] 59 | - Pay attention: No sensitive parts 60 | - Number of unique parts: 6 61 | - Number of SMD parts: 6 62 | - Number of BGA/QFP parts: 0 63 | - Number of through-hole parts: 0 64 | - Detailed information of assembly: If you request assembly at the same time as boards, you may leave this empty. If you first requested boards, and assembly later on, put the Product No of the boards that you can find under "Under Review". 65 | 66 | Add the boards and assembly services to cart. It will usually be approved within 24 hours. After approval, you may add the items to cart and pay following their instructions. 67 | -------------------------------------------------------------------------------- /arduino/5X_burst_stream/5X_burst_stream.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 5X ReSkin Board Example Code 3 | By: Tess Hellebrekers 4 | Date: October 22, 2021 5 | License: This code is public domain but you buy me a beer if you use this and we meet someday (Beerware license). 6 | 7 | Library: Heavily based on original MLX90393 library from Theodore Yapo (https://github.com/tedyapo/arduino-MLX90393) 8 | Use this fork (https://github.com/tesshellebrekers/arduino-MLX90393) to access additional burst mode commands 9 | 10 | Read the XYZ magnetic flux fields and temperature across all five chips on the 5X ReSkin board 11 | Print binary data over serial port 12 | */ 13 | 14 | #include 15 | #include 16 | 17 | #define Serial SERIAL_PORT_USBVIRTUAL 18 | 19 | MLX90393 mlx0; 20 | MLX90393 mlx1; 21 | MLX90393 mlx2; 22 | MLX90393 mlx3; 23 | MLX90393 mlx4; 24 | 25 | MLX90393::txyz data0 = {0,0,0,0}; //Create a structure, called data, of four floats (t, x, y, and z) 26 | MLX90393::txyz data1 = {0,0,0,0}; 27 | MLX90393::txyz data2 = {0,0,0,0}; 28 | MLX90393::txyz data3 = {0,0,0,0}; 29 | MLX90393::txyz data4 = {0,0,0,0}; 30 | 31 | uint8_t mlx0_i2c = 0x0C; // these are the I2C addresses of the five chips that share one I2C bus 32 | uint8_t mlx1_i2c = 0x13; 33 | uint8_t mlx2_i2c = 0x12; 34 | uint8_t mlx3_i2c = 0x10; 35 | uint8_t mlx4_i2c = 0x11; 36 | 37 | void setup() 38 | { 39 | //Start serial port and wait until user opens it 40 | Serial.begin(115200); 41 | while (!Serial) { 42 | delay(5); 43 | } 44 | 45 | //Start default I2C bus for your board, set to fast mode (400kHz) 46 | Wire.begin(); 47 | Wire.setClock(400000); 48 | delay(10); 49 | 50 | //start chips given address, -1 for no DRDY pin, and I2C bus object to use 51 | byte status = mlx0.begin(mlx0_i2c, -1, Wire); 52 | status = mlx1.begin(mlx1_i2c, -1, Wire); 53 | status = mlx2.begin(mlx2_i2c, -1, Wire); 54 | status = mlx3.begin(mlx3_i2c, -1, Wire); 55 | status = mlx4.begin(mlx4_i2c, -1, Wire); 56 | 57 | //default gain and digital filtering set up in the begin() function of library. Adjust here is you want to change them 58 | //mlx0.setGain(5); //accepts [0,7] 59 | //mlx0.setDigitalFiltering(5); // accepts [2,7]. refer to datasheet for hall configurations 60 | 61 | //Start burst mode for temp, x, y, and z for all chips 62 | //Burst mode: continuously sample temp, x, y, and z, at regular intervals without polling 63 | mlx0.startBurst(0xF); 64 | mlx1.startBurst(0xF); 65 | mlx2.startBurst(0xF); 66 | mlx3.startBurst(0xF); 67 | mlx4.startBurst(0xF); 68 | } 69 | 70 | void loop() 71 | { 72 | //continuously read the most recent data from the data registers and save to data 73 | mlx0.readBurstData(data0); //Read the values from the sensor 74 | mlx1.readBurstData(data1); 75 | mlx2.readBurstData(data2); 76 | mlx3.readBurstData(data3); 77 | mlx4.readBurstData(data4); 78 | 79 | //write string data over serial 80 | Serial.print(data0.x); 81 | Serial.print("\t"); 82 | Serial.print(data0.y); 83 | Serial.print("\t"); 84 | Serial.print(data0.z); 85 | Serial.print("\t"); 86 | Serial.print(data0.t); 87 | Serial.print("\t"); 88 | 89 | Serial.print(data1.x); 90 | Serial.print("\t"); 91 | Serial.print(data1.y); 92 | Serial.print("\t"); 93 | Serial.print(data1.z); 94 | Serial.print("\t"); 95 | Serial.print(data1.t); 96 | Serial.print("\t"); 97 | 98 | Serial.print(data2.x); 99 | Serial.print("\t"); 100 | Serial.print(data2.y); 101 | Serial.print("\t"); 102 | Serial.print(data2.z); 103 | Serial.print("\t"); 104 | Serial.print(data2.t); 105 | Serial.print("\t"); 106 | 107 | Serial.print(data3.x); 108 | Serial.print("\t"); 109 | Serial.print(data3.y); 110 | Serial.print("\t"); 111 | Serial.print(data3.z); 112 | Serial.print("\t"); 113 | Serial.print(data3.t); 114 | Serial.print("\t"); 115 | 116 | Serial.print(data4.x); 117 | Serial.print("\t"); 118 | Serial.print(data4.y); 119 | Serial.print("\t"); 120 | Serial.print(data4.z); 121 | Serial.print("\t"); 122 | Serial.print(data4.t); 123 | Serial.print("\t"); 124 | 125 | Serial.println(); 126 | 127 | //adjust delay to achieve desired sampling rate 128 | delayMicroseconds(500); 129 | 130 | } 131 | -------------------------------------------------------------------------------- /visualizations/heatmap.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | import time 3 | 4 | import argparse 5 | import matplotlib.pyplot as plt 6 | from matplotlib.animation import FuncAnimation 7 | import numpy as np 8 | 9 | from reskin_sensor import ReSkinProcess 10 | 11 | 12 | def plot_heatmap(data, num_mags): 13 | data_dim = data.shape[-1] 14 | 15 | data_mask = np.zeros((data_dim,), dtype=bool) 16 | data_mask[2:-1] = True 17 | 18 | # Filter temperature if required 19 | if data_dim == 4 * num_mags + 3: 20 | data_mask[2:-1:4] = False 21 | 22 | fig, axs = plt.subplots(2, 1, figsize=(8, 16)) 23 | 24 | filt_data = data[..., data_mask] 25 | times = data[..., 0] - data[0, 0] 26 | axs[0].plot(times, filt_data) 27 | axs[0].set_xlabel("Time, in s") 28 | pc = axs[1].pcolormesh(filt_data.T) 29 | 30 | xticks = np.arange(0, data.shape[0], max(1, data.shape[0] // 10)) 31 | axs[1].set_xlabel("Time, in s") 32 | axs[1].set_xticks(xticks) 33 | axs[1].set_xticklabels(["{:.2f}".format(times[i]) for i in xticks]) 34 | axs[1].set_yticks(np.linspace(0.5, num_mags * 3 - 0.5, num_mags * 3)) 35 | 36 | ylabels = [] 37 | for m in range(num_mags): 38 | ylabels.extend(["Bx{}".format(m), "By{}".format(m), "Bz{}".format(m)]) 39 | axs[1].set_yticklabels(ylabels) 40 | 41 | fig.colorbar(pc) 42 | plt.show() 43 | 44 | 45 | def update_data(ax, sensor, init_time, baseline, ln, xdata, ydata, i): 46 | sensor.pause_buffering() 47 | buf = np.array(sensor.get_buffer()) 48 | sensor.start_buffering() 49 | times = buf[:, 0] - init_time 50 | data = buf[:, 2:-1] - baseline 51 | 52 | xdata.extend(list(times)) 53 | ydata.extend(list(data)) 54 | 55 | ln.set_array(np.array(ydata).T) 56 | 57 | xticks = np.arange(0, len(xdata), max(1, len(xdata) // 10)) 58 | xlabels = ["{:.2f}".format(xdata[i]) for i in xticks] 59 | 60 | ax.set_xticklabels(xlabels) 61 | 62 | return (ln,) 63 | 64 | 65 | if __name__ == "__main__": 66 | # fmt: off 67 | parser = argparse.ArgumentParser(description="Visualize ReSkin data as a heatmap") 68 | parser.add_argument("--stream", action="store_true", help="Flag to stream live data") 69 | parser.add_argument("-nm", "--num-mags", type=int, required=True, help="Number of magnetometers") 70 | parser.add_argument("-p", "--port", type=str, default="/dev/ttyACM0", help="ReSkin post; ignored if not streaming") 71 | parser.add_argument("-ws", "--window-size", type=int, default=1000, help="Number of samples visualized at a time") 72 | parser.add_argument("--lims", type=float, nargs=2, default=[-300., 300.], help="Colorbar limits for streaming") 73 | 74 | parser.add_argument("-dp", "--data-path", type=str, help="Path for loading data") 75 | args = parser.parse_args() 76 | # fmt: on 77 | 78 | num_samples = args.window_size 79 | num_mags = args.num_mags 80 | 81 | if args.stream: 82 | reskin = ReSkinProcess( 83 | num_mags=args.num_mags, 84 | port=args.port, 85 | temp_filtered=True, 86 | reskin_data_struct=False, 87 | ) 88 | reskin.start() 89 | time.sleep(1.0) 90 | init_data = np.array(reskin.get_data(num_samples)) 91 | baseline = np.mean(init_data[..., 2:-1], axis=0, keepdims=True) 92 | init_time = init_data[0, 0] 93 | 94 | reskin.start_buffering() 95 | 96 | fig, ax = plt.subplots() 97 | xdata, ydata = deque(maxlen=num_samples), deque(maxlen=num_samples) 98 | 99 | xdata.extend(list(init_data[..., 0]) - init_time) 100 | ydata.extend(list(init_data[..., 2:-1] - baseline)) 101 | 102 | ln = ax.pcolormesh(np.array(ydata).T, vmin=args.lims[0], vmax=args.lims[1]) 103 | ax.set_xticks(np.arange(0, num_samples, max(1, num_samples // 10))) 104 | ax.set_yticks(np.linspace(0.5, num_mags * 3 - 0.5, num_mags * 3)) 105 | ylabels = [] 106 | for m in range(num_mags): 107 | ylabels.extend(["Bx{}".format(m), "By{}".format(m), "Bz{}".format(m)]) 108 | ax.set_yticklabels(ylabels) 109 | ax.set_xlabel("Time, in s") 110 | cbar = fig.colorbar(ln) 111 | 112 | ani = FuncAnimation( 113 | fig, 114 | lambda i: update_data(ax, reskin, init_time, baseline, ln, xdata, ydata, i), 115 | blit=False, 116 | ) 117 | plt.show() 118 | 119 | else: 120 | data_path = args.data_path 121 | 122 | # Load data 123 | with open(data_path, "rb") as f: 124 | data = np.load(f) 125 | 126 | data_dim = data.shape[-1] 127 | 128 | data_mask = np.zeros((data_dim,), dtype=bool) 129 | data_mask[2:-1] = True 130 | 131 | # Filter temperature if required 132 | if data_dim == 4 * num_mags + 3: 133 | data_mask[2:-1:4] = False 134 | 135 | with open(data_path, "rb") as f: 136 | data = np.load(f) 137 | # test_data = data["task"][..., data_mask] 138 | plot_heatmap(data, num_mags) 139 | -------------------------------------------------------------------------------- /reskin_sensor/sensor.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import struct 3 | import time 4 | 5 | import numpy as np 6 | import serial 7 | 8 | ReSkinData = collections.namedtuple("ReSkinData", "time, acq_delay, data, dev_id") 9 | 10 | 11 | class ReSkinBase(serial.Serial): 12 | """ 13 | Base class for a ReSkin sensor. 14 | 15 | Attributes 16 | ---------- 17 | num_mags: int 18 | Number of magnetometers connected to the sensor 19 | port : str 20 | System port that the sensor is connected to 21 | baudrate: int 22 | Baudrate at which data is transmitted by sensor 23 | burst_mode: bool 24 | Flag for whether sensor is using burst mode 25 | device_id: int 26 | Sensor ID; mostly useful when using multiple sensors simultaneously 27 | temp_filtered: bool 28 | Flag indicating if temperature readings should be filtered from 29 | the output 30 | reskin_data_struct: bool 31 | Flag indicating whether the ReSkinData structure should be used for 32 | output data 33 | 34 | Methods 35 | ------- 36 | get_data(num_samples) 37 | Collects num_samples samples from sensor 38 | """ 39 | 40 | def __init__( 41 | self, 42 | num_mags: int = 1, 43 | port: str = None, 44 | baudrate: int = 115200, 45 | burst_mode: bool = True, 46 | device_id: int = -1, 47 | temp_filtered: bool = False, 48 | reskin_data_struct: bool = True, 49 | ) -> None: 50 | """Initializes a ReSkinBase object.""" 51 | 52 | self.num_mags = num_mags 53 | self.port_name = port 54 | self.baud_rate = baudrate 55 | self.burst_mode = burst_mode 56 | self.device_id = device_id 57 | self.reskin_data_struct = reskin_data_struct 58 | 59 | self._msg_floats = 4 * num_mags 60 | self._msg_length = 4 * self._msg_floats + 2 61 | 62 | self._temp_mask = np.ones((self._msg_floats,), dtype=bool) 63 | if temp_filtered: 64 | self._temp_mask[::4] = False 65 | 66 | super(ReSkinBase, self).__init__(port=port, baudrate=baudrate) 67 | self._initialize() 68 | 69 | def _initialize(self): 70 | """ 71 | Opens the serial port for communication with sensor 72 | """ 73 | self.flush() 74 | print("Initializing sensor...") 75 | try: 76 | self.get_sample() 77 | print("Initialization successful") 78 | except: 79 | print("Initialization failed. Please disconnect and reconnect sensor.") 80 | 81 | def get_data(self, num_samples): 82 | """ 83 | Collects requisite number of samples from the sensor 84 | 85 | Parameters 86 | ---------- 87 | num_samples: int 88 | Number of samples of data to be collected. 89 | """ 90 | data = [] 91 | for _ in range(num_samples): 92 | t, acqd, sample = self.get_sample() 93 | if self.reskin_data_struct: 94 | data.append( 95 | ReSkinData( 96 | time=t, 97 | acq_delay=acqd, 98 | data=sample, 99 | dev_id=self.device_id, 100 | ) 101 | ) 102 | else: 103 | data.append( 104 | np.concatenate( 105 | ([t], [acqd], sample, [self.device_id]) 106 | ) 107 | ) 108 | 109 | return data 110 | 111 | def get_sample(self, num_samples=1): 112 | """ 113 | Collects requisite bytes of data from the serial communication 114 | channel 115 | 116 | """ 117 | # Just to make sure we're not reading in gibberish. Filling up the input 118 | # buffer causes serial read to give out stale data. Resetting input buffer 119 | # can occasionally result gibberish coming in. Must ensure that that does 120 | # not happen 121 | 122 | if self.in_waiting > 4000: 123 | self.reset_input_buffer() 124 | while True: 125 | # if self.in_waiting >=115: 126 | if self.in_waiting > self._msg_length: 127 | if self.read(self._msg_length)[-2:] == b"\r\n": 128 | break 129 | self.reset_input_buffer() 130 | 131 | while True: 132 | if self.in_waiting > self._msg_length: 133 | collect_start = time.time() 134 | if self.burst_mode: 135 | zero_bytes = self.read(self._msg_length) 136 | if zero_bytes[-2:] != b"\r\n": 137 | zero_bytes = self.read_until(b"\r\n") 138 | continue 139 | decoded_zero_bytes = struct.unpack( 140 | "@{}fcc".format(self._msg_floats), zero_bytes 141 | )[: self._msg_floats] 142 | 143 | else: 144 | zero_bytes = self.readline() 145 | decoded_zero_bytes = zero_bytes.decode("utf-8") 146 | decoded_zero_bytes = decoded_zero_bytes.strip() 147 | decoded_zero_bytes = [float(x) for x in decoded_zero_bytes.split()] 148 | 149 | acq_delay = time.time() - collect_start 150 | return collect_start, acq_delay, np.array(decoded_zero_bytes)[self._temp_mask] 151 | 152 | else: 153 | # Need checks to timeout if required 154 | pass 155 | 156 | 157 | class ReSkinDummy(ReSkinBase): 158 | def __init__( 159 | self, 160 | num_mags: int = 1, 161 | port: str = None, 162 | baudrate: int = 115200, 163 | burst_mode: bool = True, 164 | device_id: int = -1, 165 | temp_filtered: bool = False, 166 | reskin_data_struct: bool = True, 167 | ): 168 | 169 | self.num_mags = num_mags 170 | self.port_name = port 171 | self.baud_rate = baudrate 172 | self.burst_mode = burst_mode 173 | self.device_id = device_id 174 | self.reskin_data_struct = reskin_data_struct 175 | 176 | self._msg_floats = 4 * num_mags 177 | self._msg_length = 4 * self._msg_floats + 2 178 | 179 | self._temp_mask = np.ones((self._msg_floats,), dtype=bool) 180 | if temp_filtered: 181 | self._temp_mask[::4] = False 182 | 183 | def _initialize(self): 184 | pass 185 | 186 | def get_sample(self, num_samples=1): 187 | collect_start = time.time() 188 | data = np.random.uniform(-1., 1., size=(np.sum(self._temp_mask),)) 189 | acq_delay = time.time() - collect_start 190 | 191 | return collect_start, acq_delay, data 192 | -------------------------------------------------------------------------------- /reskin_sensor/sensor_proc.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | import ctypes as ct 3 | import sys 4 | from multiprocessing import Process, Event, Pipe, Value, Array 5 | 6 | import numpy as np 7 | import serial 8 | 9 | from .sensor import ReSkinBase, ReSkinData, ReSkinDummy 10 | 11 | 12 | class ReSkinProcess(Process): 13 | """ 14 | Process to keep ReSkin datastream running in the background. 15 | 16 | Attributes 17 | ---------- 18 | num_mags: int 19 | Number of magnetometers connected to the sensor 20 | port : str 21 | System port that the sensor is connected to 22 | baudrate: int 23 | Baudrate at which data is transmitted by sensor 24 | burst_mode: bool 25 | Flag for whether sensor is using burst mode 26 | device_id: int 27 | Sensor ID; mostly useful when using multiple sensors simultaneously 28 | temp_filtered: bool 29 | Flag indicating if temperature readings should be filtered from 30 | the output 31 | reskin_data_struct: bool 32 | Flag indicating whether the ReSkinData structure should be used for 33 | output data 34 | allow_dummy_sensor: bool 35 | Flag to instantiate a dummy sensor if a real sensor with the specified 36 | configurations is unavailable 37 | chunk_size : int 38 | Quantum of data piped from buffer at one time. 39 | 40 | Methods 41 | ------- 42 | start_streaming(): 43 | Start streaming data from ReSkin sensor 44 | start_buffering(overwrite=False): 45 | Start buffering ReSkin data. Call is ignored if already buffering 46 | pause_buffering(): 47 | Stop buffering ReSkin data 48 | pause_streaming(): 49 | Stop streaming data from ReSkin sensor 50 | get_data(num_samples=5): 51 | Return a specified number of samples from the ReSkin Sensor 52 | get_buffer(timeout=1.0, pause_if_buffering=False): 53 | Return the recorded buffer 54 | """ 55 | 56 | def __init__( 57 | self, 58 | num_mags: int = 1, 59 | port: str = None, 60 | baudrate: int = 115200, 61 | burst_mode: bool = True, 62 | device_id: int = -1, 63 | temp_filtered: bool = False, 64 | reskin_data_struct: bool = True, 65 | allow_dummy_sensor: bool = False, 66 | chunk_size: int = 10000, 67 | ): 68 | """Initializes a ReSkinProcess object.""" 69 | super(ReSkinProcess, self).__init__() 70 | self.num_mags = num_mags 71 | self.port = port 72 | self.baudrate = baudrate 73 | self.burst_mode = burst_mode 74 | self.device_id = device_id 75 | self.temp_filtered = temp_filtered 76 | self.reskin_data_struct = reskin_data_struct 77 | self.allow_dummy_sensor = allow_dummy_sensor 78 | 79 | self._pipe_in, self._pipe_out = Pipe() 80 | self._sample_cnt = Value(ct.c_uint64) 81 | self._buffer_size = Value(ct.c_uint64) 82 | 83 | self._last_time = Value(ct.c_double) 84 | self._last_delay = Value(ct.c_double) 85 | self._last_reading = Array(ct.c_float, self.num_mags * (4 - temp_filtered)) 86 | 87 | self._chunk_size = chunk_size 88 | 89 | self._event_is_streaming = Event() 90 | self._event_quit_request = Event() 91 | self._event_sending_data = Event() 92 | 93 | self._event_is_buffering = Event() 94 | 95 | atexit.register(self.join) 96 | 97 | @property 98 | def last_reading(self): 99 | if self.reskin_data_struct: 100 | return ReSkinData( 101 | time=self._last_time.value, 102 | acq_delay=self._last_delay.value, 103 | data=self._last_reading[:], 104 | dev_id=self.device_id, 105 | ) 106 | else: 107 | return np.concatenate( 108 | ( 109 | [self._last_time.value], 110 | [self._last_delay.value], 111 | self._last_reading[:], 112 | [self.device_id], 113 | ) 114 | ) 115 | # return self._last_reading 116 | 117 | @property 118 | def sample_cnt(self): 119 | return self._sample_cnt.value 120 | 121 | def start_streaming(self): 122 | """Start streaming data from ReSkin sensor""" 123 | if not self._event_quit_request.is_set(): 124 | self._event_is_streaming.set() 125 | print("Started streaming") 126 | 127 | def start_buffering(self, overwrite: bool = False): 128 | """ 129 | Start buffering ReSkin data. Call is ignored if already buffering 130 | 131 | Parameters 132 | ---------- 133 | overwrite : bool 134 | Existing buffer is overwritten if true; appended if false. Ignored 135 | if data is already buffering 136 | """ 137 | 138 | if not self._event_is_buffering.is_set(): 139 | if overwrite: 140 | # Warn that buffer is about to be overwritten 141 | print("Warning: Overwriting non-empty buffer") 142 | self.get_buffer() 143 | self._event_is_buffering.set() 144 | else: 145 | # Warn that data is already buffering 146 | print("Warning: Data is already buffering") 147 | 148 | def pause_buffering(self): 149 | """Stop buffering ReSkin data""" 150 | self._event_is_buffering.clear() 151 | 152 | def pause_streaming(self): 153 | """Stop streaming data from ReSkin sensor""" 154 | self._event_is_streaming.clear() 155 | 156 | def get_data(self, num_samples=5): 157 | """ 158 | Return a specified number of samples from the ReSkin Sensor 159 | 160 | Parameters 161 | ---------- 162 | num_samples : int 163 | Number of samples required 164 | """ 165 | # Only sends samples if streaming is on. Sends empty list otherwise. 166 | 167 | samples = [] 168 | if num_samples <= 0: 169 | return samples 170 | last_cnt = self._sample_cnt.value 171 | samples = [self.last_reading] 172 | while len(samples) < num_samples: 173 | if not self._event_is_streaming.is_set(): 174 | print("Please start streaming first.") 175 | return [] 176 | # print(self._sample_cnt.value) 177 | if last_cnt == self._sample_cnt.value: 178 | continue 179 | last_cnt = self._sample_cnt.value 180 | samples.append(self.last_reading) 181 | 182 | return samples 183 | 184 | def get_buffer(self, timeout: float = 1.0, pause_if_buffering: bool = False): 185 | """ 186 | Return the recorded buffer 187 | 188 | Parameters 189 | ---------- 190 | timeout : int 191 | Time to wait for data to start getting piped. 192 | 193 | pause_if_buffering : bool 194 | Pauses buffering if still running, and then collects and returns buffer 195 | """ 196 | # Check if buffering is paused 197 | if self._event_is_buffering.is_set(): 198 | if not pause_if_buffering: 199 | print( 200 | "Cannot get buffer while data is buffering. Set " 201 | "pause_if_buffering=True to pause buffering and " 202 | "retrieve buffer" 203 | ) 204 | return 205 | else: 206 | self._event_is_buffering.clear() 207 | rtn = [] 208 | if self._event_sending_data.is_set() or self._buffer_size.value > 0: 209 | self._event_sending_data.wait(timeout=timeout) 210 | while self._pipe_in.poll() or self._buffer_size.value > 0: 211 | rtn.extend(self._pipe_in.recv()) 212 | self._event_sending_data.clear() 213 | 214 | return rtn 215 | 216 | def join(self, timeout=None): 217 | """Clean up before exiting""" 218 | self._event_quit_request.set() 219 | self.pause_buffering() 220 | self.pause_streaming() 221 | 222 | super(ReSkinProcess, self).join(timeout) 223 | 224 | def run(self): 225 | """This loop runs until it's asked to quit.""" 226 | buffer = [] 227 | # Initialize sensor 228 | try: 229 | self.sensor = ReSkinBase( 230 | num_mags=self.num_mags, 231 | port=self.port, 232 | baudrate=self.baudrate, 233 | burst_mode=self.burst_mode, 234 | device_id=self.device_id, 235 | temp_filtered=self.temp_filtered, 236 | reskin_data_struct=True, 237 | ) 238 | # self.sensor._initialize() 239 | self.start_streaming() 240 | except (serial.serialutil.SerialException, AttributeError) as e: 241 | print("ERROR: ", e) 242 | if self.allow_dummy_sensor: 243 | print("Using dummy sensor") 244 | self.sensor = ReSkinDummy( 245 | num_mags=self.num_mags, 246 | port=self.port, 247 | baudrate=self.baudrate, 248 | burst_mode=self.burst_mode, 249 | device_id=self.device_id, 250 | temp_filtered=self.temp_filtered, 251 | reskin_data_struct=True, 252 | ) 253 | self.start_streaming() 254 | else: 255 | sys.exit(-1) 256 | 257 | is_streaming = False 258 | while not self._event_quit_request.is_set(): 259 | if self._event_is_streaming.is_set(): 260 | if not is_streaming: 261 | is_streaming = True 262 | # Any logging or stuff you want to do when streaming has 263 | # just started should go here 264 | ( 265 | self._last_time.value, 266 | self._last_delay.value, 267 | self._last_reading[:], 268 | ) = self.sensor.get_sample() 269 | 270 | self._sample_cnt.value += 1 271 | 272 | if self._event_is_buffering.is_set(): 273 | buffer.append(self.last_reading) 274 | self._buffer_size.value = len(buffer) 275 | elif self._buffer_size.value > 0: 276 | self._event_sending_data.set() 277 | chk = self._chunk_size 278 | while len(buffer) > 0: 279 | if chk > len(buffer): 280 | chk = len(buffer) 281 | self._pipe_out.send(buffer[0:chk]) 282 | buffer[0:chk] = [] 283 | self._buffer_size.value = len(buffer) 284 | 285 | else: 286 | if is_streaming: 287 | is_streaming = False 288 | # Logging when streaming just stopped 289 | 290 | if self._buffer_size.value > 0: 291 | self._event_sending_data.set() 292 | chk = self._chunk_size 293 | while len(buffer) > 0: 294 | if chk > len(buffer): 295 | chk = len(buffer) 296 | self._pipe_out.send(buffer[0:chk]) 297 | buffer[0:chk] = [] 298 | self._buffer_size.value = len(buffer) 299 | 300 | self.pause_streaming() 301 | --------------------------------------------------------------------------------