Skip to content

Open Hardware- and Software-Plattform for controlling tactile Displays

License

Notifications You must be signed in to change notification settings

OpenTactile/SCRATCHy

Repository files navigation

SCRATCHy

The SCalable Reference Architecture for a Tactile display Control Hardware

SCRATCHy is an open hardware- and software-plattform for controlling so-called "tactile Displays", i. e. devices that are intended to stimulate the touch sensitive receptors in human skin. In combination with ITCHy, the tactile mouse, this project forms a complete research environment for e. g. developing new models controlling tactile displays or for conducting user studies.

This repository contains the following parts that reflect the folder structure:

  • ./pcb/ → Schematics and layouts for printed circuit boards
  • ./libSCRATCHy/ → Library to access the Signal Generators and peripherals
  • ./SCRATCHPy/ → Python wrapper covering the high-level functionality of libSCRATCHy
  • ./examplesCpp/ → Example programs using the C++ interface
  • ./examplesPython/ → Example programs using the Python interface
  • ./generatorFirmware/ → Signal Generator "operating system"

Assembling the hardware system

The circuit boards located in the ./pcb/ folder have been designed with Fritzing, an open source EDA software. For convenience, production ready Extended Gerber files can be found in their respective subfolders.

Since most of the parts on the PCBs come in SMD packages, some experience in soldering such small parts is beneficial. However, the smallest components used here (0805 Package) can still be soldered by hand easily. (There are some nice tutorial videos on how to solder SMD components by hand.)

Each of the PCB subfolders contains a Bill Of Material (BOM) listing the components required for a single board. The part numbers given in these lists can be looked up on Farnell/Newark.

System overview

Image of Yaktocat The hardware system consists of a single main processor and several so-called Signal Generators (SGs) that are connected to a custom SPI/I²C bus. Each of these SGs is able to generate up to 4 indepentent analog signals. Depending of the type of SG, these signals can be amplified externally or are amplified onboard to up to ±200 V (theoretically, has been tested with 120V).

MainComputeUnit

The whole system builds upon the MainComputeUnit (MCU) that hosts a Raspberri Pi 3. This linux based computer is intended to generate - depending on e.g. position and velocity of the Tactile Mouse - a rather abstract signal specification for each actuator of the tactile display that is distributed across the SPI bus. Finally, the connected SGs generate a continuous waveform that can be further amplified or processed.

The PCB exposes various GPIO pins as well as SPI and I²C ports of the Raspberry Pi 3 to a common data-bus. Additionally, the DisplayBreaktout board can be connected using a separate plug. This PCB also acts as a pedestal for other modules that can simply be stacked on top. It can be fixated with four M3 screws.

DisplayBreakout

This optional PCB hosts a small graphical OLED display as well as some tactile switches that can be accessed via libSCRATCHy. It is connected to the MCU via I²C (and some GPIO pins used for the buttons). Since the Raspberry Pi 3 can connect via HDMI to an external monitor, this board is completely optional.

AnalogSignalGenerator

The AnalogSignalGenerator (ASG) contains two Signal Generators within one PCB and is therefore able to generate 8 analog output signals simultaneously. The control data sent from the MCU is interpreted using a custom software running on a Teensy 3.2 USB development board and the resulting time-varying waveform is generated using PWM. This signal is then lowpass filtered to the frequency range relevant to the mechanoreceptors (0-1000 Hz).

HighVoltageAmplifier

The HighVoltageAmplifier (HVA) contains a single Signal Generator as well as a series of Class-D amplifiers in H bridge topology. Using a separate power supply (ranging from 0 to 200V max.) the PWM signals generated by the Teensy 3.2 are amplified to amplitudes of up to 200V (peak to peak, max.). In combination with a separate passive low pass filter, this signal may be used to drive e. g. piezoelectric actuators directly.

Working with such high voltages is dangerous! A sufficient casing is needed to prevent accidential contact. Use at your own risk! This device has been tested for voltages up to 120 V. Depending on load and voltage, active cooling may be needed.

PiezoFilter

This board is needed for low-pass filtering the high voltage signals coming from the HVA. It contains several inductors that form in combination with the capacitivity of the piezoelectric actuators a sufficient filter.

Programming the Signal Generators

Before the Teensy can be flashed, please install Teensyduino by following the official instructions. Afterwards, building and flashing the firmware is straightforward:

cd generatorFirmware
make

If the build process was successfull, you can install the firmware by connecting the Teensy (separate it from the SG board before!) via USB and running

