Measurement Studio for VC++

cancel
Showing results for 
Search instead for 
Did you mean: 

Problem with using DAQmx library in Unity3D (C#)

Solved!
Go to solution

Hello everyone,

 

I'm trying to use the DAQmx C library in Unity to receive signal from National Instruments hardware in Unity.

 

Because the C library is static and there is no .dll file, only .lib and header file, I created a wrapper dll that I chose which DAQmx method to integrate into Unity.

 

Currently I implemented the following methods:

DAQmxCreateTask(taskName, taskHandle);
DAQmxStopTask(taskHandle);
DAQmxStartTask(taskHandle);
DAQmxCreateAIVoltageChan(taskHandle, physicalChannel, nameToAssignToChannel, terminalConfig, minVal, maxVal, units, customScaleName);
DAQmxSetSampTimingType(taskHandle, data);
DAQmxReadAnalogF64(taskHandle, numSampsPerChan, timeout, fillMode, readArray, arraySizeInSamps, sampsPerChanRead, reserved);
DAQmxCfgSampClkTiming(taskHandle, source, rate, activeEdge, sampleMode, sampsPerChan);
DAQmxGetExtendedErrorInfo(errorString, bufferSize);

When I try to replicate this example "Acq-IntClk" in Unity, only my DAQmxCreateTask() call returned a 0, and when I call DAQmxCreateAIVoltageChan(); DAQmxCfgSampClkTiming(); DAQmxStartTask(); DAQmxReadAnalogF64(); they all returned error code -200088, which seemed to be this https://knowledge.ni.com/KnowledgeArticleDetails?id=kA00Z0000019QMjSAM&l=en-US , meaning "Task specified is invalid or does not exist."

 

Further more, when I try to use DAQmxGetExtendedErrorInfo() to get the error message, it doesn't seem to properly write value to the char[] I'm passing in. And it doesn't seem to work with string either. However when I try to pass an empty string (string errBuff = "";) with a char[] size of 2048, Unity will crash, which means that Unity is accessing the C library successfully? (DAQmxCreateTask() returns 0 should also prove this)

 

I'm a Unity developer, and I don't have any previous experience with DAQmx. I've successfully integrated and using other native library in Unity before, but I'm stuck on this one.

0 Kudos
Message 1 of 8
(6,338 Views)

First, I'm not sure what you refer to about creating a DLL from the DAQmx lib file. That lib file is simply an import library for the actual DAQmx dll file anyhow, so there IS actually a DLL file you can call. If you need some DLL import directives in Unity3D similar to C#'s Interop DLLImport declarations, since you can't link in lib files. All you need to do is directly referencing the DAQmx user space DLL nicaiu.dll that gets installed in your System directory by the DAQmx installer.

 

The problem you see could be either that your turning the daqmx.lib into a daqmx.dll didn't quite initialize the real daqmx dll correctly or it could be something with your Unity3D import directives not being quite right. This Interop stuff is always tricky to do right.

Rolf Kalbermatter  My Blog
DEMO, Electronic and Mechanical Support department, room 36.LB00.390
Message 2 of 8
(6,273 Views)

And since Unity3D seems to use C# in fact, you should not need to have to create your own Interop import declarations to use DAQmx from it.

 

This document describes the installation of DAQmx and other driver support for Visual Studio C# or other .Net applications. You should be able to import the according libraries directly in your C# project after that. 

Rolf Kalbermatter  My Blog
DEMO, Electronic and Mechanical Support department, room 36.LB00.390
0 Kudos
Message 3 of 8
(6,263 Views)

Hi, thanks for the help.

 

I tried to use the .net DLL for DAQmx in Unity, but it shows up as native plugin instead of managed plugin.

 

When I look this up, other people are having the same problem, and they suspect that DAQmx .net DLL is a "mixed library".

 

So I then try to wrap the c library (which according to internet is a "static library" hence no DLL for it) into a C++ DLL and use it in Unity.

 

It looks like at least this method is partially working, as the CreateTask() is returning a "0", meaning it succeeded, but all other methods returns the same error code -200088.

0 Kudos
Message 4 of 8
(6,252 Views)
Solution
Accepted by SnowTV

While the DAQmx .Net interface is of course a mixed library internally I was under the impression that its interface is fully managed. Under the hood it simply calls into the real DAQmx DLLs which of course are fully native. So I'm not quite sure what the problem is here. Is Unity3D only able to call fully managed DLLs? Doesn't it support Interop calls, that are used to call into native libraries under the hood?

Rolf Kalbermatter  My Blog
DEMO, Electronic and Mechanical Support department, room 36.LB00.390
0 Kudos
Message 5 of 8
(6,243 Views)

Yes, I believe Unity cannot read mixed library. It sees the .Net dll as unmanaged.

 

However, I succeeded using LabView to get the readings and send to Unity through UDP.

 

Thanks for the help!

0 Kudos
Message 6 of 8
(5,866 Views)

@rolfk : You saved the day. Using "nicaiu.dll" was the answer. I've been struggling with this issue for such a long time, but your hint solved it! Thank you so much.

0 Kudos
Message 7 of 8
(33 Views)

By the way, here is an example. This code uses keyboard inputs, UDP packets (only skeleton: actual parser and control logic have not been implemented), and NIDAQ inputs (only the first three AIs are implemented) to control an Agent in Unity. You need to specify the device name and channels in the inspector. This code has been confirmed to work with USB6001 in Unity 6.3.

 

 

 

//AgentController.cs

 

using UnityEngine;

using UnityEngine.InputSystem;

using System;

using System.Net;

using System.Net.Sockets;

using System.Text;

using System.Collections.Generic;

using System.Threading;

using System.Runtime.InteropServices;

using System.Diagnostics;

 

/// <summary>

/// Threaded Controller managing Keyboard, UDP, and NI-DAQmx Analog Inputs.

/// Background threads handle I/O to keep the main Unity Render/Physics thread smooth.

/// </summary>

public class AgentController : MonoBehaviour

{

    [Header("Movement Settings")]

    public float moveSpeed = 10f;

    public float rotationSpeed = 100f;

 

    [Header("UDP Configuration")]

    public bool useUDP = false;

    public int udpPort = 11000;

    private UdpClient udpClient;

    private IPEndPoint remoteEndPoint;

    private Thread udpThread;

    private string lastUdpMsg = null;

    private readonly object udpLock = new object(); // Synchronizes access to UDP string

 

    [Header("NIDAQ Configuration")]

    public bool useNIDAQ = false;

    [Tooltip("Order: Element 0=X, 1=Y, 2=Z. Supports up to 8 channels.")]

    public List<string> physicalChannels = new List<string> { "Dev1/ai0" };

    private IntPtr taskHandle = IntPtr.Zero;

    private Thread daqThread;

    private double[] lastDaqSignals = new double[8]; // Thread-safe buffer for hardware values

    private readonly object daqLock = new object(); // Synchronizes access to DAQ array

    private int activeChannelCount = 0;

 

    // Master switch to shut down background threads on exit

    private bool isRunning = false;

 

    void Start()

    {

        isRunning = true;

        if (useUDP) InitUDPThreaded();

        if (useNIDAQ) InitNIDAQThreaded();

    }

 

    /// <summary>

    /// Starts a background thread to listen for UDP packets.

    /// Only the latest packet is kept; older ones are overwritten.

    /// </summary>

    private void InitUDPThreaded()

    {

        try

        {

            udpClient = new UdpClient(udpPort);

            remoteEndPoint = new IPEndPoint(IPAddress.Any, 0);

 

            // Fixes a Windows-specific bug where UDP throws an exception if the remote port closes

            const int SIO_UDP_CONNRESET = -1744830452;

            udpClient.Client.IOControl(SIO_UDP_CONNRESET, new byte[] { 0 }, null);

 

            udpThread = new Thread(() => {

                while (isRunning)

                {

                    // Check if data is waiting in the OS buffer

                    while (udpClient.Available > 0)

                    {

                        byte[] data = udpClient.Receive(ref remoteEndPoint);

                        string msg = Encoding.ASCII.GetString(data);

 

                        // Overwrite previous message so Update() only sees the newest data

                        lock (udpLock) { lastUdpMsg = msg; }

                    }

                    //Thread.Sleep(5); // Supposed to be 5ms, but in reality it is close to 15ms because of OS overhead.

                    Stopwatch sw = Stopwatch.StartNew();

                    while (sw.Elapsed.TotalMilliseconds < 5)

                    {

                        // "Spin" until time has passed -> Uses the CPU more but precise.

                    }

                }

            });

            udpThread.IsBackground = true;

            udpThread.Start();

        }

        catch (Exception e) { UnityEngine.Debug.LogError($"UDP Thread Init Failed: {e.Message}"); }

    }

 

    /// <summary>

    /// Initializes NI-DAQmx via P/Invoke and starts a hardware polling thread.

    /// </summary>

    private void InitNIDAQThreaded()

    {

        if (physicalChannels == null || physicalChannels.Count == 0) return;

        activeChannelCount = Mathf.Min(physicalChannels.Count, 8);

 

        try

        {

            string config = string.Join(", ", physicalChannels.ToArray());

 

            // Create and configure the hardware task

            NINative.DAQmxCreateTask("AgentTask", out taskHandle);

            NINative.DAQmxCreateAIVoltageChan(taskHandle, config, "", -1, -10.0, 10.0, 10348, null);

            NINative.DAQmxStartTask(taskHandle);

 

            daqThread = new Thread(() => {

                double[] buffer = new double[activeChannelCount];

                while (isRunning)

                {

                    int read;

                    // Poll 1 sample per channel. Timeout 0.1s to allow thread to exit quickly.

                    int err = NINative.DAQmxReadAnalogF64(taskHandle, 1, 0.1, 0, buffer, (uint)activeChannelCount, out read, IntPtr.Zero);

                    if (err == 0 && read > 0)

                    {

                        lock (daqLock) { Array.Copy(buffer, lastDaqSignals, activeChannelCount); }

                    }

                    //Thread.Sleep(1); // Supposed to be 5ms, but in reality it is close to 15ms because of OS overhead.

                    Stopwatch sw = Stopwatch.StartNew();

                    while (sw.Elapsed.TotalMilliseconds < 5)

                    {

                        // "Spin" until time has passed -> Uses the CPU more but precise.

                    }

                }

            });

            daqThread.IsBackground = true;

            daqThread.Start();

            UnityEngine.Debug.Log($"NIDAQ Thread Started for: {config}");

        }

        catch (Exception e) { UnityEngine.Debug.LogError($"NIDAQ Thread Init Failed: {e.Message}"); }

    }

 

    void Update()

    {

        Vector3 moveInput = Vector3.zero;

        Vector3 rotInput = Vector3.zero;

        Keyboard kb = Keyboard.current;

 

        // 1. Capture Keyboard Input (Additive)

        if (kb != null)

        {

            moveInput += GetKeyboardMovement(kb);

            rotInput += GetKeyboardRotation(kb);

        }

 

        // 2. Capture UDP Input (Most recent only)

        lock (udpLock)

        {

            if (lastUdpMsg != null)

            {

                moveInput += ParseUDP(lastUdpMsg);

                lastUdpMsg = null; // Mark as "consumed" so we don't process it twice

            }

        }

 

        // 3. Capture NIDAQ Input (Thread-safe copy)

        if (useNIDAQ)

        {

            double[] sigs = new double[8];

            lock (daqLock) { Array.Copy(lastDaqSignals, sigs, 8); }

            moveInput += MapDaqToMovement(sigs);

            //rotInput += MapDaqToMovement(sigs); // needs to be implemented

        }

 

        // 4. Move Agent

        transform.Translate(moveInput * moveSpeed * Time.deltaTime, Space.Self);

        transform.Rotate(rotInput * rotationSpeed * Time.deltaTime);

    }

 

    private Vector3 GetKeyboardMovement(Keyboard kb)

    {

        float x = 0, y = 0, z = 0;

        if (kb.wKey.isPressed) z += 1; if (kb.sKey.isPressed) z -= 1;

        if (kb.aKey.isPressed) x -= 1; if (kb.dKey.isPressed) x += 1;

        if (kb.eKey.isPressed) y += 1; if (kb.qKey.isPressed) y -= 1;

        return new Vector3(x, y, z);

    }

 

    private Vector3 GetKeyboardRotation(Keyboard kb)

    {

        float p = 0, y = 0, r = 0;

        if (kb.jKey.isPressed) y -= 1; if (kb.lKey.isPressed) y += 1;

        if (kb.iKey.isPressed) p -= 1; if (kb.kKey.isPressed) p += 1;

        if (kb.uKey.isPressed) r += 1; if (kb.oKey.isPressed) r -= 1;

        return new Vector3(p, y, r);

    }

 

    private Vector3 MapDaqToMovement(double[] signals)

    {

        // Maps list elements to axes: Index 0=X, 1=Y, 2=Z

        float x = (activeChannelCount > 0) ? (float)signals[0] : 0;

        float y = (activeChannelCount > 1) ? (float)signals[1] : 0;

        float z = (activeChannelCount > 2) ? (float)signals[2] : 0;

        return new Vector3(x, y, z);

    }

 

    private Vector3 ParseUDP(string msg)

    {

        // Example: Assume message is forward-only. Replace with your custom parser.

        return Vector3.forward;

    }

 

    void OnApplicationQuit()

    {

        isRunning = false; // Flag background threads to stop loops

 

        // Cleanup NI-DAQmx Task

        if (taskHandle != IntPtr.Zero)

        {

            NINative.DAQmxStopTask(taskHandle);

            NINative.DAQmxClearTask(taskHandle);

        }

 

        // Close UDP Socket

        if (udpClient != null) udpClient.Close();

 

        // Give threads time to finish clean shutdown

        if (daqThread != null) daqThread.Join(200);

        if (udpThread != null) udpThread.Join(200);

    }

}

 

/// <summary>

/// Static Native P/Invoke wrapper for the National Instruments nicaiu.dll driver.

/// </summary>

public static class NINative

{

    private const string dllName = "nicaiu.dll";

 

    [DllImport(dllName)]

    public static extern int DAQmxCreateTask(string taskName, out IntPtr taskHandle);

 

    [DllImport(dllName)]

    public static extern int DAQmxCreateAIVoltageChan(IntPtr taskHandle, string physicalChannel, string nameToAssignToChannel, int terminalConfig, double minVal, double maxVal, int units, string customScaleName);

 

    [DllImport(dllName)]

    public static extern int DAQmxStartTask(IntPtr taskHandle);

 

    [DllImport(dllName)]

    public static extern int DAQmxReadAnalogF64(IntPtr taskHandle, int numSampsPerChan, double timeout, uint fillMode, double[] readArray, uint arraySizeInSamps, out int sampsPerChanRead, IntPtr reserved);

 

    [DllImport(dllName)]

    public static extern int DAQmxStopTask(IntPtr taskHandle);

 

    [DllImport(dllName)]

    public static extern int DAQmxClearTask(IntPtr taskHandle);

}

0 Kudos
Message 8 of 8
(32 Views)