Skip to content

Theory of operation

Zoltan Derzsi edited this page Jun 27, 2018 · 17 revisions

The toolbox uses a hybrid approach: Most functions are available through Matlab's shared library support, which allows the direct use of the functions described in the API. However, there are functions that don't work properly with this approach, so I wrote handler functions in C, and everything gets compiled during toolbox set-up.

During the execution of the RUNME.m script, Matlab searches for the available functions in the library and the header file, and pastes these functions together in its own binary file. Then, all needs to be done is to make sure that the library is loaded:

loadlibrary(<libname>, <prototypes.m>)

and then, the functions are made to be available in a wrapper function as:

[output] = calllib('libname', 'function_name', <input arguments>)

I had to separate the 32 and 64-bit function calls as the functions are located in different library files. I did this by looking for a file I generated during the set-up process. new_or_old.m then can be used to call the appropriate function. Additionally, the Linux library is separated too, because the same Optotrak API library has three different names:

libname is:
    -'oapi' for 32-bit Windows
    -'oapi64' for 64-bit Windows
    -'liboapi' for Linux, both 32- and 64-bits

Generally, in Matlab, overwriting a function's input argument during function call by the function itself is bad practice. And doing this via an external library leads to access violations, which causes it to crash ever so often. I generated the pointers myself including typecasting NDI's own structures and variable types so NDI's software can work within the space of the function, and Matlab will take care of the memory management. So, all the input variables are return variables too, so Matlab will return them as if they were 'normal' functions. Also, this way you can see if there have been variable corruption due to type conversion. I also added a fail variable to be the very first return value, which helps debugging. If you don't want to handle all return parameters, ignore it with a tilde:

[fail, variable_you_want, ~] = function(variable_you_want, variable_you_don't_want);

if(fail)
    fprintf('the cleverly written function above that should never fail has failed.\n')
    optotrak_tell_me_what_went_wrong;
end

This approach works for hardware management function (for example OptotrakGetStatus()), but it doesn't work on data handling functions, such as DataGetLatest3D(). This is due to the limitations of Matlab's shared library support. When using this feature, programmers can find recommendations for implementing their functions in C as a shared library to be used with Matlab, not to use functions that have a changing number of arguments, or pointer arguments. Matlab doesn't support passing nested structures, and it has a hard time with pointer arguments. There is no convenient error management: if a piece of C code writes outside its allocated memory range, Matlab will crash.

When looking at the NDI Optotrak API manual, you will find that nearly all functions that access data are written with pointer arguments. Sometimes, especially with the data buffer handling functions, the data is not even organised into structures, the manual just tells you to allocate a piece of memory to a void *, and hope for the best.

This lead me to write data management functions as .mex* files, which call an API function, and then re-organises the data in a way that is easier to handle with Matlab: these functions are now returning the data in a 2D array, and I also replaced BAD_FLOAT with a NaN for markers that are not visible in each frame.