make upload

Dependencies

libSCRATCHy requires the following dependencies to be installed:

If the Raspberry Pi 3 is going to be used for the MCU (recommended), the following libraries need to be installed as well:

SCRATCHPy, the python wrapper for libSCRATCHy, does need two more dependencies:

Building libSCRATCHy and SCRATCHPy

libSCRATCHy uses the qmake build system and is available for linux platforms only. For building and installing the library system-wide, please follow these steps within the base directory:

mkdir libSCRATCHy-build
cd libSCRATCHy-build
qmake ../libSCRATCHy
make && sudo make install

Using fake mode

If you intend to use libSCRATCHy on a personal computer (not using the Raspberry Pi 3), you may pass an additional configuration option to qmake that disables some platform specific tests at runtime

[...]
qmake ../libSCRATCHy CONFIG+=fake
make && sudo make install

This also causes the iowrap_dummy.cpp to be built instead of iowrap_raspberry.cpp. By using fake mode, one can make use of the complete SCRATCHy API without triggering any hardware specific actions (e.g. GPIO access, I²C communication, etc.).

First steps in using libSCRATCHy

The directory examplesCpp contains a minimal example project that can be build using qmake:

mkdir minimalExample-build
cd minimalExample-build
qmake ../examplesCpp/minimalExample
make
./MinimalExample

Please make sure that libSCRATCHy as well as libITCHy has been build and installed beforehand. This application will output a frequency that is scaled with the current movement speed of the tactile mouse.

Interfacing with Python

A python-based application equivalent to the C++ example can be found in the examplesPython directory. The python script minimalExample.py needs the python bindings of libSCRATCHy and libITCHy to be build. When running

python minimalExample.py

the application should do exactly the same as the C++ example.

In case there are ModuleNotFoundError messages, please adjust the lines

sys.path.append(os.path.abspath('../../SCRATCHPy/build/lib.linux-x86_64-3.6/'))
sys.path.append(os.path.abspath('../../../ITCHy/ITCHPy/build/lib.linux-x86_64-3.6/'))

to match the actual build-path of the libraries.

API Reference

The SCRACTHy API is separated into two categories: High- and Lowlevel functionality. In the majority of cases, the Highlevel API should be sufficient to control the complete system. In case custom devices have been added to the bus-system, or if the SignalGenerators following a non-standard protocol, the Lowlevel API allows for direct access to GPIO, SPI and I²C.

Highlevel API

GraphicalDisplay

This class allows accessing the (optional) DisplayBreakout by providing convenience functions for displaying icons and text.

bool detach()

Separates the GraphicalDisplay from the main thread, causing the show and text methods to be non-blocking. However, special attention has to be drawn when communication via I²C from a different thread simultaneously.

bool isPressed(Button button)

Queries if a specific button is pressed at the moment of the call. Button may be one of the following values:

  • Button::Back
  • Button::Up
  • Button::Down
  • Button::Select (The rightmost button on the DisplayBreakout)
void clear()

Clears the OLED Display causing all pixels to be set to black.

void show(Icon icon, const std::string& header, const std::string& body)

Renders a default arrangement to the OLED-display consisting of a small 32x32px pictogram on the left, a header text in a slightly larger font and a body text with the default 6x6px font. In case the strings are too long to be displayed they will be truncated.

The following pictograms can be selected:

None Icon::None, Scratchy Icon::Scratchy, Spectrum Icon::Spectrum, Forbidden Icon::Forbidden

Bolt Icon::Bolt, Bug Icon::Bug, Check Icon::Check, Clock Icon::Clock

Cog Icon::Cog, Fire Icon::Fire, Image Icon::Image, Reboot Icon::Reboot

Power Icon::Power, Pulse Icon::Pulse, Random Icon::Random, Warning Icon::Warning

Wrench Icon::Wrench, X Icon::X

void show(Icon icon, const std::string& header, float body)

This is an overloaded method for quickly showing numerical values without the need for explicit string conversion.

void text(const std::string& text)

Displays the specified text on the OLED-display. In case the string is too long to be displayed in one line (21 characters) it will be wrapped automatically.

SignalManager

The SignalManager is responsible for initializing the individual SignalBoards correctly. It automatically enumerates all connected SignalBoards, assigns the correct I²C addresses and gives access to individual boards. For debugging purposes, error messages and general information will be printed to std::cerr.

void maskDevice(uint8_t address)

