Click here to Skip to main content
13,248,562 members (78,066 online)
Click here to Skip to main content
Add your own
alternative version

Stats

24.5K views
721 downloads
39 bookmarked
Posted 26 Jan 2017

Multithreaded communication for GPIB/Visa/Serial interfaces

, 24 May 2017
Rate this:
Please Sign up or sign in to vote.
synchronous/asynchronous control via multiple interfaces with command queuing

Introduction

This project originated from my efforts to obtain efficient GPIB control in an environment where some devices are slow to respond and usually create a bottleneck in the data flow. The solution proposed in this software consists in using as much as possible the asynchronous operations where a different thread for each device is used so that the write/read operation sequences for different devices can be interleaved. This works quite well as I could easily obtain relatively high reading rates (e.g. an average of 2-3 reading/second/device for 10 devices i.e. a total of 20-30 operations/second) even in configurations where a few devices are very slow (e.g. 2-3 seconds response time). Asynchronous operations are queued which makes programming very simple. However standard synchronous (blocking) operations are allowed too, and the code can safely and transparently handle concurrent use of both synchronous and asynchronous commands.

As the software has been made sufficiently general to adapt it to various Gpib libraries it could eventually also be adapted to other interfaces such as the NI's Visa library and the serial port. Visa is especially interesting as it enables controlling devices via USB (for devices compliant with USB-TMC protocol) or TCP/IP (LXI standard) without need to develop specific drivers. Of course it would be great if someone could develop an implementation for these protocols not relying on Visa.

The software needs third-party drivers (except for the serial port) to be installed to operate.

Following the object-oriented philosophy, all interfaces are represented as classes derived from an abstract class representing a generic device, and the low-level interface-dependent operations are implemented as virtual methods so that the interfaces can be used "polymorphically" : except when instances of various devices are created, the code does not need to know which interface is being used for each of them. This approach also makes easy to add new interfaces or to tweak the provided implementations by creating child classes overriding methods that need to be modified.

All files and projects are written for WindowsForms applications and come in two versions: C# and VB.NET

All projects were created under VS2008 (also have been tested under SharpDevelop v4.3)

Projects and files included

The projects IODevices and IODevices_withNINET construct a library (NET assembly "IODevices.dll") which defines a generic (abstract) class "IODevice" and its implementations (inherited classes) for various interfaces (see section "IODevices assembly and implementations" for more details on each of these classes and the required drivers). The interfaces are:

  • GPIBDevice_NINET - for NI boards
  • GPIBDevice_ADLink - for ADLink boards 
  • GPIBDevice_gpib488 - for Keithley,MCC and older NI boards
  • VisaDevice - generic interface (GPIB,USB etc.) via Ni Visa
  • SerialDevice - for serial ports

The library also defines two forms to display status and error messages: DevicesForm, IOmsg

You have to add a reference to this assembly in your project to use it, see test projects. (Alternatively, you may just copy the necessary files directly to your project - but then the application will get access to all "internal" fields and methods - not very safe in principle!).

The class GPIBDevice_NINET refers to the National Instruments' .NET assemblies to access the Gpib driver (see details in the class description, these are not included here because of copyright) and any project including this file will not compile without them, therefore I made two projects:

  • project IODevices: without GPIBDevice_NINET
  • project IODevices_withNINET: including GPIBDevice_NINET

(The name of the resulting NET assembly is "IODevices.dll" for both, so it should be recompiled when switching from one version to the other)

Other implementations use Windows "dll"s only ("plain C" dll files provided by various board vendors) so that the project IODevices will always compile whether or not the interfaces are installed on the system, if a necessary dll is not present an error will only occur if a corresponding class is instanciated. Note that Gpib boards from NI can also be accessed via Visa interface which usually is also installed with them, so the GPIBDevice_NINET interface class may not be needed.

The projects testIODevice and testIODevice-withNINET are demos showing how to use the basic functions of the library. You may choose two devices (eg. one fast and one slow) and see how it works sending commands to both at the same time.

The test project contains a reference to the assembly "IODevices.dll" therefore both projects are interdependent (it is practical to put them in the same solution if you want to examine how the code in the IODevices assembly works).

All these projects are also contained in two "solutions":

  • testIODevice,
  • testIODevice-withNINET

each containing both a library and a test project.

Background: Gpib communication issues

NI references:

  • http://www.ni.com/white-paper/3275/en/
  • http://www.ni.com/tutorial/4054/en/
  • http://www.ni.com/white-paper/2927/en/
  • http://www.ni.com/tutorial/4478/en/
  • http://www.ni.com/white-paper/4629/en/

Let us examine a simple approach where Gpib devices are addressed sequentially:

  • send command to device 1
  • wait for response from device 1
  • send command to device 2
  • wait for response from device 2
  • send command to device 3
  • wait for response from device 3

This scheme has several potential shortcomings:

  1. if waiting for a response from device is achieved via low-level gpib functions then:
    • the application freezes during wait if low-level gpib functions (“receive”) are called on the main (GUI) thread. This problem can be solved using a different thread for gpib operations (asynchronous operations)
    • the GPIB bus is locked during low-level interface calls therefore even using threading the sequence is not efficient because other devices have to wait too, unless we shorten as much as possible the “waiting for response” time inside “receive” functions, which are waiting for response with the gpib bus locked. This can be achieved either using polling or setting very short timeout values and repeating reading on timeout, so that the bus is not locked most of the time.
  2. The sequential querying will take time which causes problem if we need to periodically scan many devices. The time needed to get the response is not only function of the size of data to transfer, usually the slowest response is expected for commands that trigger a measurement, such as the commonly used "Read" command of DMM's. For DMM's, the delay depends on the resolution of the measurement and getting a response can easily take a couple of seconds which is often the main source of bottleneck on the gpib. Of course there are software solutions using device specific configurations (trigger commands or auto-trigger mode if available etc.) however such an approach requires more programming effort and is more difficult to be made generic (typically an application has to handle a number of similar devices selected from a device pool each time a new experiment is configured). Therefore it is better if a generic yet efficient approach may be found.

Both the delays and the time intervals when gpib bus is unavailable can be minimized using (a) interleaving of command/response sequences and (b) polling. Command interleaving can be obtained automatically if each device has a dedicated thread for asynchronous operations, as explained below.

GPIB is a bus where the controller (PC) decides when each device is allowed to send data therefore the GPIB "write" and "read" operations can be interleaved, leading effectively to a parallel query of several devices at a time:

  • send command to device 1
  • send command to device 2
  • send command to device 3
  • wait for response from device 1
  • wait for response from device 2
  • wait for response from device 3

Then at the time we receive a response from device 1, the devices 2 and 3 might be ready too to send data therefore there is no additional performance penalty due to devices 2 and 3.

