Myo SDK MATLAB MEX Wrapper

From Mark T. Wiki
(Redirected from Myo SDK MEX Wrapper)
Jump to navigation Jump to search

This page will be used to document the development the Matlab package, Myo SDK MATLAB MEX Wrapper, which is available both on MathWorks' File Exchange <ref name="myo-sdk-mex-wrapper" > Myo SDK MATLAB MEX Wrapper; Author: Mark Tomaszewski; Year: 2024; Source: http://www.mathworks.com/matlabcentral/fileexchange/55817-myo-sdk-matlab-mex-wrapper </ref> and my personal GitHub repository, MyoMex. <ref name="myo-mex" > Myo SDK MATLAB MEX Wrapper; Author: Mark Tomaszewski; Year: 2024; Source: https://github.com/mark-toma/MyoMex </ref>

Specifically, we'll address aspects of implementing the Myo SDK, the MEX wrapper for Myo SDK, and finally the Matlab class wrapper for the MEX file. Other information about using this Matlab package will not be documented here. Please refer to the files included with the package.

Check out this video Preview on YouTube to see a glimpse of what this package can do with acquiring data from Myo.

Overview

The goal of this project is to develop an m-code interface to acquire data from Myo. The target user groups for this m-code interface include undergraduate students who may have little-to-no programming experience to graduate student researchers who require repeatability of data collection results (i.e. consistent data sampling rate with minimal jitter).

Since we desire an m-code interface for users, but access to Myo is provided through the Myo SDK (a runtime library), the core of this project is in the development of a Matlab MEX function to create a bridge between the C++ runtime and Matlab's command line.

In the following sections, we take a bottom-up approach to providing insight on the design considerations for this project. We begin by building an application on the basic usage of Myo SDK before implementing a MEX interface wrapper for this functionality and subsequently wrapping the MEX interface in a Matlab class.

Myo SDK

The Myo SDK comes packaged with a runtime library, header and source files for the library and API implementation, and some examples of simple programs that implement the API to access data from Myo. This information is enough to get developers started building similarly simple applications on Myo. In this section, it's assumed that readers have already worked through the examples provided by the Myo SDK. We're only going to focus on some elements that are pertinent to this project.

Basic Implementation

To provide some context for the content herein, here's a minimal example usage of the Myo SDK.

#include "myo/myo.hpp"

class MyDataCollector : public myo::DeviceListener
{
public:
  void onConnect(myo::Myo* myo, uint64_t timestamp, FirmwareVersion firmwareVersion)
  {
    printf("onConnect\tMyo*=%20p\ttimestamp=%20ull\n",myo,timestamp);
  }
};

int main()
{
  MyDataCollector collector; // instantiate user-defined device listener object
  myo::Hub = hub("com.example.print-callbacks"); // initialize hub
  hub.addListener(&collector); // register device listener with hub
  myo::Myo* = hub.waitForMyo(1000); // wait a second for a device

  printf("Running hub for 1 second.\n");
  hub.run(1000); // run the hub for 1 second.
  printf("Done running hub.\n");

  return 0;
}

In this example, we notice a couple of key concepts that guide further development in program control flow and Myo data acquisition.

The statements in main() outline the required setup of the myo::Hub and subsequent call to hub.run(). The latter is a blocking call wherein program control is passed to the Myo SDK in order to execute calls back into the user-defined class, MyDataCollector.

The real functionality of Myo SDK that enables data acquisition from Myo is encapsulated in the user's implementation of a class that inherits from myo::DeviceListener. If any of the virtual member functions in myo::DeviceListener are implemented by a user-defined subclass that's registered with myo::Hub, then they are called into whenever hub.run() is invoked in the main program.

The instance of MyDataCollector in this example has implemented the onConnect() callback, and so it is executed during hub.run() if any connect events are triggered.

Device Data

The myo::DeviceListener class provides access to various device data including sensor data and device state updates. The following member functions of myo::DeviceListener provide access to this information.

void onPair(myo::Myo* myo, uint64_t timestamp, FirmwareVersion firmwareVersion) {}
void onUnpair(myo::Myo* myo, uint64_t timestamp) {}
void onConnect(myo::Myo* myo, uint64_t timestamp, FirmwareVersion firmwareVersion) {}
void onDisconnect(myo::Myo* myo, uint64_t timestamp) {}
void onArmSync(myo::Myo* myo, uint64_t timestamp, Arm arm, XDirection xDirection, float rotation, WarmupState warmupState) {}
void onArmUnsync(myo::Myo* myo, uint64_t timestamp) {}
void onUnlock(myo::Myo* myo, uint64_t timestamp) {}
void onLock(myo::Myo* myo, uint64_t timestamp) {}
void onPose(myo::Myo* myo, uint64_t timestamp, Pose pose) {}
void onOrientationData(myo::Myo* myo, uint64_t timestamp, const myo::Quaternion<float>& rotation) {}
void onAccelerometerData(myo::Myo* myo, uint64_t timestamp, const myo::Vector3<float>& accel) {}
void onGyroscopeData(myo::Myo* myo, uint64_t timestamp, const myo::Vector3<float>& gyro) {}
void onRssi(myo::Myo* myo, uint64_t timestamp, int8_t rssi) {}
void onBatteryLevelReceived(myo::Myo* myo, uint64_t timestamp, uint8_t level) {}
void onEmgData(myo::Myo* myo, uint64_t timestamp, const int8_t* emg) {}
void onWarmupCompleted(myo::Myo* myo, uint64_t timestamp, WarmupResult warmupResult) {}

