Added comments to the C code. Make the release build have symbols. Fixed the license.
authorPat Thoyts <patthoyts@users.sourceforge.net>
Mon, 17 Mar 2008 10:34:38 +0000 (10:34 +0000)
committerPat Thoyts <patthoyts@users.sourceforge.net>
Mon, 17 Mar 2008 10:34:38 +0000 (10:34 +0000)
README
license.terms
makefile.vc
tclftd2xx.c

diff --git a/README b/README
index e3c233c17c85fc23198c29018ec4bb379f7cf6cf..dfb6780528a155124acf9f156ccb74fac6a51935 100644 (file)
--- a/README
+++ b/README
@@ -7,20 +7,21 @@ International Ltd. D2XX driver library. This is a commonly used USB
 device driver. See http://www.ftdichip.com/ for more details about
 their products and the drivers themselves.
 
-This package is in no way affiliated with Future Tchnology Devices
-International Ltd.
-
-The package provides some access to the general commands exposed by
-the D2XX library and in particular provides a Tcl channel interface to
-the device over which you can send and receive data. The channel acts
-as a standard Tcl channel and supports fileevents and asynchonous
-reading (non-blocking). Via the fconfigure command some of the device
-settings can be read and controlled - these include the timeouts and
-latency values along with buffersize and blocking mode.
+The author is not affiliated with Future Technology Devices
+International Ltd and this code does not represent the above company
+in any way.
+
+The package provides some access to the commands exposed by the D2XX
+library and in particular provides a Tcl channel interface to the
+device over which you can send and receive data. The channel acts as a
+standard Tcl channel and supports fileevents and nonblocking
+reads. The device settings can be managed using the standard Tcl
+channel configuration configuration command. These include the
+timeouts and latency values along with buffersize and blocking mode.
 
 FTDI provide a Linux driver for these devices and whilst this has not
 been tested there is no intrinsic reason why this package should not
-work fine as a Linux package with a small amount of porting.
+be easily ported to operate on that platform.
 
 
 COMMANDS
@@ -30,12 +31,14 @@ ftd2xx open ?-serial? ?-description? ?-location? name
   open the named device and create a channel for it. The driver
   supports naming devices using one of the serial number, a
   descriptive device name or on windows a location (port number).
+  See the 'ftd2xx list' command to obtain a list of attached devices
+  with their names and locations.
 
 ftd2xx list
 
   list all the supported devices currently connected. Each list
   element is itself a name-value list providing all the information
-  available on the device including the serial number, localtion,
+  available on the device including the serial number, location,
   description, device id, device handle and status.
 
 ftd2xx reset channel
@@ -48,9 +51,10 @@ ftd2xx purge channel
 
   purge the device buffers for the device identified by the channel
 
+
 INSTALLATION
 