The interleaving of the write/read sequences can be achieved automatically in a way totally transparent to the calling program if each device uses a different thread to perform gpib operations. The scheme implementing this will be:

  • thread1: send command to device 1; wait for response from device 1
  • thread2: send command to device 2; wait for response from device 2
  • thread3: send command to device 3; wait for response from device 3

Here each thread will proceed as soon as the gpib bus is available. There is the remaining problem of blocking the bus while waiting for a device to respond. This is most efficiently solved using the "poll" feature of GPIB: test if a device is ready to send data before actually allow it to talk. So finally the most efficient scheme is:

  • thread1: send command to device 1; periodically poll it for "data ready" status; get response from device 1
  • thread2: send command to device 2; periodically poll it for "data ready" status; get response from device 2

If a device does not support polling then it may be good to replace it by a constant delay (during which the respective thread is sent to sleep) between the write and read operations so as to shorten the "wait for response" time during which the bus is unavailable to other devices. Also, it is good to set a short timeout at the interface level and repeat reading after a delay if timeout occurs.

The purpose of this library implementing this scheme is to provide an out-of-the-box solution for creating such efficient code with minimum programming effort, all thread manipulating is transparent to the user code. Actually my own purpose was to easily adapt some existing applications that were using classical sequential approach with as few changes in the code as possible.

The IODevice class is intended to be adapted to any low level GPIB interface and therefore all low-level operations are defined as abstract (VB: MustInherit) methods. The project provides implementations (derived classes) of this abstract class for several GPIB interfaces (see file list above and reference below). Also, the abstract methods are sufficiently general to allow constructing implementations for other interfaces such as NI Visa and serial port (hence the initial name “GpibDevice” eventually became “IODevice”). It should be quite easy to create implementations for other hardware.

Note that the standard GPIB libraries also provide a sort of asynchronous operations, however somewhat limited, for example it is said in the NI reference manual :

Quote:

"The asynchronous I/O calls (BeginRead and BeginWrite) are designed so that applications can perform other non-GPIB operations while the I/O is in progress. Once the asynchronous I/O has begun, further NI-488.2 calls are strictly limited. Any calls that interfere with the I/O in progress are not allowed and return an exception."

This means that no queuing is performed (and probably also that asynchronous operations on various devices are all executed on the same thread), anyway such limitation is not compatible with the parallel querying scheme described above (it also adds a lot of programming overhead compared to simple synchronous calls) and makes these features not very helpful for our purpose. Note that, on the other hand, the GPIB "Notify" callback feature does not suffer from such limitations and can be implemented resulting in a more powerful and more flexible asynchronous reading scheme, as explained later.

The code proposed here provides a somewhat higher abstraction level since asynchronous tasks are queued (see below) so that the calling program does not have to care about the moment a command is allowed to be sent: here all asynchronous calls are allowed at all times (but we can decide if a call is necessary inspecting the queue content with "PendingTasks" methods).

Note that queuing messages resulting from asynchronous write/read operations is possible in NI Visa (cf. viWriteAsync, viReadAsync functions). However, here the philosophy is different because the whole queries (write/read sequences, including commands) are queued for each device, so that the program does not have to wait until an asynchronous read operation completes before sending other commands to the same device*. This makes the asynchronous programming extremely simple and allows an automatic “retry on error” feature. If the “retry” flag is set then, in case of error, the function will clear the device and repeat the whole write/read sequence until success or abort by user.

*Actually I don’t have enough experience with Visa to tell if it can handle a query queue (but in all examples the program waits for write event before proceeding with read) so correct me if I am wrong. On the other some lower-level protocols like HiSLIP  implement query queuing.

Polling

quoted from: http://www.ni.com/tutorial/4054/en/ :

"Serial polling is a method of obtaining specific information from GPIB devices when they request service. When you conduct a serial poll, the Controller queries each device looking for the one that asserted SRQ. The device responds to the poll by returning the value of its Status Byte. Device-dependent conditions, such as the presence of available data or an error condition, determine this value. ANSI/IEEE Standard 488.1-1987 specifies only one bit in the Status Byte, Bit 6, which is TRUE if the device requests service. The other bits in the Status Byte are left to the instrument manufacturer to define. IEEE 488.1-compatible instruments have bits that determine if an instrument error has occurred or if the device is conducting a self-test. These bit definitions are not consistent among instrument vendors and the method for determining the cause of a service request varies with each device.

ANSI/IEEE Standard 488.2-1987 solves this problem by defining certain service request conditions so that one model describes the Status Byte for all compliant devices. Bit 6, the device Request Service (RQS) bit, maintains the IEEE 488.1 definition. If Bit 6 is set, then the device requested service. The IEEE 488.2 standard defines Bits 4 and 5; instrument manufacturers define the remaining bits (0 through 3 and 7). Bit 4 is the Message Available (MAV) bit. This bit is set if the device has been previously queried for data and the device has a pending data message to send.

The polling option is enabled setting "enablepoll" field to true (default). It should be enabled if the device is compatible with the 488.2 standard. Then the serial poll is used to see if a device is ready to send data by examining its Status Byte, in this way the gpib bus is not locked most of the time when waiting for a device to respond. This is especially important when the query command also acts as a software trigger of a new measurement (standard behavior for DMMs).

This library only uses the MAV (Message Available) bit of the status byte defined in the standard as explained above.

Most devices comply to 488.2, but not all eg. some Lakeshore temperature controllers define their own meaning for all the status byte bits. If you see a "poll timeout" error appearing then it is probably the case and polling should be disabled. Alternatively, it is possible to write a derived class overriding the virtual method “pollMAV” : this method also returns the whole status byte so that it is easy to write a modified implementation where the status byte is interpreted differently (see example in the section about implementations).  If polling is not available then, as said above, we should set a short timeout at the interface level (the reading will anyway be repeated automatically on timeout) so to not to block the bus for long periods of time.

Asynchronous interface callbacks

