This is the CleverHand library, a library for the CleverHand interface.
Each request from the computer to the controller is a 8 bytes long frame. The first byte is always the command, the rest of the bytes depend on the command.
Request to read a register from a module attached to the controller.
- 'r': read commad
- id : A 32-bits mask indicating which module to address. Note: 0b0110 address module 1 and 2
- n : number of bytes to read
- nc: number of bytes of the command
- cmd: read command to be sent to the module
Note: The number of bytes replyed by the controller len
is equal to n
*N with N the number of modules addressed by the id mask.
{ signal: [
{ name: 'Tx', wave: 'x==|===.|xxxxxxx', data: ['r', 'id', 'n', 'nc', 'cmd'] },
{ name: 'Rx', wave: 'xxxxxxxxx=|==.|x', data: ['ts', 'len', 'val']},
{ node: '..E.F.G..A.BC..D'}
],
head: { text: 'Read command' },
edge: [ 'A+B 8bytes', 'C+D len bytes' , 'E+F 4 bytes', 'G+A nc bytes']
}
Request to write a register from a module attached to the controller. There is no response to this command.
- 'w': write command
- id : A 32-bits mask indicating which module to address. Note: 0b0110 address module 1 and 2
- n : number of bytes to write
- nc: number of bytes of the command
- cmd: write command to be sent to the module
- val: value to write
{ signal: [
{ name: 'Tx', wave: 'x==|====.|=|x', data: ['w', 'id', 'reg', 'n', 'nc', 'cmd','val'] },
{ node: '..A.B..E..C.D'}
],
head: { text: 'Write command' },
edge: [ 'A+B 4 bytes', 'C+D n bytes', 'E+C nc bytes']
}
Request to setup the controller. Expected response is the number of modules attached to the controller.
- 's': setup command
{ signal: [
{ name: 'Tx', wave: 'x=xxxxx', data: ['s'] },
{ name: 'Rx', wave: 'xx=|==x', data: ['ts', '1', 'nb']},
{ node: '..A.B'}
],
head: { text: 'Setup command' },
edge: [ 'A+B 8bytes' ]
}
Request to know the number of modules attached to the controller.
- 'n': number of modules command
{ signal: [
{ name: 'Tx', wave: 'x=xxxxx', data: ['n'] },
{ name: 'Rx', wave: 'xx=|==x', data: ['ts', '1', 'nb']},
{ node: '..A.B'}
],
head: { text: 'Number of modules command' },
edge: [ 'A+B 8bytes' ]
}
Request to test the communication with the controller. The expected response is the same frame sent by the computer.
- 'm': mirror command
- v1 : value 1
- v2 : value 2
- v3 : value 3
{ signal: [
{ name: 'Tx', wave: 'x====xxxxxxx', data: ['m', 'v1', 'v2', 'v3'] },
{ name: 'Rx', wave: 'xxxxx=|====x', data: ['ts', '3', 'v1', 'v2', 'v3']},
{ node: '.....A.B.C.D'}
],
head: { text: 'Mirror command' },
edge: [ 'A+B 8bytes' ]
}
Request to know the version of the controller.
- 'v': version command
{ signal: [
{ name: 'Tx', wave: 'x=xxxxxx', data: ['v'] },
{ name: 'Rx', wave: 'xx=|===x', data: ['ts', '2', 'V.M', 'V.m']},
{ node: '..A.BCDE'}
],
head: { text: 'Version command' },
edge: [ 'A+B 8bytes', 'C+D Major', 'D+E Minor' ]
}
The library is written in C++ and is composed of a namespace ClvHd
, a class Device
, a class Controller
and a class abstract class Module
.
The Device
class is the main class of the library and contains a Controller
object and a list of Module
objects. The Controller
class is used to communicate with the controller and the Module
class is an abstract class that represents a module attached to the controller.
As there is an infinite possible type of module, the Module
class is abstract and the user can create a new class that inherits from Module
to represent a new type of module. The user must implement static methods to enable the user to use access the module from the Device
class.
classDiagram
class Device {
Controller controller
Module* modules[]
+Device(int n)
+~Device()
+setup()
}
class Controller {
+open_connection()
+close_connection()
+read()
+write()
}
class Module {
+read()
+write()
}
class EMG_ADS1293 {
+setup()
+start()
+getModule()
+read()
}
Device --> Controller
Device --> Module
EMG_ADS1293 --> Module
The Device
class is the main class of the library and contains a Controller
object and a list of Module
objects pointer.
Initially, module array is filled with nullptr
. When the setup()
static method of a specific type (e.g. EMG_ADS1293
) is called, test are made to identify which module have this type, then an instance of the module is created and stored in the module array at the index corresponding to the module id.
Example: A controller with 3 ADS1293 modules attached and 1 LIS3DH module attached. When the setup()
method of the EMG_ADS1293
class is called, the function will read the RevID register of the 4 modules, the 3 first modules will reply with the correct value and the last module will reply with an error. The Device
class will create 3 instances of the EMG_ADS1293
class and store them in the module array at the index corresponding to the module id. Then the setup()
method of the LIS3DH
class is called, the function will read the WHO_AM_I register of the remaining uninstanciated module and create an instance of the LIS3DH
class and store it in the module array at the index corresponding to the module id.
The Controller
class is used to communicate with the controller board. It provides basic functionalities to read/write registers of the modules attached to the controller.
The Module
class is an abstract class that represents a module attached to the controller.
The EMG_ADS1293
class is used to read EMG data from the CleverHand board.
To create a new module type, follow these steps:
-
Inherit from the
Module
class:class NewModuleType : public ClvHd::Module { public: NewModuleType() { /* Constructor implementation */ } ~NewModuleType() { /* Destructor implementation */ } // Implement specific methods for the new module static void setup(ClvHd::Device &device); static void start(ClvHd::Device &device); static NewModuleType* getModule(ClvHd::Device &device, int id); static uint64_t read(ClvHd::Device &device, double *sample); };
-
Implement the required static methods:
setup()
: Detect, instanciate and configure modules of this type attached to the controller.foo()
: any other methods required to interact with the module.
-
Use the
Controller
class for communication: Ensure the new module interacts with theDevice
class to use theController
class for communication with the controller board and the modules array.
- CMake
- Make
- build-essential
- (optional: liblsl)
mkdir build
cd build
cmake .. -DBUILD_EXAMPLES=ON -DBUILD_PYTHON=ON
make
Here's an example of how to use the library in a main.cpp
file:
#include <iostream>
#include <vector>
#include <iomanip>
#include <unistd.h>
#include "ClvHd.h"
int main()
{
std::cout << "CleverHand Serial Interface:" << std::endl;
try
{
ClvHd::Device device(3);
// Open the serial connection between the computer and the controller board
device.controller.open_connection("/dev/ttyACM0", 500000, O_RDWR | O_NOCTTY);
usleep(500000);
// Setup the device (count and find the type of modules attached)
device.setup();
// Setup the EMG modules
bool chx_enable[3] = {true, false, false}; // Enable channel 1, disable channels 2 and 3
int route_table[3][2] = {{0, 1}, {0, 1}, {0, 1}}; // Electrodes configuration
bool chx_high_res[3] = {true, true, true}; // Enable high resolution mode
bool chx_high_freq[3] = {true, true, true}; // Enable high frequency mode
int R1[3] = {4, 4, 4}; // Gain R1 of the INA channels
int R2 = 4; // Gain R2 of the INA channels
int R3[3] = {4, 4, 4}; // Gain R3 of the INA channels
// Create and setup the EMG modules in the device
ClvHd::EMG_ADS1293::setup(device, chx_enable, route_table, chx_high_res, chx_high_freq, R1, R2, R3);
// Start the EMG modules
ClvHd::EMG_ADS1293::start_acquisition(device);
std::vector<double> sample(6);
for(int t = 0;; t++)
{
uint64_t timestamp = ClvHd::EMG_ADS1293::read_all(device, sample.data());
std::cout << "timestamp: " << timestamp << " ";
for(int i = 0; i < 6; i++)
std::cout << std::fixed << std::setprecision(2) << sample[i] << " ";
std::cout << std::endl;
usleep(1000);
}
}
catch(std::exception &e)
{
std::cerr << "[ERROR] Got an exception: " << e.what() << std::endl;
}
catch(std::string str)
{
std::cerr << "[ERROR] Got an exception: " << str << std::endl;
}
return 0; // success
}