License requirement
The functionality described requires a MANUS Bodypack
or a MANUS license dongle
with one of the following licenses:
Core Pro
Core XR
Core Xsens Pro
Core Qualisys Pro
Core OptiTrack Pro
Demo
, or a Feature
license with the SDK
feature enabled.
SDK Minimal Client example
Introduction
The SDK Minimal client is an example client that demonstrates the basic functionality of the MANUS SDK. It will go over what is minimally required to get the SDK up and running and demonstrate how to receive raw skeleton data from all connected gloves. The SDKMinimalClient covers the following steps:
-
- The SDK is initialized using the
CoreSdk_Initialize(SessionType::SessionType_CoreSDK, t_Remote)
function call. This sets up the session type and specifies whether the client will work inintegrated
orremote
mode. - The coordinate system for the client is set using the
CoreSdk_InitializeCoordinateSystemWithVUH()
function.
- The SDK is initialized using the
-
- When not running in
integrated
mode. The client attempts to connect locally by calling theConnectLocally()
function in a loop until successful. - Once connected, the client sets the RawSkeletonHandMotion to auto.
- In the main loop, it checks for new raw skeleton data and prints the first node position and rotation.
- When not running in
-
- The
PrintRawSkeletonNodeInfo
function demonstrates how to interpret the data coming in from the RawSkeletonStream. - It prints the position and rotation of the first node in the first skeleton and interprets and prints the raw skeleton data.
- The
The SDK Minimal client is composed of the ManusSDK
library. For Windows this is the ManusSDK.dll
and for Linux libManusSDK.so
or libManusSDK_Integrated.so
. The SDKMinimalClient.cpp
files and some headers found in the include folder.
libManusSDK_Integrated.so
The libManusSDK_Integrated.so
can only be used for the integrated
mode of the SDK, and is not able to connect to a MANUS Core on the network. The libManusSDK.so
can be used for both the remote
and integrated
modes of the SDK. It's advantageous to use the libManusSDK_Integrated.so
to get around the specific dependency requirements of the libManusSDK.so
.
Initialize
Before using any functionality of the SDK, it is necessary to initialize it. This ensures that the system is set up correctly and ready for use.
The PlatformSpecificInitialization()
function is an initializer used that prepares console output for the specific OS, but it is not required in a non-console environment.
The app then prompts the user what mode they would like to run the app in.
Core Integrated
mode: The SDK is integrated into the client application.Core Local
mode: The SDK will connect to a MANUS Core running locally on this machine.Core Remote
mode: The SDK will search and connected to a MANUS Core instance on the network.
During the InitializeSDK()
function, the SDK is initialized and sets the mode to integrated
or remote
and specifies the type of session. Typically, this will be of type SessionType_CoreSDK
.
Next, the necessary callbacks are registered using the RegisterAllCallbacks()
function. For this example only the CoreSdk_RegisterCallbackForRawSkeletonStream
is registered. It is important to note that there are additional callbacks beyond this one, but they are not included in this example. For more information on other callbacks, please refer to the SDK Client article.
After registering the callbacks, the coordinate system used by the client is set using the CoreSdk_InitializeCoordinateSystemWithVUH()
function. In this example, a z-up, x-positive, right-handed coordinate system is used. It is recommended to align the coordinate system with the application being developed, as this will make it easier to work with the data by letting MANUS Core handle the conversion.
There are two ways to set up the coordinate system: using the VUH (View, Up, Handedness) system or using the Direction system.
``` C++ title="Coordinate system initialization"
CoordinateSystemVUH t_VUH;
CoordinateSystemVUH_Init(&t_VUH);
t_VUH.handedness = Side::Side_Right;
t_VUH.up = AxisPolarity::AxisPolarity_PositiveZ;
t_VUH.view = AxisView::AxisView_XFromViewer;
t_VUH.unitScale = 1.0f; //1.0 is meters, 0.01 is cm, 0.001 is mm.
// The above specified coordinate system is used to initialize and the coordinate space is specified (world vs local).
const SDKReturnCode t_CoordinateResult = CoreSdk_InitializeCoordinateSystemWithVUH(t_VUH, true);
// this is an example of an alternative way of setting up the coordinate system instead of VUH (view, up, handedness)
CoordinateSystemDirection t_Direction;
t_Direction.x = AxisDirection::AD_Right;
t_Direction.y = AxisDirection::AD_Up;
t_Direction.z = AxisDirection::AD_Forward;
const SDKReturnCode t_InitializeResult = CoreSdk_InitializeCoordinateSystemWithDirection(t_Direction, true);
The unit scale is specified as a float value. A scale of 1
represents meters, 0.01
represents centimeters, and 0.001
represents millimeters.
The second parameter of the CoreSdk_InitializeCoordinateSystemWithVUH()
function indicates whether the coordinates should be set as world coordinates or relative coordinates. In this case, it is set to false to use relative coordinates.
Once the initialization is complete, the SDK is ready to be used, but it is not yet connected to an instance of MANUS Core.
Connection
When running the example it follows the following command structure:
- Connect when not running integrated, if not successful, just wait and retry.
- Set the hand motion mode to auto. Auto will make the hand move based on available tracking data. If no trackers are available IMU rotation will be used. This can alternatively be set to any of the HandMotion enum values (HandMotion_None, HandMotion_Auto, HandMotion_Tracker, HandMotion_Tracker_RotationOnly, HandMotion_IMU).
- Start the main loop.
- Check if there is new raw skeleton data.
- If there is, signal that there is and print the first node's position and rotation.
- In case of the first time data is received, some information about the node structure is printed.
- Wait for the escape key to exit.
void SDKMinimalClient::Run()
{
// first loop until we get a connection
m_ConnectionType == ConnectionType::ConnectionType_Integrated ?
ClientLog::print("minimal client is running in integrated mode.") :
ClientLog::print("minimal client is connecting to MANUS Core. (make sure it is running)");
while (Connect() != ClientReturnCode::ClientReturnCode_Success)
{
// not yet connected. wait
ClientLog::print("minimal client could not connect.trying again in a second.");
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
if (m_ConnectionType != ConnectionType::ConnectionType_Integrated)
ClientLog::print("minimal client is connected, setting up skeletons.");
// set the hand motion mode of the RawSkeletonStream. This is optional and can be set to any of the HandMotion enum values. Default = None
// auto will make it move based on available tracking data. If none is available IMU rotation will be used.
const SDKReturnCode t_HandMotionResult = CoreSdk_SetRawSkeletonHandMotion(HandMotion_Auto);
if (t_HandMotionResult != SDKReturnCode::SDKReturnCode_Success)
{
ClientLog::error("Failed to set hand motion mode. The value returned was {}.", (int32_t)t_HandMotionResult);
}
while (m_Running)
{
// check if there is new data available.
m_RawSkeletonMutex.lock();
delete m_RawSkeleton;
m_RawSkeleton = m_NextRawSkeleton;
m_NextRawSkeleton = nullptr;
m_RawSkeletonMutex.unlock();
if (m_RawSkeleton != nullptr && m_RawSkeleton->skeletons.size() != 0)
{
// print whenever new data is available
ClientLog::print("raw skeleton data obtained for frame: {}.", std::to_string(m_FrameCounter));
PrintRawSkeletonNodeInfo();
m_FrameCounter++;
}
std::this_thread::sleep_for(std::chrono::milliseconds(33)); // Roughly 30fps, good enough to show the results, but too slow to retrieve all data.
if (GetKeyDown(' ')) // press space to exit
{
m_Running = false;
}
}
}
The Connect
function in the SDK Minimal Client attempts to establish a connection to a MANUS Core instance. This is only relevant when not running in integrated
mode. The function performs the following steps:
-
Look for Hosts:
- The function starts by calling
CoreSdk_LookForHosts
to search for available MANUS Core hosts. It searches locally if the connection type is set toConnectionType_Local
. - If the search fails, the function returns
ClientReturnCode_FailedToFindHosts
.
- The function starts by calling
-
Retrieve Number of Hosts:
- The function retrieves the number of available hosts found using
CoreSdk_GetNumberOfAvailableHostsFound
. - If the retrieval fails or no hosts are found, the function returns
ClientReturnCode_FailedToFindHosts
.
- The function retrieves the number of available hosts found using
-
Get Available Hosts:
- The function allocates memory for the available hosts and retrieves their information using
CoreSdk_GetAvailableHostsFound
. - If the retrieval fails, the function returns
ClientReturnCode_FailedToFindHosts
.
- The function allocates memory for the available hosts and retrieves their information using
-
Host Selection:
- If not connecting locally and multiple hosts are found, the function prompts the user to select a host.
- The user is asked to input the host number, and if the input is invalid, the function returns
ClientReturnCode_FailedToConnect
.
-
Connect to Host:
- The function attempts to connect to the selected host using
CoreSdk_ConnectToHost
. - If the connection fails, the function returns
ClientReturnCode_FailedToConnect
.
- The function attempts to connect to the selected host using
-
Return Success:
- If all steps are successful, the function returns
ClientReturnCode_Success
.
- If all steps are successful, the function returns
ClientReturnCode SDKMinimalClient::Connect()
{
bool t_ConnectLocally = m_ConnectionType == ConnectionType::ConnectionType_Local;
SDKReturnCode t_StartResult = CoreSdk_LookForHosts(1, t_ConnectLocally);
if (t_StartResult != SDKReturnCode::SDKReturnCode_Success)
{
return ClientReturnCode::ClientReturnCode_FailedToFindHosts;
}
uint32_t t_NumberOfHostsFound = 0;
SDKReturnCode t_NumberResult = CoreSdk_GetNumberOfAvailableHostsFound(&t_NumberOfHostsFound);
if (t_NumberResult != SDKReturnCode::SDKReturnCode_Success)
{
return ClientReturnCode::ClientReturnCode_FailedToFindHosts;
}
if (t_NumberOfHostsFound == 0)
{
return ClientReturnCode::ClientReturnCode_FailedToFindHosts;
}
std::unique_ptr<ManusHost[]> t_AvailableHosts;
t_AvailableHosts.reset(new ManusHost[t_NumberOfHostsFound]);
SDKReturnCode t_HostsResult = CoreSdk_GetAvailableHostsFound(t_AvailableHosts.get(), t_NumberOfHostsFound);
if (t_HostsResult != SDKReturnCode::SDKReturnCode_Success)
{
return ClientReturnCode::ClientReturnCode_FailedToFindHosts;
}
uint32_t t_HostSelection = 0;
if (!t_ConnectLocally && t_NumberOfHostsFound > 1)
{
ClientLog::print("Select which host you want to connect to (and press enter to submit)");
for (size_t i = 0; i < t_NumberOfHostsFound; i++)
{
auto t_HostInfo = t_AvailableHosts[i];
ClientLog::print("[{}] hostname: , IP address: {}, version {}.{}.{}", i + 1, t_HostInfo.hostName, t_HostInfo.ipAddress, t_HostInfo.manusCoreVersion.major, t_HostInfo.manusCoreVersion.minor, t_HostInfo.manusCoreVersion.patch);
}
uint32_t t_HostSelectionInput = 0;
std::cin >> t_HostSelectionInput;
if (t_HostSelectionInput <= 0 || t_HostSelectionInput > t_NumberOfHostsFound)
return ClientReturnCode::ClientReturnCode_FailedToConnect;
t_HostSelection = t_HostSelectionInput - 1;
}
SDKReturnCode t_ConnectResult = CoreSdk_ConnectToHost(t_AvailableHosts[t_HostSelection]);
if (t_ConnectResult == SDKReturnCode::SDKReturnCode_NotConnected)
{
return ClientReturnCode::ClientReturnCode_FailedToConnect;
}
return ClientReturnCode::ClientReturnCode_Success;
}
Raw Skeleton Stream Callback
The PrintRawSkeletonNodeInfo
function demonstrates how to interpret the data coming in from the RawSkeletonStream. It will print the position and rotation of the first node in the first skeleton, as well as interpreting and printing the raw skeleton data. Here's a detailed explanation of the function:
-
Initial Check:
- The function first checks if the
m_RawSkeleton
isnullptr
or if theskeletons
vector is empty. If either condition is true, it returns immediately. - If the
skeletons
vector is not empty and the first skeleton has nodes, it prints the position and rotation of the first node in the first skeleton.
- The function first checks if the
-
Interpreting Raw Skeleton Data:
- The function retrieves the
gloveId
and the node count for the first skeleton. - It then calls
CoreSdk_GetRawSkeletonNodeCount
to get the number of nodes in the skeleton. If the call fails, an error message is printed, and the function returns.
- The function retrieves the
-
Getting Hierarchy Data:
- The function allocates memory for an array of
NodeInfo
structures to hold the hierarchy data. - It calls
CoreSdk_GetRawSkeletonNodeInfoArray
to fill the array with the hierarchy data. If the call fails, an error message is printed and the function returns.
- The function allocates memory for an array of
-
Printing Node Information:
- The function prints the glove data and the node information for each node in the skeleton. The information includes the node ID, side, chain type, finger joint type, and parent node ID. More information on the
NodeInfo
structure can be found in the Skeleton article. - After printing the information, the allocated memory for the
NodeInfo
array is deleted, and them_PrintedNodeInfo
flag is set totrue
as to only do this once.
- The function prints the glove data and the node information for each node in the skeleton. The information includes the node ID, side, chain type, finger joint type, and parent node ID. More information on the
void SDKMinimalClient::PrintRawSkeletonNodeInfo()
{
if ((m_RawSkeleton == nullptr || m_RawSkeleton->skeletons.size() == 0) || m_PrintedNodeInfo)
{
if (m_RawSkeleton->skeletons.size() != 0 && m_RawSkeleton->skeletons[0].nodes.size() != 0) {
// prints the position and rotation of the first node in the first skeleton
ManusVec3 t_Pos = m_RawSkeleton->skeletons[0].nodes[0].transform.position;
ManusQuaternion t_Rot = m_RawSkeleton->skeletons[0].nodes[0].transform.rotation;
ClientLog::print("Node 0 Position: x {} y {} z {} Rotation: x {} y {} z {} w {}", t_Pos.x, t_Pos.y, t_Pos.z, t_Rot.x, t_Rot.y, t_Rot.z, t_Rot.w);
}
return;
}
// this section demonstrates how to interpret the raw skeleton data.
// how to get the hierarchy of the skeleton, and how to know bone each node represents.
uint32_t t_GloveId = 0;
uint32_t t_NodeCount = 0;
t_GloveId = m_RawSkeleton->skeletons[0].info.gloveId;
t_NodeCount = 0;
SDKReturnCode t_Result = CoreSdk_GetRawSkeletonNodeCount(t_GloveId, t_NodeCount);
if (t_Result != SDKReturnCode::SDKReturnCode_Success)
{
ClientLog::error("Failed to get Raw Skeleton Node Count. The error given was {}.", (int32_t)t_Result);
return;
}
// now get the hierarchy data, this needs to be used to reconstruct the positions of each node in case the user set up the system with a local coordinate system.
// having a node position defined as local means that this will be related to its parent.
NodeInfo* t_NodeInfo = new NodeInfo[t_NodeCount];
t_Result = CoreSdk_GetRawSkeletonNodeInfoArray(t_GloveId, t_NodeInfo, t_NodeCount);
if (t_Result != SDKReturnCode::SDKReturnCode_Success)
{
ClientLog::error("Failed to get Raw Skeleton Hierarchy. The error given was {}.", (int32_t)t_Result);
return;
}
ClientLog::print("Received Skeleton glove data from Core. skeletons:{} first skeleton glove id:{}", m_RawSkeleton->skeletons.size(), m_RawSkeleton->skeletons[0].info.gloveId);
ClientLog::print("Printing Node Info:");
// prints the information for each node, the chain type will which part of the body it is. The finger joint type will be which bone of the finger it is.
for (size_t i = 0; i < t_NodeCount; i++)
{
ClientLog::printWithPadding("Node ID: {} Side: {} ChainType: {} FingerJointType: {}, Parent Node ID: {}",2, std::to_string(t_NodeInfo[i].nodeId), t_NodeInfo[i].side, t_NodeInfo[i].chainType, t_NodeInfo[i].fingerJointType, std::to_string(t_NodeInfo[i].parentId));
}
delete[] t_NodeInfo;
m_PrintedNodeInfo = true;
}