In a scheme based solely on polling the effective response time of a device has a minimum granularity defined by the polling frequency.  For time-critical applications we can increase this frequency but this will also increase inefficient traffic on the bus (and may even cause errors if delays between subsequent polls are very short).  On the other hand, a low-level driver can have access to hardware interrupts and thus knows immediately about all signals appearing on the bus.  Various interfaces provide means of asynchronous signalling of events which can make polling more efficient.   In GPIB,  we can configure selected devices to pull the Service Request (SRQ) GPIB line when they are ready to send data and configure the driver to fire an asynchronous callback each time SRQ is detected (http://www.ni.com/white-paper/4629/en/).  The protocols USBTMC, VXI11 and HiSLIP also implement asynchronous service requests, using out-of-band signalling.  Likewise, the serial port can be configured to raise an event each time new data arrives.

The IODevice class provides a very simple (optional) feature that can be used with asynchronous callbacks from the driver and which seamlessly integrates with the polling scheme described above:  the waiting for read after write or for next polling/reading trial can be asynchronously interrupted by another thread calling the device's method  "WakeUp". Two delays are concerned: delay between write and read (delayread) and the delay between subsequent read/poll trials (delayrereadontimeout).  This method can be called from any thread and is intended to be used in a callback function called by a low-level driver (i.e. unsynchronized callbacks can be used).

 In the current project version this technique is implemented in three classes: the class GPIBDevice_NINET and VisaDevice offer an optional possibility to set up the GPIB "Notify" callback or its equivalent in other protocols supported by Visa,  and  the class SerialDevice uses it by default, implementing a handler of the DataReceived event of the SerialPort class (see class descriptions below for details).

Using the code : IODevice class reference

I/O functions

There are two types of I/O functions:

  1. "Send" functions are intended for commands where no response is expected from device
  2. "Query" functions write a command and read response

Both use the same code and architecture built around the “IOQuery” class, in the code the term “query” is used for both (the “type” field in the IOquery class distinguishing between the two versions).

NB. There is no separate “read” operation as this would be incompatible with the “retry” feature (note that VISA defines three sorts of functions: write, query and read).

The "read" operation alone would only be required in the case of "talk only" devices, I don't know any but if such operation is needed, if a query method is called with command set to empty string then it will not call the “send” operation, it is therefore equivalent to a “read” alone.

The I/O functions don't throw any exceptions. External exceptions in the library functions or in the user callback functions can be catched or not (see "catchinterfaceexceptions" and "catchcallbackexceptions" flags). Note however that the class constructors can throw exceptions (this is to avoid creating ill-defined objects, catching constructor exceptions should be done outside the constructor), see descriptions of different classes below.

Each type of command is provided in two versions (see reference below for the syntax of each command):

  1. blocking commands: SendBlocking, QueryBlocking are immediately executed on the calling thread (usually GUI thread), the method waits until it gets response from the interface.

    Here “blocking” means that the call will not return until the response is received, however the bus is not blocked during the whole query therefore other queries can be conducted in parallel, exactly as for “async” commands. Of course for each device only one blocking command is allowed at a time.

  2. asynchronous commands: SendAsync, QueryAsync : here queries are queued and the queue is processed on a different thread (producer-consumer model). The call appends the query to the queue and returns immediately. When the query is completed the user "callback" function is called.

Each device has its own queue and runs its own dedicated thread processing queries from the queue. In this manner the asynchronous commands to a device are processed sequentially but commands to different devices can be processed in parallel, as explained above.

The idea of using both types of queries is that often the mainstream sequence (eg. needing complex sequence for device configuration, arming, acquiring etc., where the commands to sent may sometimes depend on the data received so it is simpler and natural to use synchronous calls) runs in parallel with some annex tasks repeated at constant intervals to update the status of the experiment (e.g. reading temperature every second). For these tasks we usually use timers, and such tasks are much more efficient with asynchronous queries, using available bus bandwidth and time slots.

One can mix blocking and asynchronous commands even on the same device, then a blocking command should in principle be processed as soon as the current asynchronous operation (if any) completes or when it is waiting for retry (however there is no guarantee as for the exact timing which is decided by the OS task scheduler).

The method "WaitAsync" can be used for synchronization between the asynchronous command queue and the main thread (see class description below): this method waits until queries initiated before the call are completed (NB. not until the queue is empty - this may never happen if asynchronous queries are issued in timers).

Common arguments:

  • string cmd: command string
  • bool retry: if set to true the whole query will be repeated on error (until success or abort by user)
  • bool cbwait: if set to true the async thread will wait until the callback function completes before processing remaining queries in the queue (this is the default behavior for short versions where this argument is absent). May be set to false eg. if a callback function starts a long processing and there is no need to wait, however in this case the callback function has either to block event processing or to be reentrant (can be called again before it returns). Note also that with this setting the class' "catching callback exceptions" feature ("catchcallbackexceptions" flag set to true) will not work, if you want to understand why I recommend this excellent series of articles: https://www.codeproject.com/Articles/31971/Understanding-SynchronizationContext-Part-I
  • int tag: an additional field passed on to the query variable, may be used to distinguish between queries if the same callback function is used to process different queries.

Blocking commands

1) SendBlocking

C#

public int SendBlocking(string cmd, bool retry)

VB

Public Function SendBlocking(ByVal cmd As String, ByVal retry As Boolean) As Integer

2) QueryBlocking

C#

public int QueryBlocking(string cmd, out IOQuery q, bool retry)  

public int QueryBlocking(string cmd, out string resp, bool retry)

public int QueryBlocking(string cmd, out byte[] resparr, bool retry)

VB

Public Function QueryBlocking(ByVal cmd As String, ByRef q As IOQuery, ByVal retry As Boolean) As Integer

Public Function QueryBlocking(ByVal cmd As String, ByRef resp As String, ByVal retry As Boolean) As Integer

Public Function QueryBlocking(ByVal cmd As String, ByRef resparr As Byte(), ByVal retry As Boolean) As Integer

In the first syntax the variable q contains the full information on the query (status,data, timings), the other two are simpler versions giving directly the result either as a string (resp) or a byte array (resparr). In case of error (return value different from 0) q will contain the error code and message, however the data fields (ResponseAsString, ResponseAsByteArray) will be null references (VB: Nothing), as well as the corresponding variables resp and resparr in the two other versions.

Return value: same as q.status (0 if ok or error code), otherwise -1 if blocking call on this device is already in progress (may happen if events allowed), -2 if device is disposing.

Asynchronous commands: SendAsync and QueryAsync

Most of these methods use an argument of type IOCallback to define the callback function. The signature of this function is given by the declaration:

VB :   Public Delegate Sub IOCallback(ByVal q As IOQuery)

C#:    public delegate void IOCallback(IOQuery q);

Here the variable q will contain the status and data relative to the operation, the rules are the same as for the blocking calls i.e. the data fields are null references if an error occurred.

Note that, even though the callback is initiated from a different thread, the callback function will be executed on the main (GUI) thread (this is to allow updating GUI components within the callback function), in other words the asynchronous thread sends a message to the main thread to call the function. Therefore the callback function will not be called until processing messages by the GUI thread is allowed.

The return value for all versions of SendAsync and QueryAsync methods is: 0 if ok, -1 if the queue is full (for each device the maximum queue length is defined by the field maxtasks, default is 50), -2 if device is disposing.

SendAsync

C#

public int SendAsync(string cmd, bool retry)

// complete version (with callback)

  public int SendAsync(string cmd, IOCallback callback, bool retry, bool cbwait, int tag)

VB

Public Function SendAsync(ByVal cmd As String, ByVal retry As Boolean)

  ' complete version (with callback)