-The FTDI D2XX driver package should be downloaded from the web-site
+The FTDI D2XX driver package should be downloaded from the website
 (http://www.ftdichip.com/Drivers/D2XX.htm) and the Makefile.vc
 modified such that FTDI_INCLUDE points to the directory containing the
 ftdi2xx.h header and FTDI_LIB points to the directory containing the
@@ -61,4 +65,9 @@ Using MSVC (6 to 9) do:
  nmake -f Makefile.vc TCLDIR=c:\Tcl install
 
 If the ftd2xx.dll is not installed already, it should be manually
-copied to the installation folder (eg: c:\tcl\lib\tclftd2xx)
+copied to the installation folder (eg: c:\tcl\lib\tclftd2xx).
+
+You may have to install drivers for your device (ftdibus.sys or
+ftd2xx.sys). This should be provided by the device manufacturer as the
+driver .INF file needs to be specifically configured for each device
+type.
index 4bda16e44e9b2bf6cdfe3b3574978d56ae4aba20..d0015c78a0c563f10b677dc237d3a0bef1fcf7bb 100644 (file)
@@ -1,6 +1,6 @@
 LICENSE ("MIT-style")
 
-This software is Copyright (C) 2003 Joe English and other parties.
+This software is Copyright (C) 2008 Pat Thoyts.
 
 The following terms apply to all files associated with this software
 unless explicitly disclaimed in individual files.  
index 19a81bc5f84cd2770271d569bab4bb80aa34434c..abcc20f660beaa9e25c772e499e97ab0395445fb 100644 (file)
@@ -219,7 +219,7 @@ COMPATDIR   = $(ROOT)\compat
 !if !$(DEBUG)
 !if $(OPTIMIZING)
 ### This cranks the optimization level to maximize speed
-cdebug = $(OPTIMIZATIONS)
+cdebug = -Zi $(OPTIMIZATIONS)
 !else
 cdebug =
 !endif
@@ -273,7 +273,7 @@ ldebug      = -debug:full -debugtype:cv
 ldebug = $(ldebug) -nodefaultlib:msvcrt
 !endif
 !else
-ldebug = -release -opt:ref -opt:icf,3
+ldebug = -debug -opt:ref -opt:icf,3
 !endif
 
 ### Declarations common to all linker options
index a344131b0739c972e402c1a4249667e3b6878536..def15b8341fddcafb4fba560fc85f76fa8f592e2 100644 (file)
@@ -4,7 +4,7 @@
  *
  * ----------------------------------------------------------------------
  *     See the accompanying file 'licence.terms' for the software license.
- *     In essence - this is MIT licencensed code.
+ *     In essence - this is MIT licensed code.
  * ----------------------------------------------------------------------
  */
 
@@ -34,6 +34,7 @@ typedef struct Channel {
     unsigned long rxtimeout;
     unsigned long txtimeout;
     FT_HANDLE handle;
+    HANDLE event;
 } Channel;
 
 typedef struct ChannelEvent {
@@ -44,6 +45,7 @@ typedef struct ChannelEvent {
 
 typedef struct Package {
     struct Channel *headPtr;
+    unsigned long count;
     unsigned long uid;
 } Package;
 
@@ -76,6 +78,14 @@ static Tcl_ChannelType Ftd2xxChannelType = {
     NULL /*ChannelWideSeek*/
 };
 
+/**
+ * Close the channel and clean up all allocated resources. This requires
+ * removing the channel from the linked list (hence we need some way to
+ * access the head of the list which is in the Package structure).
+ * This function is called either from an explicit 'close' call from script
+ * or when the interpreter is deleted.
+ */
+
 static int
 ChannelClose(ClientData instance, Tcl_Interp *interp)
 {
@@ -86,10 +96,15 @@ ChannelClose(ClientData instance, Tcl_Interp *interp)
     FT_STATUS fts;
 
     OutputDebugString("ChannelClose\n");
-    fts = FT_Purge(instPtr->handle, FT_PURGE_RX | FT_PURGE_TX);
+    CloseHandle(instPtr->event);
+    if ((fts = FT_Purge(instPtr->handle, FT_PURGE_RX | FT_PURGE_TX)) != FT_OK) {
+       OutputDebugString("ChannelClose error: ");
+       OutputDebugString(ConvertError(fts));
+    }
     fts = FT_Close(instPtr->handle);
     if (fts != FT_OK) {
-       Tcl_AppendResult(interp, "error closing device: ",
+       Tcl_AppendResult(interp, "error closing \"",
+                        Tcl_GetChannelName(instPtr->channel), "\": ",
                         ConvertError(fts), NULL);
        r = TCL_ERROR;
     }
@@ -99,11 +114,19 @@ ChannelClose(ClientData instance, Tcl_Interp *interp)
        tmpPtrPtr = &(*tmpPtrPtr)->nextPtr;
     }
     *tmpPtrPtr = instPtr->nextPtr;
-
+    --pkgPtr->count;
     ckfree((char *)instPtr);
     return r;
 }
 
+/**
+ * Read data from the device. We support non-blocking reads by checking the 
+ * amount available in the receive queue. Note that the FTD2XX devices implement
+ * a read timeout (which we may set via fconfigure) and the blocking read will
+ * terminate when the timeout triggers anyway.
+ * If the device is disconnected then we will get a read error.
+ */
+
 static int
 ChannelInput(ClientData instance, char *buffer, int toRead, int *errorCodePtr)
 {
@@ -125,27 +148,51 @@ ChannelInput(ClientData instance, char *buffer, int toRead, int *errorCodePtr)
        }
     }
     if (FT_Read(instPtr->handle, buffer, toRead, &cbRead) != FT_OK) {
+       OutputDebugString("ChannelInput error: ");
        OutputDebugString(ConvertError(fts));
-       *errorCodePtr = EINVAL;
+       switch (fts) {
+           case FT_DEVICE_NOT_FOUND: *errorCodePtr = ENODEV; break;
+           default: *errorCodePtr = EINVAL; break;
+       }
+       cbRead = -1;
     }
     return (int)cbRead;
 }
 
+/**
+ * Write to the device. We don't have any non-blocking handling for write as it
+ * isnt obvious how to do this. However the devices implement a write timeout 
+ * which likely cause us to return and retry.
+ * If the device is disconnected we will get an error.
+ */
+
 static int
 ChannelOutput(ClientData instance, const char *buffer, int toWrite, int *errorCodePtr)
 {
     Channel *instPtr = instance;
+    FT_STATUS fts = FT_OK;
     char sz[80];
     DWORD cbWrote = 0;
     DWORD dwStart = GetTickCount();
-    if (FT_Write(instPtr->handle, (void *)buffer, toWrite, &cbWrote) != FT_OK) {
-       *errorCodePtr = EINVAL;
+    if ((fts = FT_Write(instPtr->handle, (void *)buffer, toWrite, &cbWrote)) != FT_OK) {
+       OutputDebugString("ChannelOutput error: ");
+       OutputDebugString(ConvertError(fts));
+       switch (fts) {
+           case FT_DEVICE_NOT_FOUND: *errorCodePtr = ENODEV; break;
+           default: *errorCodePtr = EINVAL; break;
+       }
+       cbWrote = -1;
     }
-    sprintf(sz, "ChannelOutput %ld ms\n", GetTickCount()-dwStart);
+    sprintf(sz, "ChannelOutput %lu bytes in %ld ms\n", cbWrote, GetTickCount()-dwStart);
     OutputDebugString(sz);
     return (int)cbWrote;
 }
 
+/**
+ * Implement device control via the Tcl 'fconfigure' command.
+ * We can change the timeouts and the latency timer here.
+ */
+
 static int
 ChannelSetOption(ClientData instance, Tcl_Interp *interp,
                 const char *optionName, const char *newValue)
@@ -189,6 +236,12 @@ ChannelSetOption(ClientData instance, Tcl_Interp *interp,
     return TCL_OK;
 }
 
+/**
+ * Read the additional channel settings. The timeout values cannot be
+ * read from the device so we maintain the values in the channel instance
+ * data. The latency can be read back.
+ */
+
 static int
 ChannelGetOption(ClientData instance, Tcl_Interp *interp, 
                 const char *optionName, Tcl_DString *optionValue)
@@ -252,6 +305,15 @@ ChannelGetOption(ClientData instance, Tcl_Interp *interp,
     return r;
 }
 
+/**
+ * This function is called by Tcl to setup fileevent notifications
+ * on this channel. We only really support readable events (our channel
+ * type is basically always writable).
+ * Our channel state monitoring is actually done via the notifier. All
+ * that occurs here is to reduce the blocking time if our channel has
+ * readable events configured.
+ */
+
 static void
 ChannelWatch(ClientData instance, int mask)
 {
@@ -267,6 +329,10 @@ ChannelWatch(ClientData instance, int mask)
     }
 }
 
+/**
+ * Provide access to the underlying device handle.
+ */
+
 static int
 ChannelGetHandle(ClientData instance, int direction, ClientData *handlePtr)
 {
@@ -276,6 +342,10 @@ ChannelGetHandle(ClientData instance, int direction, ClientData *handlePtr)
     return TCL_OK;
 }
 
+/**
+ * Control the blocking mode.
+ */
+
 static int
 ChannelBlockMode(ClientData instance, int mode)
 {
@@ -289,6 +359,12 @@ ChannelBlockMode(ClientData instance, int mode)
     return TCL_OK;
 }
 
+/**
+ * If a fileevent has occured on a channel then we end up in this event handler
+ * function. We now notify the channel that an event is available. We also
+ * remove the pending flag to permit more events to be raised as needed.
+ */
+
 static int
 EventProc(Tcl_Event *evPtr, int flags)
 {
@@ -307,6 +383,13 @@ EventProc(Tcl_Event *evPtr, int flags)
     return 1;
 }
 
+/**
+ * This function is called to setup the notifier to monitor our
+ * channel for file events. Our CheckProc will be called anyway after some
+ * interval so we really only need to ensure that it is called at some 
+ * appropriate interval.
+ */
+
 static void
 SetupProc(ClientData clientData, int flags)
 {
@@ -327,6 +410,18 @@ SetupProc(ClientData clientData, int flags)
     Tcl_SetMaxBlockTime(&blockTime);
 }
 
+/**
+ * To support fileevents we have to check for any new data arriving. This
+ * is done by polling the device at intervals. To avoid making calls to the
+ * device we can use a Win32 event handle which will be signalled when
+ * the device has data for us. When this occurs we raise a Tcl event
+ * for this channel and queue it.
+ * An alternative method would be to have a secondary thread wait on all the
+ * event handles for all our channels. That would improve the latency at a
+ * cost to code simplicity and maintainability. However a second thread might
+ * help with non-blocking writes too so should be considered at some point.
+ */
+
 static void
 CheckProc(ClientData clientData, int flags)
 {
@@ -341,6 +436,7 @@ CheckProc(ClientData clientData, int flags)
        DWORD rx = 0, tx = 0, ev = 0;
        FT_STATUS fts = FT_OK;
 
+       /* already has an event queued so move on */
        if (chanPtr->flags & FTD2XX_PENDING) {
            continue;
        }
@@ -350,25 +446,32 @@ CheckProc(ClientData clientData, int flags)
            continue;
        }
 
-       if ((fts = FT_GetStatus(chanPtr->handle, &rx, &tx, &ev)) == FT_OK) {
-           if (rx != 0 || tx != 0 || ev != 0) {
-               int mask = 0;
-
-               mask = TCL_WRITABLE | ((rx)?TCL_READABLE:0);
-               //if (ev != 0) evPtr->flags |= TCL_EXCEPTION;
-               if (chanPtr->watchmask & mask) {
-                   ChannelEvent *evPtr = (ChannelEvent *)ckalloc(sizeof(ChannelEvent));
-                   chanPtr->flags |= FTD2XX_PENDING;
-                   evPtr->header.proc = EventProc;
-                   evPtr->instPtr = chanPtr;
-                   evPtr->flags = mask;
-                   Tcl_QueueEvent((Tcl_Event *)evPtr, TCL_QUEUE_TAIL);
+       if (WaitForSingleObject(chanPtr->event, 0) == WAIT_OBJECT_0) {
+           if ((fts = FT_GetStatus(chanPtr->handle, &rx, &tx, &ev)) == FT_OK) {
+               if (rx != 0 || tx != 0 || ev != 0) {
+                   int mask = 0;
+                   
+                   mask = TCL_WRITABLE | ((rx) ? TCL_READABLE : 0);
+                   //if (ev != 0) evPtr->flags |= TCL_EXCEPTION;
+                   if (chanPtr->watchmask & mask) {
+                       ChannelEvent *evPtr = 
+                           (ChannelEvent *)ckalloc(sizeof(ChannelEvent));
+                       chanPtr->flags |= FTD2XX_PENDING;
+                       evPtr->header.proc = EventProc;
+                       evPtr->instPtr = chanPtr;
+                       evPtr->flags = mask;
+                       Tcl_QueueEvent((Tcl_Event *)evPtr, TCL_QUEUE_TAIL);
+                   }
                }
            }
        }
     }
 }
 
+/**
+ * Called to remove the event source when the interpreter exits.
+ */
+
 static void
 DeleteProc(ClientData clientData)
 {
@@ -378,6 +481,10 @@ DeleteProc(ClientData clientData)
     ckfree((char *)pkgPtr);
 }
 
+/**
+ * Convert FTD2XX status errors into strings.
+ */
+
 static const char *
 ConvertError(FT_STATUS fts)
 {
@@ -402,6 +509,13 @@ ConvertError(FT_STATUS fts)
     return s;
 }
 
+/**
+ * Open a named device and create a Tcl channel to represent the open device
+ * and to enable communications with Tcl programs. By default these channels
+ * are configured to be binary and to have 500ms timeouts but all these can
+ * be configured at runtime using the 'fconfigure' command.
+ */
+
 static int
 OpenCmd(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *const objv[])
 {
@@ -409,6 +523,7 @@ OpenCmd(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *const objv
     const char *name = NULL;
     FT_HANDLE handle = 0;
     FT_STATUS fts = FT_OK;
+    HANDLE hEvent = INVALID_HANDLE_VALUE;
     int r = TCL_OK, index, nameindex = 2, ftmode = FT_OPEN_BY_SERIAL_NUMBER;
     const unsigned long rxtimeout = 500, txtimeout = 500;
     enum {OPT_SERIAL, OPT_DESC, OPT_LOC};
@@ -439,6 +554,12 @@ OpenCmd(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *const objv
     fts = FT_OpenEx((void *)name, ftmode, &handle);
     if (fts == FT_OK)
        fts = FT_SetTimeouts(handle, rxtimeout, txtimeout);
+    if (fts == FT_OK) {
+       hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
+        fts = FT_SetEventNotification(handle, FT_EVENT_RXCHAR, hEvent);
+       if (fts != FT_OK)
+           CloseHandle(hEvent);
+    }
     if (fts == FT_OK) {
         Channel *instPtr;
         char name[6+TCL_INTEGER_SPACE];
@@ -451,6 +572,7 @@ OpenCmd(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *const objv
        instPtr->rxtimeout = rxtimeout;
        instPtr->txtimeout = txtimeout;
         instPtr->handle = handle;
+       instPtr->event = hEvent;
         instPtr->channel = Tcl_CreateChannel(&Ftd2xxChannelType, name,
                                             instPtr, instPtr->validmask);
        Tcl_SetChannelOption(interp, instPtr->channel, "-encoding", "binary");
@@ -461,17 +583,22 @@ OpenCmd(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *const objv
        instPtr->pkgPtr = pkgPtr;
        instPtr->nextPtr = pkgPtr->headPtr;
        pkgPtr->headPtr = instPtr;
+       ++pkgPtr->count;
 
         Tcl_SetObjResult(interp, Tcl_NewStringObj(name, -1));
         r = TCL_OK;
     } else {
-        Tcl_AppendResult(interp, "failed to open device: \"",
+        Tcl_AppendResult(interp, "failed create device channel: \"",
                         name, "\": ", ConvertError(fts), NULL);
         r = TCL_ERROR;
     }
     return r;
 }
 
+/**
+ * Purge the device transmit and receive buffers.
+ */
+
 static int
 PurgeCmd(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *const objv[])
 {
@@ -503,6 +630,11 @@ PurgeCmd(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *const obj
     return TCL_OK;
 }
 
+/**
+ * Reset the device. This requires an open channel as a means of identifying the
+ * device to reset.
+ */
+
 static int
 ResetCmd(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *const objv[])
 {
@@ -534,6 +666,15 @@ ResetCmd(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *const obj
     return TCL_OK;
 }
 
+/**
+ * The implementation of the 'ftd2xx list' command. This function builds a list
+ * of all the available D2XX compatible devices connected. Each list element
+ * is a list of value-name pairs suitable for use with 'array set' or 'dict create'
+ * that return the various bits of information provided by the D2XX device info
+ * structure. Of note are the serial number and device description which may be
+ * required when opening the device.
+ */
+
 static int
 ListCmd(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *const objv[])
 {
@@ -629,6 +770,10 @@ EnsembleCmd(ClientData clientData, Tcl_Interp *interp,
     return TCL_ERROR;
 }
 
+/**
+ * Package initialization function.
+ */
+
 int DLLEXPORT
 Ftd2xx_Init(Tcl_Interp *interp)
 {
@@ -642,6 +787,7 @@ Ftd2xx_Init(Tcl_Interp *interp)
 
     pkgPtr = (Package *)ckalloc(sizeof(Package));
     pkgPtr->headPtr = NULL;
+    pkgPtr->count = 0;
     pkgPtr->uid = 0;
     Tcl_CreateEventSource(SetupProc, CheckProc, pkgPtr);
     Tcl_CreateObjCommand(interp, "ftd2xx", EnsembleCmd, pkgPtr, DeleteProc);