NMotion Transport Library
Our library provides comprehensive support for communication between NMotion components and the main system. The library can communicate with the connected NLink Adapter via USB or directly with NMotion components via native CAN.
It features two distinct layers of abstraction: the Interface Layer, which manages all USB or native CAN interface operations, including methods and functionalities specific to communication protocols, and the Device Layer, which abstracts device-level functionalities, encompassing operations such as data handling and device management.
We offer both C/C++ and Python libraries. The core library is implemented in C++, while the Python library serves as a high-level wrapper that provides access to the C++ library.
Currently our library only supports components which are connected to PC via our own NLink Adapter
Basic Code Flow
Before using the library make sure all the NMotion components are properly connected to the CAN Bus and have been assigned unique Node IDs. The NMotion CLI tool can be used to configure each of these components easily.
While writing the code to control NMotion components using this library, start by creating an Interface object and then initializing it with a specified port — such as /dev/ttyACM0
for USB or can0
for native CAN bus. Once initialized, create a Device object using this interface object and the CAN Node ID of the device, which represents the connected NMotion device. After this, wait for sometime to make sure that the devices get connected to the interface. Then you can utilize the various methods provided by the Device object to interact with and control the components as required by your application. After calling each device method, it is suggested to add a delay so that the next command is executed only after the system completely executes the prior command. This delay is especially crucial for actuators and drivers with connected motors, as it allows time for their motion to complete before issuing another command. After completing operations with the Device object, ensure that the interface is closed to release resources and terminate communication properly.
/* Create an Interface object based on the interface type */
interface = Interface();
/* Initialize the interface based on the interface port: */
// For USB the interface_port value would be something like /dev/ttyACM0 or COM5
// For native CAN the interface_port value would be can0
interface.initialize(interface_port);
/* Create a Device object, by using the can_node_id of the device and passing the Interface object */
device = Device(can_node_id, interface);
// wait for sometime to make sure that the device's get connected to the interface
wait();
/* Utilize the device's methods as needed */
device.method1();
// wait for method1 to be completed
wait();
device.method2();
// wait for method2 to be completed
wait();
/* Close the interface to release resources */
interface.close();
Library Usage
As per the above codeflow the first step is to declare an interface class object. For that we have implmented two classes which inherits the core Interface
class:
USBInterface
: For USB related interface; mainly used to connect with NLink Adapter.CANInterface
: For native CAN related interface; mainly used to directly connect with NMotion components on the native CAN port of your system.
After creating the interface object, we need to initialise it. To initialise, we need to pass the interface port to object's intitialise method. For USBInterface
, the port will be the USB port onto which the NLink Adapter is connected (eg. /dev/ttyACM0
, COM3
...etc) and for CANInterface
, the port will be the name of the native CAN port (eg. can0
, can1
...etc)
After this, we need to create the device class object while passing the Interface object onto it. Then we can call various device's methods as needed and finally close the interface to release the resources.
C/C++
Introduction
The core C++ library has abstractions for the interface (Interface
parent class which is inherited by USBInterface
and CANInterface
) and the devices (like Actuator
, NDrive Z1
...etc).
The library has mainly two types of functions: one which sets the value on the device and another one which gets the value from the device.
- The model for the function which sets the value on the device is straight-forward. You pass the value to the function and then it returns the status indicating whether the communication between the device and the system was successful.
ret_status_t status; // variable to store return status
int config = 23; // value which needs to be set
// Pass config value into the device method and get the status as return value of the function
status = Device.setConfig(config);
- Since C++ doesn't natively support returning multiple values from a function unless a custom
struct
orclass
object is used, the function that retrieves values from the device utilizes a call by reference approach. The memory address of the variable onto which the data needs to be written is passed to the function and it returns the status indicating whether the communication between the device and the system was successful.
ret_status_t status; // variable to store return status
int config; // variable to store the config value
// Pass the memory address of the config variable to the get function
// Function will fetch the value from device and write it to the passed memory address
// Function will return status of the communication
status = Device.getConfig(&config);
Usage
Setup
For the purpose of explaining the library usage, assume that the hardware setup includes an NLink Adapter connected via USB to the system's /dev/ttyACM0
port, and an actuator with CAN Node ID 23 connected to the NLink Adapter. To control this actuator, a main.cpp
code will be written and built using the CMake program.
Download the headers and the shared library files from the Downloads page to ensure that necessary files are available for the build process. After extracting the downloaded zip file, you will find the headers and library files organized in the following directory structure:
nmotion_transport
├── include
| └── nmotion_transport
| ├── interface.hpp
| └── ... // Library header files
└── lib
└── libnmotion_transport.so // Shared Library suitable to your system's architechture
After extracting the files, create a code workspace that includes src
and include
folders, along with a CMakeLists.txt
file. Move the extracted library files (the nmotion_transport
folder) into the include
folder of the code workspace.
Once the files have been copied, the directory structure of the code workspace will be as follows:
├── src
| └── main.cpp
├── include
| └── nmotion_transport
| └── <library> // Library files which were downloaded earlier
└── CMakeLists.txt
Code
The main.cpp
file contains the code that uses the library to control the actuator. This code will move the actuator in position control mode and then retrieve the encoder count from the actuator's output encoder.
#include <iostream>
#include <thread>
#include <nmotion_transport/usb_interface.hpp>
#include <nmotion_transport/actuator.hpp>
int main(int argc, char const *argv[]) {
// Create an Interface object based on the interface type
USBInterface iface; // USB Interface
// Initialize the interface with port:
// For USB the interface_port value would be something like /dev/ttyACM0 or COM5
// For native CAN the interface_port value would be can0
// Currently we have connected the NLink adapter on /dev/ttyACM0
iface.initInterface("/dev/ttyACM0");
// Create a Device object by passing its CAN Node ID and the Interface object
// Currently we have an actuator connected to the NLink Adapter whose CAN Node ID is 23
Actuator act1(23, &iface);
// Wait for the device to get connected
std::this_thread::sleep_for(std::chrono::seconds(2));
// Utilize the device's methods as needed
// Set function: values are passed directly and the function returns status of communication
ret_status_t set_status = act1.setPositionControl(180, 100);
// Wait for the set method to finish
// Wait for the actuator to complete its motion
std::this_thread::sleep_for(std::chrono::seconds(2));
// Get function: address of the variable is passed and the function returns the status of the communication
int32_t count = 0;
ret_status_t get_status = act1.getMotorEncoderCount(&count);
// Wait is not required for the get function since we are only reading the value of whatever is there in the device
// Print the value
std::cout << "Encoder count for the actuator is:" << count << std::endl;
// Close the interface to release resources
iface.closeInterface();
return 0;
}
To build the program with the cmake
tool, we will use the below CMakeLists.txt
file. This file specifies the Nmotion Transport library files, ensuring that the code compiles and links against the necessary shared library and header files. Note that the library files vary depending on the platform, so this file must be adjusted accordingly.
cmake_minimum_required(VERSION 3.10)
project(get_set_data_actuator)
include_directories(include)
# We are using modern ABI, some compilers require this
add_compile_options(-D_GLIBCXX_USE_CXX11_ABI=1)
# Add Nmotion Library
include_directories(include/nmotion_transport/include)
add_library(nmotion_transport SHARED IMPORTED)
set_target_properties(nmotion_transport PROPERTIES
IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/include/nmotion_transport/lib/libnmotion_transport.so
)
add_executable(get_set_data_actuator src/main.cpp)
# Link against Nmotion library
target_link_libraries(get_set_data_actuator nmotion_transport)
Building and Running the Program
The following commands and steps needs to be followed to build and run the program.
On Linux, the process is simple and can be done using the default terminal. Navigate to the root of the code workspace and run the following commands in the terminal.
$ mkdir build && cd build
$ cmake ..
$ make
$ ./get_set_data_actuator
For C/C++ based development, we offer better support for Linux platforms, making them generally preferred over Windows. On Windows, using alternative Python libraries is often a better choice.
Python
Introduction
The Python library is a wrapper around the core C++ library, all the functions and objects which is available in the core library has been exposed using the wrapper.
The set and get methods in the python library is implemented as follows:
- The model for the function which sets the value on the device is same as that of the C++ set methods. You pass the value to the function and it returns the status indicating whether the communication between the device and the system was successful or not.
config = 23 # value which needs to be set
# Pass the config value to the device method and get the status as the return value of the function
status = Device.setConfig(config)
- The model for the function which gets the value is straight-forward, you call the function and it returns the status as well as the value together as a
tuple
.
# Call the get method directly and get the value and status inside a tuple
# The first value of the tuple will be the status followed by the get values.
(status, config) = Device.getConfig()
Usage
Setup
For the purpose of explaining the library usage, assume that the hardware setup includes an NLink Adapter connected via USB to the system's /dev/ttyACM0
port, and an actuator with CAN Node ID 23 connected to the NLink Adapter. To control this actuator, a main.py
script will be written.
Download the Python library from the Downloads page, extract it, and place the files in the root directory of your project. This is where main.py
will also reside. The resulting project directory structure will be as follows:
├── nmotion_transport
| └── <Python library files>
└── main.py
The Library is compatible with Python versions 3.8
, 3.9
and 3.10
.
Code
The main.py
file contains the code that uses the library to control the actuator. This code will move the actuator in position control mode and then retrieve the encoder count from the actuator's output encoder.
from nmotion_transport import USBInterface, Actuator
import time
# Create an Interface object and initialize based on the interface type
# In case of Python we create and initialise the interface at the same time.
# For USB the interface_port value would be something like /dev/ttyACM0 or COM5
# For native CAN the interface_port value would be can0
# Currently we have connected the NLink adapter on /dev/ttyACM0
iface = USBInterface("/dev/ttyACM0") # USB Interface
# Create a Device object by passing its CAN Node ID and the Interface object
# Currently we have an actuator connected to the NLink Adapter whose CAN Node ID is 23
act1 = Actuator(23, iface)
# Wait for the device to get connected
time.sleep(2)
# Utilize the device's methods as needed
# Set function: values are passed directly and the function returns status of communication
set_status = act1.setPositionControl(180,100)
# Wait for the set method to finish
# Wait for the actuator to complete its motion
time.sleep(2)
# Get funciton: function is called directly and it returns the status of the communication and the get value
(get_status, count) = act1.getEncoderCount()
# Wait is not required for the get function since we are only reading the value of whatever is there in the device
# Print the value
print(f'Encoder count is: {count}')
# Delete the actuator object before closing interface
del act1
# Close the interface to release resources
iface.close()
Running the program
Run the python program in the project directory by using the command:
$ python main.py
In case of errors, ensure that a compatible version of Python is being used. In some cases, it is necessary to use python3
instead of python
with the above command to use the compatible version.
Supported Devices & Versions
Device | Version |
---|---|
NDrive Z1 | Frimware Version = 0.3.2 , Hardware Version <= 1.1 |
Smart Actuators | Firmware Version <= 0.3.1 |
C/C++ API Reference
Return Status Enum
typedef enum {
RETURN_OK,
RETURN_ERROR,
RETURN_TIMEOUT,
RETURN_INVALID_ID,
RETURN_DEVICE_NOT_CONNECTED,
RETURN_INTERFACE_NOT_UP,
RETURN_WRONG_ARGUMENT,
} ret_status_t;
Description
Used to return status after calling a device's method.
Enumerator | Value | Description |
---|---|---|
RETURN_OK | 0 | Status OK |
RETURN_ERROR | 1 | Unclassified Error |
RETURN_TIMEOUT | 2 | Communication Timeout |
RETURN_INVALID_ID | 3 | Device ID is invalid |
RETURN_DEVICE_NOT_CONNECTED | 4 | Device is not connected on the interface |
RETURN_INTERFACE_NOT_UP | 5 | Interface is not running |
RETURN_WRONG_ARGUMENT | 6 | Passed Argument is not valid |
Interface Class
This is the parent class for USBInterface
and CANInterface
classes. The methods defined below will be available in child classes as well.
Interface.initInterface()
Interface::initInterface(std::string interface_name,
std::function<void(uint32_t, uint8_t)> on_new_dev_cb = nullptr,
std::function<void(uint32_t)> on_dev_disconnect_cb = nullptr)
Description
Invokes lower level code to initlialize the Interface
Parameters
Datatype | Variable | Description |
---|---|---|
std::string | interface_name | Interface identifier string eg. can0 , /dev/ttyACM0 , COM3 ...etc |
std::function<void(uint32_t, uint8_t)> | on_new_dev_cb | Callback function which will be called when a new device is connected. The arguments of the function are: • CAN Node ID of the device of type uint32_t • Value of uint8_t type variable represents the type of the device which is connected. • 0 means that an actuator is connected. • 1 means that a motor driver is connected. • 2 means that an IMU is connected. Return value of the function is void ie. it returns nothing. |
std::function<void(uint32_t)> | on_dev_disconnect_cb | Callback function which will be called when a device is disconnected. The argument of this function is the CAN Node ID of the device of type uint32_t and return value of the function is void i.e. it returns nothing. |
on_new_dev_cb
and on_dev_disconnect_cb
callback functions are optional and they will be assigned with null pointers if nothing is passed.
Interface.closeInterface()
uint8_t Interface::closeInterface()
Description
Invokes lower level code to close the Interface
Returns
Datatype | Description |
---|---|
uint8_t | Returns 1 for successful close or 0 for failure |
Python API Reference
Python library is a wrapper around the core C++ library and hence all the relations/objects which exists in core library will exist here as well.
Interface Class
This is the parent class for USBInterface
and CANInterface
classes. The methods defined below will be available in child classes as well.
Interface()
Interface(interface_name)
Description
Constructor for Interface class, it creates the Interface object and also invokes lower level code to initlialize the Interface.
Parameters
Datatype | Variable | Description |
---|---|---|
str | interface_name | Interface identifier string eg. can0 , /dev/ttyACM0 , COM3 ...etc |
Interface.close()
Interface.close()
Description
Invokes lower level code to close the Interface