Public Function SendAsync(ByVal cmd As String, ByVal callback As IOCallback, ByVal retry As Boolean, ByVal cbwait As Boolean, ByVal tag As Integer) As Integer

In the complete version (probably rarely needed) the callback function will be called to signal the status of the operation (however there will be no valid data in the IOquery variable passed to it). This version enables however the calling program to break the “retry” loop calling the “AbortRetry” method of the IOQuery variable.

QueryAsync with callback

When the function completes (or when there is an error) it will call the callback function passing the result in a IOQuery variable. The callback function has to check the status field of the received IOQuery variable to check if there is a valid data there (if status is not 0 then ResponseAsString and ResponseAsByteArray will be null references).

C#

//standard:

        public int QueryAsync(string cmd, IOCallback callback, bool retry)

//complete:

      public int QueryAsync(string cmd, IOCallback callback, bool retry, bool cbwait, int tag)

VB

'standard:

Public Function QueryAsync(ByVal cmd As String, ByVal callback As IOCallback, ByVal retry As Boolean) As Integer

'complete:

Public Function QueryAsync(ByVal cmd As String, ByVal callback As IOCallback, ByVal retry As Boolean, ByVal cbwait As Boolean, ByVal tag As Integer) As Integer
QueryAsync with TextBox

This is a "light" version where, instead of calling a callback function, the result is put (if no error occurs) in a TextBox variable (its Text property). This version does not return any error information (except in the message window, if enabled).

 

C#

//2nd version : update textbox with data string

   public int QueryAsync(string cmd, TextBox text, bool retry)

  public int QueryAsync(string cmd, TextBox text, bool retry, int tag)

VB:

   ' 2nd version : update textbox with data string

Public Function QueryAsync(ByVal cmd As String, ByVal text As TextBox, ByVal retry As Boolean) As Integer


Public Function QueryAsync(ByVal cmd As String, ByVal text As TextBox, ByVal retry As Boolean, ByVal tag As Integer) As Integer

Other methods of IODevice class

Instance public methods:

C#

public bool IsBlocking();          // true when blocking call in progress

public int PendingTasks();         //  return number of queries in the queue

public int PendingTasks(string  cmd); //  same for a specific command: number of copies of specific command in the queue

public void WaitAsync();  //this method can be used to synchronize blocking and async calls

public void AbortAllTasks();    //as in the title: aborts all queries (blocking and async)

public void Dispose();