Excludes a specific I²C address from the enumeration process. This method can be called multiple times in order to mask more than one address. It can be used to permanently deactivate specific SignalBoards or in case additional I²C periphery has been added (e.g. the DisplayBreakout that will occupy the I²C address 0x3C).

bool initializeBoards(unsigned int dacResolution, unsigned int samplingTime)

Resets all SignalBoards connected to the system and runs a "standard procedure" consisting of the following steps:

  1. (global Reset)
  2. Initialization of connected Signalboards
  3. Assignment of I²C addresses according to the selected hardware address
  4. Passing through the different runlevels of the SignalBoards and configuring the DAC resolution and sampling rate
  5. Initiating the "running" state on all SignalBoards

The samplingTime is given in micro seconds and defaults to 62 µs resulting in a sampling rate of approximately 1/(62 µs) = 16 kHz.

std::vector<uint8_t> scanDevices()

Iterates over the whole (non masked) I²C address range and asks for an acknowledgment to the SystemRequest::Alive request. All properly initialized SignalBoards that respond to this request will be included in the returned list of I²C addresses.

std::vector<SignalGenerator>& generators()

Returns a list of all SignalGenerators that have been successfully enumerated.

std::vector<uint8_t> addresses() const

Returns a list of I²C addresses representing all active SignalBoards.

SignalGenerator& generator(uint8_t address)

Returns the SignalGenerator instance that maps to the specified address. Warning: Currently there are no checks if the address actually has been assigned.


void reset(uint8_t address)

Causes a specific SignalBoard to be resetted. Warning: This method usually is called within the initializeBoards method. In most use cases, you probably do not want to call it manually.

void reset()

Causes all (non masked) SignalBoard connected to the system to be resetted. Warning: This method usually is called within the initializeBoards method. In most use cases, you probably do not want to call it manually.

void initSystem()

Initializes all SignalBoards that have been resetted beforehand, causing them to enter Runlevel 0 Warning: This method usually is called within the initializeBoards method. In most use cases, you probably do not want to call it manually.

void assignAddresses()

Iterates through all possible SignalGenerator addresses and assigns a new I²C address where applicable. Causes all SignalGenerator instances to be deleted an re-instanciated. Please make sure, the SignalGenerators have been resetted and initialized before this method is called. Warning: This method usually is called within the initializeBoards method. In most use cases, you probably do not want to call it manually.

SignalGenerator

uint8_t address() const
SystemStatus status() const
void resetStatus() const
bool isAlive()
void finishR0()
void finishR1()
void setDACResolution(uint8_t resolution)
void setSamplingRate(uint32_t rate)
void finishR2()
void startSignalGeneration() const
void send(const std::array<FrequencyTable, 4>& data)
void send(const FrequencyTable& dataABCD)
void send(const FrequencyTable& dataA, [...], const FrequencyTable& dataD)
void shutdown()

PositionQuery

virtual QVector2D position() const
virtual QVector2D velocity() const
virtual float orientation() const
virtual float angularVelocity() const
virtual bool buttonPressed() const
virtual bool initialize()
virtual void update()
virtual void feedback(unsigned char r, unsigned char g, unsigned char b)
ConstantVelocityQuery
MousePositionQuery

Lowlevel API

All lowlevel functions can be accessed via the iowrap.h header file. The lowlevel API gives access to platformspecific functionality such as GPIO configuration, direct SPI communication as well as sending custom I²C commands to the SignalBoards.

GPIO

void GPIOSetDirection(unsigned int pin, GPIODirection direction)
void GPIOHigh(unsigned int pin)
void GPIOLow(unsigned int pin)
bool GPIOIsLow(unsigned int pin)
bool GPIOIsHigh(unsigned int pin)
void GPIOInit()
void GPIOFree()
void GPIOSetAddress(unsigned int address)
void GPIOSetBroadCast(bool value)

SPI

void SPIInit()
void SPIFree()
void SPISetDivider(unsigned int divider)
unsigned int SPIGetCurrentSpeed()
void SPISend(const uint16_t *data, int size)

I²C

void I2CInit()
void I2CFree()
bool I2CSetAddress(int address)
char I2CReadByte(int cmd)
bool I2CWriteByte(int cmd, unsigned char buffer)
bool I2CReadBlock(int cmd, uint16_t length, unsigned char *buffer)
bool I2CWriteBlock(int cmd, uint16_t length, const unsigned char* buffer)

About

Open Hardware- and Software-Plattform for controlling tactile Displays

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published