The on<data>Data() functions provide access to new sensor data, whereas the other functions provide more general information, or meta data, relating to devices state. The former set of functions are the primary focus of data acquisition applications, and will be discussed further in this section,

  • onOrientationData()
  • onGyroscopeData()
  • onAcceleromaterData()
  • onEmgData()

Inertial Measurement Unit (IMU) Data

The IMU on Myo measures raw gyroscope (gyro) and accelerometer (accel) data and uses these data sources to estimate the orientation of the device with respect to an inertial reference frame encoded in a unit quaternion (quat). These IMU data samples are provided by Myo SDK in the same time instant and are obtained through their respective on<data>Data() functions.

  • void onOrientationData(Myo* myo, uint64_t timestamp, const Quaternion<float>& rotation) {}
  • void onGyroscopeData(Myo* myo, uint64_t timestamp, const Vector3<float>& gyro) {}
  • void onAccelerometerData(Myo* myo, uint64_t timestamp, const Vector3<float>& accel) {}

All IMU data is sampled on the Myo device at a rate of 50Hz (20ms sample time), and the timestamp values passed into the on<data>Data() functions are specified to be non-decreasing for successive samples. In practice, it's realized that successive timestamp values are typically strictly increasing (no duplicate sample times) so that identical timestamp values can be assumed to belong to the same sampling time instant.

The matter of collecting all IMU data in a synchronized way involves validating that a unique set of {quat, gyro, accel} has been collected in the event that a new timestamp has been received in one of these on<data>Data() functions.

Electromyography (EMG) Data

The Myo device also contains eight electromyography sensors from which an eight element array of signed eight bit integer intensity values is sampled in the emg data provided in the onEmgData() function.

  • void onEmgData(myo::Myo* myo, uint64_t timestamp, const int8_t* emg) {}

This data is sampled at 200Hz (5ms sample time) on the Myo device, but it is received by the Myo SDK in pairs of consecutive samples due to bandwidth limitations of the Bluetooth Smart protocol. Although the sampling of EMG data in its own time base suggests a straightforward process by which to validate this data, this fact slightly complicate the process.

The expected format for pairs of emg and timestamp data is,

timestamp(0)  emg(0)
timestamp(0)  emg(1)
timestamp(1)  emg(2)
timestamp(1)  emg(3)

...

timestamp(k)  emg(2*k)
timestamp(k)  emg(2*k+1)

...

Where the timestamp values can only be assumed as non-decreasing. That is, it should be assumed that consecutive timestamp values may be equal, or timestamp(k+1) == timestamp(k).

Because of these phenomena, the validation of emg data involves determining whether or not the emg samples with a previous timestamp form a set with even cardinality. Since the emg sample time is significantly small (5ms), and the Bluetooth transmission of this data is not verified, it's very well possible that entire time instants (two emg samples) can be lost in transmission.

Within the limitations of the best-effort policy for streaming emg data in Myo SDK, the best we can do in acquiring this data is enforce a similar best-effort policy in validating the received data. When a new timestamp values is received, or if more than a pair of emg samples has been received for the same timestamp value, we validate the previous few emg samples. If an odd number of emg samples is found for the most recent timestamp then we pad this set with one extra sample to conform with the expected data format. Note that if data loss is undetected, it will be lost in pair of samples. Users should take much care in validating that data is not being lost throughout application development since there is no way to fully characterize lost data in the stream received from Myo SDK.

Identifying Unique Myo Devices

One may be interested in acquiring data from multiple Myo devices in the same session from Myo SDK. In this care, one can use the myo::Myo* pointer that is passed into the myo::DevicesListener callbacks to test for equivalence between a new Myo and a previously received Myo.

A stripped down implementation of this technique may store the myo::Myo* pointers in a std::vector of, and rely on a single member function to performs both lookup of a new Myo from the vector and push back of a new Myo onto the vector. For example, consider the following.

class MyDataCollector : public myo::DeviceListener
{
std::vector<myo::Myo*> myos;
public:
  int getMyoID(myo::Myo* myo)
  {
    // return 1-indexed id of myo if it's in myos
    for (int id=0; id<myos.size(); ii++)
      if (myos[id]==myo)
        return (id+1);
    // otherwise, add myo to myos and return the id
    myos.push_back(myo);
    return myos.size();
  }
};

Device Listener Class

In order to receive data from Myo, application source code must implement a class that inherits from myo::DeviceListener. Implementations of some specific public member functions are then used as callbacks when new data is available from the device. We can add our own member variables and functions to this class to implement some additional features,

  • Store the Myo data in a queue
  • Write this data in callbacks
  • Read this data with public getter
  • Maintain consistent state throughout this process

MEX File

Matlab Class

References

</references>