VB: (see C# code comments above)

Public Function IsBlocking() As Boolean

Public Function PendingTasks() As Integer

Public Function PendingTasks(ByVal cmd As String) As Integer 

Public Sub WaitAsync()           

public void AbortAllTasks()   

Public Sub AbortAllTasks()

Public Sub Dispose()

Static (VB shared) methods

C#

public static void ShowDevices() //shows DevicesForm displaying current status of all devices, see below.


public static IODevice DeviceByName(string name) // find device among created devices using name


public static void DisposeAll() // dispose all created devices

VB:

Public Shared Sub ShowDevices()

Public Shared Function DeviceByName(ByVal name As String) As IODevice

Public Shared Sub DisposeAll()

Public fields :

public int maxtasks;  //max queue length (default=50)

public string devname, devaddr;          //device name and address
public static string statusmsg; //optional message (status etc.) to display in device list window

 // some delays to tweak performance (all in ms):
public int delayread; //default delay between cmd and read : to avoid blocking gpib bus by slow devices when polling is not available

public int delayrereadontimeout;      //default delay before retrying read after timeout or delay between polls if polling used

public int delayop; //delay to wait between operations, for old devices that may not accept frequent requests

public int readtimeout;          //cumulative timeout for read

public int delayretry; //delay before retry on timeout

public bool checkEOI;         //use EOI information: if true repeat read if EOI not detected (eg. buffer too small); default=true,

public bool enablepoll; //use serial poll, set to false for devices not  supporting polling ("poll timeout" message)


public bool stripcrlf;         //remove crlf in ByteArrayToString method

public bool eventsallowed; //for blocking commands : when waiting for response and during and retry loop

public bool showmessages;         // showing error window IOmsg enabled

public volatile IOQuery lastasyncquery;

public bool catchinterfaceexceptions;         //default=true , set to false when debugging a new interface
public bool catchcallbackexceptions;          //default=true

public bool callbackonretry;         //default=true, if callback called on each retry when error

Protected methods and properties to use in implementations:

C# :

protected void AddToList(); //register device in "devicelist" shown in DeviceForm, this should only be called by child class constructors (is not called in the base class constructor to avoid registering ill-defined objects when constructor exception occurs)

protected void WakeUp();  //interrupt waiting for next read or poll trial, to be used in interface callbacks

protected IOQuery currentactivequery   //to use in implementation methods e.g. if the command string is needed to determine EOI mode (currently not used)

VB:

Protected Sub WakeUp()

Protected Sub AddToList()

Protected ReadOnly Property currentactivequery() As IOQuery

Devices list window

opens when calling the static method IODevices.ShowDevices(), looks like this example:

By default ShowDevices(), will be called at startup (in the static constructor of IODevices), if you find it annoying you can set the constant showdevicesonstartup to false. 

Error message window

When "showmessages" field is set to true (default) this forms opens when an error occurs. The form is not modal and is for information only: the program continues the same way whether the form is displayed or not. However it allows you to easily abort a "retry". When an error is corrected after a successful retry then the form will close itself. In the example above it signals that a device is not connected and that the query will be repeated, once it is connected the message will disappear. If you close the window and the error persists then the message will pupup again, minimize the window instead of closing it if you find it annoying. 

IOQuery class public members

C#

public string cmd;  // copy of the command to identify query

public int tag;  //optional additional query identifier

public string ResponseAsString; // non null only if status=0

public byte[] ResponseAsByteArray; // non null only if status=0

public int status;

 //0:ok, otherwise combination:

 //bit 1:timeout, bit 2: on send(0) or recv(1), bit 3 : other error (see errcode), , bit 4: aborted by user, bit 5: poll error, bit 8: error in callback (exceptions catched when "catchcallbackexceptions" flag is set)

 // so if not aborted: status=1: tmo on send; =3 tmo on rcv, =4 other err on send, =6 other err on rcv; if aborted add 8 , if  poll timeout add 16,


 public int errcode; //interface error code (valid if status>0)

 public string errmsg; //interface error message(valid if status>0)

 //additional fields for testing performance:

 public DateTime timecall; // when method called

 public DateTime timestart; //when device unlocked and operation started

 public DateTime timeend; // when data received (or aborted)

 public int type;          //type of query: 1 ("send", no resp.), 2: true "query" (wait for response)

 public IODevice device ; // device used (to access other fields)

 public void AbortRetry();//abort this task(async or blocking)

 public void AbortAll();// calls device.AbortAllTasks

VB (see C# code comments above)

Public cmd As String

Public tag As Integer    

Public ReadOnly Property ResponseAsString() As String

Public ReadOnly Property ResponseAsByteArray() As Byte()

Public status As Integer

Public errcode As Integer 

Public errmsg As String    

Public timecall As DateTime

Public timestart As DateTime 

Public timeend As DateTime

Public type As Integer   

Public ReadOnly Property device() As IODevice

Public Sub AbortRetry()

IODevices assembly and implementations

The assembly defines two namespaces: IODevices and IODeviceForms. The latter is used internally to access forms displaying the device list and error messages and does not need to be imported in the application.

The IODevices namespace which should be imported to the application defines the following classes:

class IOQuery

as explained above

class IODevice

Is an abstract (VB: MustInherit) class from which various kinds of real devices are derived as child classes. Contains the main code and public methods but refers to four abstract methods to address the low level interface (see explanation in the code under “abstract methods” comment if you want to implement other interfaces).

Following the object-oriented philosophy every device derives from IODevice which contains all public methods, whereas the low level interface is accessed via private virtual methods, in this way the child classes can be used polymorphically: except when the instance of a device is created the code does not need to know which interface is used (see sample code in the “testIODevice” project).

The implementations of this abstract class given here provide a basic configuration (but succesfully tested with various devices). Again following the object-oriented philosophy, it is easier to write a derived class for each specific configuration than to make a class which would take into account all available options. For example, all GPIB classes use the standard "EOI" signal to detect the end of message. If your device cannot set EOI but instead uses a specific caracter to terminate messages (e.g."\n") you can write a child class which redefines the constructor to set the device options accordingly (alternatively, it may also override the "ReceiveByteArray" method, see section about writing new implementations).

All implementations configure the interface with a relatively short timeout (300ms for GPIB, few ms for serial) because anyway the reading will be repeated until the "cumulative" timeout period "readtimeout" elapses, in this way the bus will not be blocked for a long time even if polling is disabled.  

The constructor of each of these classes will throw an exception if the device initialization fails. This is to prevent creating ill-defined objects, catching constructor exceptions should be done outside the constructor.

For all GPIB classes, if "buffersize" is not specified it will be set to 32k.

The current version provides the following implementations :

class GPIBDevice_NINET

uses the native .NET library for NI Gpib , uses as reference the following .NET assemblies from NI: NationalInstrumentsCommon, NationalInstruments.NI4882 (these are installed with the NI GPIB driver). It was intensively tested with the GPIB-USB-HS+ board from NI. The assembly will not compile if these files are not found (and were not included here because of copyright), therefore a version of the project where this class is not included is also provided. Note that in many cases we won't necessary need this library: usually the NI software for GPIB will also install Visa.

class constructors:

C#

​​​​​public GPIBDevice_NINET(string name, string addr)

public GPIBDevice_NINET(string name, string addr, int buffersize)

 

VB: 

Sub New(ByVal name As String, ByVal addr As String)

Sub New(ByVal name As String, ByVal addr As String, ByVal buffersize As Integer)

name is the name given to the device and displayed in the DevicesForm.

addr is the GPIB address and can have the following forms:

"n"        device n at board n°0   e.g.  "1"

"b:n"     device n at board n° b   e.g.  "0:1"

"GPIBb::n::INSTR"   (Visa format)  device n at board n° b   e.g.  "GPIB0::1::INSTR"

 

The version of April 2017 adds a boolean property EnableNotify (default=false).   Setting this property to true will enable the board's "Notify" callback to be activated on SRQ and subscribe to the notify event for the device (the class keeps a list of all devices for which EnableNotify was set).  Each callback on SRQ will then call the device's WakeUp() method as explained above.  

The NI library provides two versions of the callback: at board level and at device level.  For the latter, as there is only a single SRQ line, the NI driver will automatically poll all selected devices to find the source of SRQ.

This library uses only the board-level callback, since calling Wakeup will proceed to poll anyway (if polling is enabled, however note that you can use this feature even with polling disabled: then calling Wakeup will result in trying to read data immediately).   As only one callback function can be defined for a board, each SRQ request will call WakeUp methods of all devices having subscribed therefore if used for many devices on the same board it may become less efficient and even cause driver errors.  Typically, it should be used for selected devices for which it is essential to get data as soon as it is ready.  

In the previous release I was complaining that the Notify feature was sometimes not working properly, depending on the hardware configuration.  I found since that the problems were related to the NI driver's "automatic polling" feature which is activated by default (and apparently tries to poll all devices whether or not they subscribed to device-level callbacks). In this version the autopolling has been disabled (in the class constructor).  

To enable setting SRQ when the MAV bit is set you need to enable the appropriate bit in the Service Request Enable Register of your instrument. Usually it is the bit 4 and it will be set sending the command "*SRE 16" to the device (note that you can also add other flags to wakeup on, eg. OPC,  error etc.). There is an example of function doing all these configuration steps in the test Form:

public void setnotify(GPIBDevice_NINET dev)
  {
          dev.delayread = 1000;
          dev.delayrereadontimeout = 1000; //set long wait delays (will be interrupted anyway)
          dev.SendBlocking("*SRE 16", false); //set bit 4 in the Service Request Enable Register, so that the MAV status will set SRQ
          dev.EnableNotify = true;  //enable calling WakeUp on SRQ
  }

 

class GPIBDevice_gpib488

uses the Windows dll library "gpib488.dll" provided with Measurement&Computing or Keithley boards and also some older NI boards. This dll is usually provided in both 32bit and 64bit versions (with the same name but in different Windows directories). The class has been tested with the KUSB-488A board from Keithley. I don't have a MCC board but the signatures of all the gpib488.dll functions are exactly the same for both (let me know if there are problems). For older NI boards there is an equivalent library named "NI4882.dll" that I have tested too, it works in principle but seems a bit flaky with some devices, using Visa is apparently more reliable. The name of the dll is defined by the string constant "_GPIBDll" so it is easy to change it in the code. However, for NI I noticed that many provided GPIB examples for C/C++ programming use rather Visa interface instead of these older dlls so this is probably the way to go if you don't want to use the NINET interface.

The constructor parameters are the same as for GPIBDevice_NINET.

class GPIBDevice_ADLink

uses the Windows dll  "gpib-32.dll" provided with ADLink boards. Was intensively tested with the USB-GPIB3488A board from ADLink.

NB. this dll has the same name as the one installed by NI software or older MCC software. It may be wiser to rename this dll before installing it in the Windows directory (Windows/SysWow64 on 64 bit systems) and then change the string constant “_GPIBDll” in the code.

The constructor parameters are the same as for GPIBDevice_NINET.

Note: In the previous version the class was using the driver calls provided in the ADLink library import module for C#. These are not all compatible with the "standard" calls found in other "gpib-32" dlls, however all standard routines exist there as can be found using a dll browser.  In this version I made some modifications (transparent to the class users though) so that only standard calls are used.  Therefore this class should also be compatible with NI drivers  (so the class could rather be called "GPIBDevice_gpib-32" but I did not want to change the name), note however that error messages are less complete here than those returned by the NI NET library.   Also, the code of the class is now almost identical to the one of GPIBDevice_gpib488  except for some tiny differences in the driver functions signatures.  

 

class VisaDevice

Uses Windows dll “Visa32.dll”. It is provided in both 32bit and 64bit versions (with the same name, to force using the 64 bit version “Visa64.dll” you may change the constant “_VisaDll” in the code).

NB. In 32bit Windows dlls are in Windows/system32 but in 64bit Windows, 32bit dlls are in Windows/SysWow64 and 64bit dlls are in Windows/system32.

National Instruments' Visa library (that may be downloaded from NI website) provides a generic interface that can be used to access various physical interfaces (Gpib, serial, USB, TCP/IP etc.). Visa implements various protocols developed for instrumentation like  USBTMC (over USB),  VXI-11 and HiSLIP (over TCP/IP).  These protocols mimic the behavior of GPIB in many aspects as they were intended to replace it. We find there the equivalents of polling the status register, out-of-band signalling for service request messages etc.   The VXI-11 and HiSLIP protocols are part of the LXI standard (Lan eXtension for Instrumentation, which defines protocols for controlling instruments via TCP/IP). 

In principle the basic read/write functions are the same for all interfaces handled by Visa however each of them may need a specific configuration (options – called “attributes” in Visa, error handling etc.). The VisaDevice class provides a basic Visa configuration, you may need to create derived classes to tweak it for a specific configuration. It was successfully tested with GPIB (using GPIB-USB-HS+ board from NI),  USB and TCP/IP.  For GPIB, a small advantage of VisaDevice over the GPIBDevice_NINET class is that it does not require any external assemblies from NI (the needed version of these may depend on the compiler, .NET version etc.).

The "Notify" feature is now available for Visa.  The class' property EnableNotify works exactly like in the GPIBDevice_NINET class.

VisaDevice can be used for GPIB with NI drivers (then you don't need to install the NI Net assemblies), then there are small differences compared to GPIBDevice_NINET :

  •  NINET allows to create a device even if it is not connected to the system while in Visa this will cause an error (actually the error occurs when trying to clear the device on startup, this may be disabled in the code setting the constant clearonstartup in VisaDevice to false)
  • in VisaDevice only a few common errors give full-text messages, for other errors only the hexadecimal error code is reported (I was just too lazy to code all possible messages)
  • Visa only provides device-level callbacks, for NI GPIB these rely on "autopolling", therefore for some hardware configuration EnableNotify may encounter problems that I previously had with  GPIBDevice_NINET (see the GPIBDevice_NINET class description above).

For USB, the device has to be compatible with the "Test and Measurement Class" (USB-TMC) protocol. When after connecting it the device is detected as a "Test and Measurement Device" then it can be used immediately. If the driver is not present then see http://www.ni.com/tutorial/4478/en/ for instructions to create one.

For VXI-11 and HiSLIP it may be convenient to use NI MAX utility to setup the LAN parameters.

The constructor parameters are the same as for GPIBDevice_NINET. However, here the format for the address parameter must be compatible with Visa specifications:

for Gpib:   GPIB[board]::address::INSTR

for USB:    USB[board]::manufacturer ID::model code::serial number::INSTR

for TCPIP:  TCPIP[board]::IP address[::LAN device name]::INSTR

 

This class also adds a method to set Visa attributes:

C# : 

public uint SetAttribute(int attribute, int attrState)

VB :

​​​​​​​Public Function SetAttribute(ByVal attribute As Integer, ByVal attrState As Integer) As UInteger
class SerialDevice

Although Visa can be used to access serial ports it was simple (also more useful and efficient since it does not need Visa resources) to write an implementation using the standard SerialPort class provided in NET.

 

constructors:

C#:

public SerialDevice(string name, string addr)

public SerialDevice(string name, string addr, string termstr, int buffersize)

VB

Sub New(ByVal name As String, ByVal addr As String)

Sub New(ByVal name As String, ByVal addr As String, ByVal termstr As String, ByVal buffersize As Integer)

parameters:

termstr is the "NewLine" string

address format:

COMport,baud,parity,databits,stopbits [,newline]

newline can be CR ("\r"),LF ("\n") or CRLF ("\r\n") (for other values use the second version of the constructor).

for example:

"COM1:9600,N,8,1,CRLF"

if "termstr" is defined in the constructor then it will take over the value set in "addr".

 

There is no polling feature for serial port, therefore for this interface 1) the polling function sets the MAV status to true as soon as the input buffer is not empty; 2) then the serial port timeout is set to very short value (few ms) so that blocking commands do not freeze GUI when waiting for a line to be completed (here reading will almost always be repeated more than once).

Like for the classes GPIBDevice_NINET and VisaDevice, the current version uses an asynchronous signalling, here the property controlling it is EnableDataReceivedEvent and is set to true by default in the class constructor.   When enabled it defines a handler for the SerialPort class' DataReceivedEvent, the handler calls WakeUp() to immediately interrupt any waiting delay.  Unlike for GPIB, each serial port has its own handler so there is no problem of making lots of unnecessary polls.  With this feature the SerialDevice class  will provide response as soon as it is completed, in both blocking and asynchronous queries.   In principle there is no reason to disable this default behavior - maybe except when you are expecting an extremely long response and want to avoid events to be fired every few incoming characters or so.  

Note that unlike protocols like GPIB, USBTMC or LXI which are defined to make things uniform, the serial port does not have any standard protocol for messages therefore it is not possible to make a plug-and-play class (like Visa) working for all serial connections.  The SerialDevice class implements a simple line-oriented protocol: each message terminates with the same special end-of-line character and there is one response message for each query. However if your device uses something different  -  such as either fixed length messages with no termination or some weird syntax where both terminated and fixed-length messages are mixed or where responses can use different termination characters depending on command - then it will be necessary to make a derived class overriding the method "ReadByteArray".  In case this method needs to know what the command sent was to determine how to format the response, I have added a property currentactivequery which may be used to access this information.

Writing implementations

It is easy to create derived classes to create new implementations or to tweak existing implementations for a specific configuration. There are four abstract methods to define (override):

C#

protected abstract int ClearDevice(ref int errcode, ref string errmsg);
protected abstract int Send(string cmd, ref int errcode, ref string errmsg);
protected abstract int PollMAV(ref bool mav, ref byte statusbyte, ref int errcode, ref string errmsg);
protected abstract int ReceiveByteArray(ref byte[] arr, ref bool EOI, ref int errcode, ref string errmsg);

protected abstract void DisposeDevice();

VB:

Protected MustOverride Function ClearDevice(ByRef errcode As Integer, ByRef errmsg As String) As Integer
Protected MustOverride Function Send(ByVal cmd As String, ByRef errcode As Integer, ByRef errmsg As String) As Integer
Protected MustOverride Function PollMAV(ByRef mav As Boolean, ByRef statusbyte As Byte, ByRef errcode As Integer, ByRef errmsg As String) As Integer 'poll for status, return MAV bit

Protected MustOverride Function ReceiveByteArray(ByRef arr As Byte(), ByRef EOI As Boolean, ByRef errcode As Integer, ByRef errmsg As String) As Integer

Protected MustOverride Sub DisposeDevice()

See explanations in the code of IODevice class under the comment "interface abstract methods that have to be defined" for the meaning of parameters and return values.

Example: if your device does not comply with the 488.2 standard on the meaning of th status byte bits, you may need to re-interpret this byte to get the "message available" status, something like this:

protected override int PollMAV(ref bool mav, ref byte statusbyte, ref int errcode, ref string errmsg){

int pollresult = base.PollMAV(ref mav, ref statusbyte, ref errcode, ref errmsg);

if (pollresult == 0) { mav = (statusbyte & newmask)!=0;}//reinterpret received statusbyte with a different mask

return pollresult;

}

Some technical details: query sequence and lock levels

There are three lock levels: bus, device, queue.

The query sequence is the same for blocking/async commands, the only difference is that 1) they are executed on different threads 2) for blocking commands executed on the main thread, processing application events is allowed in waiting loops to not to freeze the GUI (this may be disabled setting "eventsallowed" to false)

In order to prevent deadlocks, all locks are released at the time the user callback is invoked (therefore in principle there are no restrictions on what you can do inside a callback function).

Note that locking the GPIB bus is not necessary for modern Gpib libraries that are said to be thread-safe (e.g. NI NET and Visa library, I did not check for the others). However there might be an advantage of adding this additional lock level: here both bus and device locking is made via “Monitor.TryEnter” repeated in a loop, this to prevent freezing GUI in blocking calls when the bus or device is not available immediately.

In order for the bus locking to be flexible, the respective lock object is selected from an array whose index is in the variable

protected int interfacelockid;

This variable has to be set by the child class constructor. The idea is that if two different interfaces that the can be used concurently are present they will use different lock objects so that locking will not degrade performance. In this code "interfacelockid" is set to 0 for NI-NET/NIVisa, 1 for ADLink and 2 for gpib488. If interfacelockid is set to a negative value then no bus locking is performed. This is the case for the serial port implementation (there is no common bus or common driver and the "SerialPort" .NET class is thread-safe).

Query Sequence

lock device

check if a minimum delay ("delayop") elapsed since last operation, otherwise wait until the condition is true (delay cannot be interrupted)

send command (bus locked during send)

wait a delay “delayread” (can be interrupted from another thread calling WakeUp() )

if send ok and response expected:

  • if polling enabled: poll status byte periodically until MAV bit is set, quit if timeout or abort (waiting between subsequent polling trials can be interrupted by any other thread calling WakeUp() )
  • try to read periodically (bus locked during each read) , quit if "readtimeout" elapsed or abort (waiting between subsequent reading trials can be interrupted by any other thread calling WakeUp() )
  • if “checkEOI” enabled: repeat reading until EOI set, appending new data to receive buffer  

if any gpib function returned error: clear device, if “showmessages” flag set then show message

unlock device

for async threads, if user callback function defined: send message to GUI thread to call it (if "cbwait" flag set then wait until the callback returns)

  • if error and retry enabled: wait a delay “delayretry” (cannot be interrupted), then repeat the whole query process unless aborted by user

Here the bus is locked during each I/O operation but not between write and read, however the device is locked during the whole query. Therefore when a thread is waiting for response from a device other async threads can send commands to their respective devices. Interleaving is obtained automatically.

Also, for the same reason interleaving is possible for blocking calls if “eventsallowed” field is set (default): this flag enables processing application events during delay and wait loops in blocking calls: if there are blocking calls in timers then timer events can be processed between write and read (this is disabled if “eventsallowed” is set to false, then however the GUI may freeze during blocking calls!).

It must be precised here that the device lock, which is based on interthread synchronisation, does not prevent a simultaneous querying of the same device by different processes (e.g. if a C# application runs in parallel with a Labview program). For such situations some GPIB drivers provide their own lock instructions ensuring an interprocess synchronization, these however are not used in the current version.

 

Demo

see "test" projects included

History

1st version submitted on Jan 26, 2017

13 Feb 2017 :

  • C# version: corrected an address format problem in VisaDevice.cs (by mistake I had included a version which tried to format it for GPIB like the other classes, now the formatting has been removed so it will work correctly for all Visa address formats like USB etc.)
  • minor updates in other files

10 Apr 2017 :

  • added a possibility of asynchronous callback from the driver, for time-critical applications.  See section Asynchronous interface callbacks, the query sequence and the description of modified implementations: GPIBDevice_NINET , SerialDevice

  • DevicesForm update method improved
  • minor modification in GPIBDevice_ADLink, see class description
  • minor updates in test forms

24 May 2017

  • asynchronous callbacks on device service request are now implemented for VisaDevice, see class description for details 
  • class GPIBDevice_NINET :  driver configuration modified (autopolling disabled) so that asynchronous callbacks are more reliable (see class description)
  • minor updates in the main class IODevices (slight changes in error messages; rearming for asynchronous callback when command sent;  in C# version some parameters changed from "ref" to "out")  
  • test forms adapted to changes

contact: pawel.wzietek (at) u-psud.fr

 

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Pawel Wzietek
France France
No Biography provided

You may also be interested in...

Comments and Discussions

 
GeneralMy vote of 5 Pin
Harrison Walker2-Jun-17 6:50
memberHarrison Walker2-Jun-17 6:50 
QuestionMy vote of 5 Pin
loPetS24-May-17 21:20
memberloPetS24-May-17 21:20 
PraiseA true hacker Pin
3Drti12-Apr-17 2:39
member3Drti12-Apr-17 2:39 
GeneralRe: A true hacker Pin
Pawel Wzietek12-Apr-17 3:33
memberPawel Wzietek12-Apr-17 3:33 
GeneralMy vote of 5 Pin
Jakub Szymanowski11-Apr-17 3:25
professionalJakub Szymanowski11-Apr-17 3:25 
GeneralRe: My vote of 5 Pin
Pawel Wzietek11-Apr-17 5:29
memberPawel Wzietek11-Apr-17 5:29 
QuestionBoard ID of 2nd device Pin
Member 1306385120-Mar-17 7:21
memberMember 1306385120-Mar-17 7:21 
AnswerRe: Board ID of 2nd device Pin
Member 1306385120-Mar-17 7:38
memberMember 1306385120-Mar-17 7:38 
GeneralRe: Board ID of 2nd device Pin
Member 1306385120-Mar-17 8:42
memberMember 1306385120-Mar-17 8:42 
AnswerRe: Board ID of 2nd device Pin
Pawel Wzietek21-Mar-17 3:09
memberPawel Wzietek21-Mar-17 3:09 
GeneralRe: Board ID of 2nd device Pin
Member 1306385121-Mar-17 6:22
memberMember 1306385121-Mar-17 6:22 
GeneralRe: Board ID of 2nd device Pin
Pawel Wzietek21-Mar-17 7:25
memberPawel Wzietek21-Mar-17 7:25 
GeneralRe: Board ID of 2nd device Pin
Member 1306385121-Mar-17 7:50
memberMember 1306385121-Mar-17 7:50 
GeneralRe: Board ID of 2nd device Pin
Pawel Wzietek21-Mar-17 8:20
memberPawel Wzietek21-Mar-17 8:20 
GeneralRe: Board ID of 2nd device Pin
Member 1306385121-Mar-17 8:54
memberMember 1306385121-Mar-17 8:54 
GeneralRe: Board ID of 2nd device Pin
Pawel Wzietek21-Mar-17 9:25
memberPawel Wzietek21-Mar-17 9:25 
GeneralRe: Board ID of 2nd device Pin
Member 1306385121-Mar-17 9:30
memberMember 1306385121-Mar-17 9:30 
GeneralRe: Board ID of 2nd device Pin
Pawel Wzietek21-Mar-17 10:20
memberPawel Wzietek21-Mar-17 10:20 
GeneralRe: Board ID of 2nd device Pin
Member 1306385121-Mar-17 7:04
memberMember 1306385121-Mar-17 7:04 
GeneralRe: Board ID of 2nd device Pin
Pawel Wzietek21-Mar-17 7:56
memberPawel Wzietek21-Mar-17 7:56 
GeneralRe: Board ID of 2nd device Pin
Member 1306385121-Mar-17 9:15
memberMember 1306385121-Mar-17 9:15 
GeneralRe: Board ID of 2nd device Pin
Pawel Wzietek21-Mar-17 10:02
memberPawel Wzietek21-Mar-17 10:02 
GeneralRe: Board ID of 2nd device Pin
Member 1306385121-Mar-17 10:13
memberMember 1306385121-Mar-17 10:13 
GeneralRe: Board ID of 2nd device Pin
Pawel Wzietek21-Mar-17 10:25
memberPawel Wzietek21-Mar-17 10:25 
GeneralRe: Board ID of 2nd device Pin
Member 1306385121-Mar-17 10:28
memberMember 1306385121-Mar-17 10:28 
GeneralRe: Board ID of 2nd device Pin
Pawel Wzietek21-Mar-17 11:27
memberPawel Wzietek21-Mar-17 11:27 
GeneralRe: Board ID of 2nd device Pin
Member 1306385121-Mar-17 12:02
memberMember 1306385121-Mar-17 12:02 
GeneralRe: Board ID of 2nd device Pin
Pawel Wzietek21-Mar-17 12:41
memberPawel Wzietek21-Mar-17 12:41 
GeneralRe: Board ID of 2nd device Pin
Pawel Wzietek21-Mar-17 13:06
memberPawel Wzietek21-Mar-17 13:06 
GeneralRe: Board ID of 2nd device Pin
Member 1306385122-Mar-17 5:35
memberMember 1306385122-Mar-17 5:35 
GeneralRe: Board ID of 2nd device Pin
Member 1306385131-Mar-17 4:45
memberMember 1306385131-Mar-17 4:45 
GeneralRe: Board ID of 2nd device Pin
Pawel Wzietek31-Mar-17 12:16
memberPawel Wzietek31-Mar-17 12:16 
GeneralRe: Board ID of 2nd device Pin
Pawel Wzietek3-Apr-17 22:28
memberPawel Wzietek3-Apr-17 22:28 
GeneralRe: Board ID of 2nd device Pin
Member 130638515-Apr-17 12:54
memberMember 130638515-Apr-17 12:54 
GeneralRe: Board ID of 2nd device Pin
Pawel Wzietek7-Apr-17 0:02
memberPawel Wzietek7-Apr-17 0:02 
GeneralRe: Board ID of 2nd device Pin
Member 130638517-Apr-17 4:33
memberMember 130638517-Apr-17 4:33 
GeneralRe: Board ID of 2nd device Pin
Member 1306385113-Apr-17 8:49
memberMember 1306385113-Apr-17 8:49 
GeneralRe: Board ID of 2nd device Pin
Pawel Wzietek14-Apr-17 1:32
memberPawel Wzietek14-Apr-17 1:32 
GeneralRe: Board ID of 2nd device Pin
Harrison Walker14-Apr-17 6:22
memberHarrison Walker14-Apr-17 6:22 
GeneralRe: Board ID of 2nd device Pin
Pawel Wzietek14-Apr-17 9:55
memberPawel Wzietek14-Apr-17 9:55 
GeneralRe: Board ID of 2nd device Pin
Harrison Walker14-Apr-17 10:39
memberHarrison Walker14-Apr-17 10:39 
GeneralRe: Board ID of 2nd device Pin
Pawel Wzietek15-Apr-17 3:16
memberPawel Wzietek15-Apr-17 3:16 
GeneralRe: Board ID of 2nd device Pin
Harrison Walker17-Apr-17 5:46
memberHarrison Walker17-Apr-17 5:46 
GeneralRe: Board ID of 2nd device Pin
Pawel Wzietek17-Apr-17 6:13
memberPawel Wzietek17-Apr-17 6:13 
GeneralRe: Board ID of 2nd device Pin
Harrison Walker17-Apr-17 6:23
memberHarrison Walker17-Apr-17 6:23 
GeneralRe: Board ID of 2nd device Pin
Pawel Wzietek17-Apr-17 7:48
memberPawel Wzietek17-Apr-17 7:48 
GeneralRe: Board ID of 2nd device Pin
Harrison Walker19-Apr-17 4:47
memberHarrison Walker19-Apr-17 4:47 
GeneralRe: Board ID of 2nd device Pin
Pawel Wzietek19-Apr-17 4:52
memberPawel Wzietek19-Apr-17 4:52 
GeneralRe: Board ID of 2nd device Pin
Harrison Walker19-Apr-17 5:03
memberHarrison Walker19-Apr-17 5:03 
GeneralRe: Board ID of 2nd device Pin
Pawel Wzietek14-Apr-17 3:48
memberPawel Wzietek14-Apr-17 3:48 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.171114.1 | Last Updated 24 May 2017
Article Copyright 2017 by Pawel Wzietek
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid