Automation Programming Guide

Automation programming can be done to remotely control device operations.  Queries and commands are performed over a TCP/IP socket interface connected to the device.  TCP/IP socket libraries are available in virtually all programming languages.  Queries and commands are all synchronous text style commands that interact with the device server to change properties and initiate operations in the same manner that the browser user interface clients interact with the device.  In fact, the automation protocol provided for automation programming is the same protocol as that used by the browser user interface, so everything that can be done from the browser user interface can also be done using automation programming.

If you're working on automation for the device, below are a few important topics that can help you get started.

Use PUTTY and CLI as debugging tool

Use PUTTY to connect using SSH to an interactive CLI (command line interpreter) while you are developing your automation software.  This can be a very helpful tool to monitor and examine and test automation commands and program running logic. 

Remember the responses to st? and ev? commands from your CLI connection will not necessarily be the same your automation connection receives because the st? responses represent command status for each individual connection, and likewise, the ev? responses represent events that are pertinent to the individual connection. 

See Also

Connecting with PUTTY


Connecting to socket port 923

BitWise device automation host servers listen on port 923, This is used for interactive-mode CLI access as well as for computer-to-computer automation.  To connect to the BitWise device, make sure to indicate the appropriate IP address, and port 923. 

All interactions are synchronous

Sockets are bi-directional byte streams that can be used to implement networking applications such as the automation of test equipment like BitWise devices.  Its important to remember that sending a command into one end of the socket and having the socket library's send() procedure return does not mean the action you requested has been completed.  The operating systems will send the command over to the device and the device's host automation driver will dequeue the command and finally initiate the operation.  If your request is a query, then,  you begin by sending a message to cause the device to respond with a reply.  Once you get this reply, you know the action has been completed.  Finally, since there is a single byte stream in each direction, you can be assured that a command issued to a server has been completed if you also send a query immediately afterwards and receive the reply to the query.  This is a helpful strategy to ensure that commands have completely executed, since the host automation server will execute commands and queries in order of receiving them, 

Also, every message the host automation server sends to your computer will be the result of some command or query you sent first to the device.  That is, the automation server never asynchronously sends you a message without having first been asked to do so.

Error status management - "stc" command and "st?" query

Error management is critical for good automation and to assist in faster debugging during development.  It is common to have automation code that isn't quite working right and for a failure to occur and blame to be assigned to the current command even if the actual command in error occurred previously and an error condition wasn't noticed at that time.  To prevent this, you can use the "stc" (Status Clear) command and the "st?" ("Status Query) query to prevent this. 

The Status Clear command clears a queue of error statuses that may have accumulated since the last time the queue was cleared.  Status queue elements are text messages of the form "[Error_Message]".  There may be multiple error statuses, in which case, they are separated by semi-colons.  Issuing the Status Query causes the automation host server to respond with the current contents of the error status queue.  If there are no entries in the queue, the sentinel value "[none]" is produced.  When you perform a Status Query operation, this automatically performs a clear of the status queue.  Each CLI connection has its own status queue.

Multiple commands may be present on one line issued to the automation host server, if they are separated by semicolons.  This code precedes user queries and commands with "stc;" in order to clear the status queue before performing the user operation.  Notice that since this mechanism performs a query (st?) and obtains a response from the automation host server before returning, this ensures synchronization because the user operation will be completed by the time these procedures return. 

For example, here is a C++ Library code segment that overrides lower-level SocketDevice class functions for querying a key value and for issuing a command with higher-level BitWiseDevice versions of these functions that add proper error detection and raise an exception if an error occurs.


char * BitwiseDevice::QueryResponse( char *buffer, int buflen, const char *command, ... ) /* add error-checking to QueryResponse */

{

       char outBuffer[4096+4] = "stc;";

       va_list argptr;

       va_start(argptr,command);


       vsnprintf(outBuffer+4,4096,command,argptr);

       va_end(argptr);


       base::QueryResponse(buffer,buflen,outBuffer);


       char inBuffer[4096];

       base::QueryResponse(inBuffer,4096,(char*)"st?\n");


       if( strcmp(inBuffer,"[none]") )

       {

               static char static_throw_buffer[4096];

               snprintf(static_throw_buffer,4096,"[%s]",inBuffer);

               throw (const char*)static_throw_buffer;

       }


       return buffer;

}


void BitwiseDevice::SendCommand( const char *command, ... ) /* add error-checking to SendCommand */

{

       char outBuffer[4096+4] = "stc;";

       va_list argptr;

       va_start(argptr,command);


       vsnprintf(outBuffer+4,4096,command,argptr);

       va_end(argptr);


       base::SendCommand(outBuffer);


       char inBuffer[4096];

       base::QueryResponse(inBuffer,4096,(char*)"st?\n");


       if( strcmp(inBuffer,"[none]") )

       {

               static char static_throw_buffer[4096];

               snprintf(static_throw_buffer,4096,"[%s]",inBuffer);

               throw (const char*)static_throw_buffer;

       }

}


Here is Python Library code segment for same functions:


#Override
def QueryResponse( self, command:str, maxLength:int = 4096 ) -> str:
    """Query response from command (ending with '\n') from socket device, with error handling."""

    response = super().QueryResponse( "stc;"+command, maxLength)
    statusResponse = super().QueryResponse( "st?\n")

    if statusResponse != "[none]":
        raise Exception("[" + statusResponse + "]")

    return response


# Override
def SendCommand(self, command:str ):
    """Send command (ending with '\n') to socket device, with error handling."""

    super().SendCommand( "stc;"+command)
    statusResponse = super().QueryResponse( "st?\n")

    if statusResponse != "[none]" :
        raise Exception("["+statusResponse+"]")

    return None




Monitoring long-running commands to completion

Operations that require a long running time are generally implemented with a command protocol that supports starting the operation and a query protocol for interrogating either whether the operation is still on going, or in some cases, the progress of the operation.

These mechanisms enable you start and monitor long operations, to know when these are completed.

For example, here a C++ code segment from the Git Repository that sets a desired clocking rate and waits for the changing clock rate to settle appropriately.

void BranchSyn::setClockRateGHz(double newValue) /* Internal clock rate */

{

    SendCommand("ClockRate %lf\n",newValue);

}


/* notice clock rate is 1/2 data rate */

void BranchPG::WaitForClockToSettle( double targetClockGHz, double timeoutSec, double toleranceGHz )

{

       double now = SocketDevice::timestamp();

       double timeout = now + timeoutSec;

       double readGHz = getReadRateGHz();


       while( now<timeout )

       {

               if(getDebugging())

                       fprintf(stderr,"Settle %.3lf GHz\n", readGHz );


               if( fabs(readGHz-targetClockGHz) <= toleranceGHz )

                       break;


               usleep(500*1000);

               now = SocketDevice::timestamp();


               readGHz = getReadRateGHz();

       }


       if( now >=timeout )

               throw "[Timeout_During_Clock_Settle]";

}


void test_002(char *ip_address, double clockRateGHz )

{

       PegaDevice Pega;

       Pega.Connect( ip_address );

       Pega.Syn.setClockRateGHz(clockRateGHz);

       Pega.PG.WaitForClockToSettle(clockRateGHz);

       Pega.Disconnect();

}


If you search the Git Repository for SocketDevice::timestamp() you will find other examples of this strategy in use.

For another example, here is a Python code segment for initiating auto-alignment and waiting for it to complete.

def AlignData(self, alignType: AlignBy = AlignBy.All):
    """Perform data alignment and wait until completed. """

    self.SendCommand("AlignData " + alignType.value + "\n")

    now = SocketDevice.timestamp()
    begin_time = now
    timeout = now + 30.0

    while now < timeout:
        time.sleep(0.5)
        now = SocketDevice.timestamp()
        if self.getDebugging():
            print("Aligning "+ "{:.1f}".format(now - begin_time))

        if not self.QueryResponse_bool("InProgress?\n"):
            break

    if now >= timeout:
        raise Exception("[Timeout_During_Alignment]")

    message = self.getAlignDataMsg();
    if not message.upper().startswith("SUCCESS"):
        raise Exception("["+message.replace(" ","_",)+"]")

    return None


If you search the Git Repository for SocketDevice.timestamp() you will find other examples of this strategy in use.

Receiving binary data from special commands

Whereas it is normal for every query and some commands to respond with a single text line of response ending with a newline (or carriage-return newline for interactive sessions), Some commands and queries respond with binary data instead.  These types of responses may contain multiple newline or carriage-return characters, so message receiving must be modified to no longer terminate upon receiving the normal end of line character(s).  Furthermore, the characters may also not be displayable ascii characters, depending on what the command or query is.  For these commands, the message transmitted from the BitWise device to your computer is preceded by a 4-byte integer in LITTLE ENDIAN format.  This integer contains the number of bytes that will subsequently be sent, so your receiving code should first read this integer, and then read that many bytes to ensure you are processing every byte of transmission.  Notice in odd cases, the byte count may be zero; in which case, you would read no further bytes.

As an example, the Pattern Generator Tub analysis produces a number of results that can be obtained using the "Tub:FetchResults" command.  When connected interactively, the response to this command is shown below:

Notice in interactive mode, the response is preceded by a line containing "[175 Bytes]".  This contents is representation of the 4-byte integer value that is communicated when not in interactive mode.

For example, here is a C++ code segment demonstrating how the function of querying a binary response is implemented.    Notice further that socket recv operations may not reply with every byte of a message.  In fact, for large messages, it is likely that many recv operations are required to transfer the entire message.  The code segment below shows how to do this.


char *SocketDevice::QueryBinaryResponse( int *pcount, const char *command, ... ) /* caller responsible for free(return_value) */

{

       char outBuffer[4096];

       va_list argptr;

       va_start(argptr,command);


       vsnprintf(outBuffer,4096,command,argptr);

       va_end(argptr);


       int status;

       status = Send( outBuffer, strlen(outBuffer) );

       if( status != (int)strlen(outBuffer) )

               throw "[Error_Sending_Command]";


       int count=0;

       /* okay for 4-byte little-endian architectures */

       if( sizeof(int)!=4 )

               throw "[Requires_4_Byte_Int]";


       status = recv( getSock(), (char*)&count,4, 0 );

       if( status!=4 )

               throw "[Error_Receiving_Count]";


       if(pcount)

               *pcount = count;


       char *retn = (char*) malloc( count+1 );

       retn[count]=0 ;


       if( count>0 )

       {

               try

               {

                       int total=0;

                       while(total<count)

                       {

                               int xfer = recv( getSock(), retn+total,count-total,0);

                               if( xfer<=0 )

                                       throw "[Received_Zero_Byte_Buffer]";


                               if( xfer<0 )

                                       throw "[Error_Receiving_Buffer]";


                               total += xfer;

                               usleep(1); /* ensure scheduler task-switch */

                       }

               }

               catch(...)

               {

                       free(retn);

                       throw;

               }

       }


       return retn;

}


Here is an example of the same operation in Python.

def QueryBinaryResponse( self, command:str ) -> bytes :
    """Query array of bytes response from command (ending with '\n') from socket device."""

    if not isinstance(command, str) :
        raise Exception("[Invalid_Command_Type]")

    if not self.IsConnected:
        raise Exception("[Not_Connected]")

    self.Sock.send( bytes(command,'utf-8') )

    countBytes = self.Sock.recv(4)
    if len(countBytes) != 4:
        raise Exception("[Missing_Count_Response]")

    count = int.from_bytes(countBytes, byteorder="little")
    return_value = bytes(0)

    if count>0 :
        total = 0
        while total<count :
            portion = self.Sock.recv(count-total)
            amount = len(portion)

            if amount == 0:
                raise Exception("[Error_Receiving_Buffer]")

            return_value += portion
            total += amount
        pass

    return return_value


Sending binary data for special commands

Similar to receiving binary messages, there are some commands that expect to be immediately followed by specific binary data messages.  This direction of binary transfer is accomplished using the same mechanism of preceding the message with a 4-byte LITTLE ENDIAN integer containing the number of bytes to be transferred subsequently.

Here is an example of how this is done in C++ Library

void SocketDevice::SendBinaryCommand( const char *buffer, int count, const char *command, ... )

{

       char outBuffer[4096];

       va_list argptr;

       va_start(argptr,command);


       vsnprintf(outBuffer,4096,command,argptr);

       va_end(argptr);


       if( strlen(outBuffer)==0 )

               throw "[Sending_Empty_Command]";


       int status;

       status = Send(outBuffer,strlen(outBuffer));

       if( status != (int)strlen(outBuffer) )

               throw "[Error_Sending_Command]";


       /* okay for 4-byte little-endian architectures */

       if( sizeof(int)!=4 )

               throw "[Requires_4_Byte_Int]";


       status = send(getSock(),(char*)&count,4,0);

       if( status != 4 )

               throw "[Error_Sending_Count]";


       status = send(getSock(),buffer,count,0);

       if( status != count )

               throw "[Error_Sending_Buffer]";

}


Here is an example of how this is done in Python Library

def SendBinaryCommand(self, command: str, buffer: bytes):
    """Send command (ending with '\n') followed by 4-byte count and array of bytes to socket device."""

    if not isinstance(command, str):
        raise Exception("[Invalid_Command_Type]")

    if not isinstance(buffer, bytes):
        raise Exception("[Invalid_Buffer_Type]")

    if not self.IsConnected:
        raise Exception("[Not_Connected]")

    count = len(buffer)

    self.Sock.send(bytes(command,'utf-8'))
    self.Sock.send(count.to_bytes(4, byteorder='little'))
    self.Sock.send(buffer)

    return None



Always start from a known condition

Each BitWise device has many many properties that determine how operations are performed.  A common problem with Automation programming is that many properties may be set in the device using interactive or user interface modes, that cause the device to operate differently than assumed.  One strategy is for automation programming to take responsibility for every property that can affect its operations.  This is generally a good idea, but can also be difficult if there are many many properties.  A second recommended strategy is to start your automation programming by always restoring the device to its factory conditions using the "restore [factory]" command.  Once you initiate a restore operation, it is important to monitor the progress to completion because it can take a few seconds.  You don't want to send any other commands or while a restore configuration operation is underway.


Here is an example from the C++ Library in the Git Repository that demonstrates this mechanism:


/* specifying configurations: */

/* "[recent]"  ...  most recent settings */

/* "[factory]"  ... factory settings */

/* "[startup]"  ... settings from selectable startup configuration file */

/* full-path-name ... settings from fully-specified configuration file path  */

/* filename-only ... settings from file located in configuration folder */


void BitwiseDevice::SaveConfiguration( const char *configuration )

{

       base::SendCommand( "save \"%s\"\n", configuration );

}


void BitwiseDevice::RestoreConfiguration( const char *configuration )

{

       App.Stop(); // just to make sure


       base::SendCommand( "stc; restore \"%s\"\n", configuration );/* use base: to avoid error checking */


       double now = timestamp();

       double timeout = now + 30.0;

       double begin_time=now;


       while( now < timeout )

       {

               usleep(500*1000);

               now = timestamp();

               printf("Restoring configuration %.1lf\n",now-begin_time);


               char buffer[4096];

               base::QueryResponse(buffer,4096,"inprogress\n"); /* use base: to avoid error checking */

               if( buffer[0]=='F'||buffer[0]=='0' )

                       break;

       }


       if( now >= timeout )

               throw "[Timeout_Restoring_Configuration]";


       printf("Restoring configuration complete %.1lf\n",timestamp()-begin_time);

       base::SendCommand( "stc\n");/* use base: to avoid error checking */

}


Here is the same example from the Python Library:



def SaveConfiguration(self, configuration:str ):
    """Restore configuration file and optionally pause while operation completes.

    specifying configurations:
    [recent]  ...  most recent settings
    [factory]  ... factory settings
    [startup]  ... settings from selectable startup configuration file
    full-path-name ... settings from fully-specified configuration file path
    filename-only ... settings from file located in configuration folder
    """

    super().SendCommand( "stc;"+"save \"" + configuration + "\"\n" )
    super().SendCommand( "stc\n")
    return None

def RestoreConfiguration(self, configuration:str ):
    """Restore configuration file and optionally pause while operation completes.

    specifying configurations:
    [recent]  ...  most recent settings
    [factory]  ... factory settings
    [startup]  ... settings from selectable startup configuration file
    full-path-name ... settings from fully-specified configuration file path
    filename-only ... settings from file located in configuration folder
    """

    self.App.Stop() # just to make sure

    super().SendCommand( "stc;"+"restore \"" + configuration + "\"\n" )

    now = SocketDevice.timestamp()
    timeout = now + 30.0
    begin_time = now

    while now < timeout:
        time.sleep(0.5)
        now = SocketDevice.timestamp()
        print("Restoring configuration " + "{:.1f}".format(now - begin_time) )

        response = super().QueryResponse("inprogress\n")
        if response == "F" or response == "0":
            break

    if now >= timeout:
        raise Exception("[Timeout_Restoring_Configuration]")

    super().SendCommand( "stc\n")
    return None

How to start running a specific application

Each BitWise device provides a series of analysis applications that are initiated from the user interface by first opening a user interface Tab containing the application (or creating a new tab and adding the application to it from the application menu), and then, by pressing the Run or Single button to begin the analysis application.  Each device has multiple applications available and each user interface Tab may have more than one application present that may be requested to be Run simultaneously (if they are compatible).  The management of running applications is provided by the "App" category of properties and commands shown below.


Properties:



App:List - array of possible user-interface applications provided on the device

App:RunList - array of server run-objects that produce output for various user-interface applications

App:RunActive - array of boolean values indicating whether the current viewing Tab activates associated run-objects

App:RunState - array of running states for each run-object indicating if individual objects are running or stopped

App:RunDurLimit - means to specify how long a run session should run for.  If zero, no limit

App:Tab - current tab name


Commands:



App:Clear - clears intermediate analysis results

App:GuiReset - resets Tabs to factory condition

App:Refresh - refreshes GUI layout (diagnostic only)

App:Run - initiates all running objects on current Tab if they are compatible.  If "Once" as single parameter, then issues Run-once (aka Single) operation.

App:Stop - stops any currently running run objects


Here is an example from the C++ Library of using the "App:Tab", "App:Run", "App:Stop", in order to open a tab, initiate a running analysis, and monitor it while it runs.


void test_001()

{

       PegaDevice Pega;


       Pega.Connect( "192.168.1.167" );

       Pega.Stop();

       Pega.App.setTab("TUB");

       Pega.ED.AlignData(BranchED::AlignBy::All);

       Pega.RunSingle();


       static const double TIMEOUT_SEC=300.0;

       double now = SocketDevice::timestamp();

       double timeout = now + TIMEOUT_SEC;


       while( now<timeout && Pega.getIsRunning() )

       {

               usleep( 200*1000 ); /* poll 5 times per second */

               now=SocketDevice::timestamp();


               int progress = Pega.Tub.getProgress100Pcnt();

               printf("Tub progress: %d%%   \r", progress );

       }


       printf("\n");


       Pega.Stop();


       if( now>=timeout )

               throw "[Stop_Timeout]";


       Pega.Tub.getStatusMsg( buffer, 4096 );

       printf("TUB STATUS: %s\n", buffer );


       char *results = Pega.Tub.FetchResults();

       if( results!=0 )

       {

               printf("\nRESULTS:\n%s\n",results);

               free(results);

       }


       Pega.Disconnect();

}


And here is Python Library code segment for the same function:



def test_001():
    Pega = PegaDevice()
    try:
        Pega.Connect("192.168.1.176")

        Pega.Stop()
        Pega.App.setTab("TUB")
        Pega.ED.AlignData(BranchED.AlignBy.All)
        Pega.RunSingle()

        now = SocketDevice.timestamp()
        timeout = now + 600;

        last_progress = -1
        while now < timeout and Pega.getIsRunning():
            time.sleep(0.5)
            now = SocketDevice.timestamp()

            progress = Pega.Tub.getProgress100Pcnt()
            if progress != last_progress:
                print('\r'+'='*progress+'-'*(100-progress)+' ' + str(progress) + "% ", end='', flush=True)
                last_progress = progress

        Pega.Stop()

        progress = Pega.Tub.getProgress100Pcnt()
        print('\r' + '=' * progress + '-' * (100 - progress) + ' ' + str(progress) + "% ", flush=True)

        if now >= timeout:
            raise Exception("[Tub_Completion_Timeout]")

        status_message = Pega.Tub.getStatusMsg()
        print("STATUS: " + status_message)

        results = Pega.Tub.FetchResults()
        print("RESULTS:\n" + results)

    finally:
        Pega.Disconnect()
        Pega = None
    return None


How to determine if an application is running

The "App:RunState?" property responds with an array of running states (Stop, Run, RunOnce) for each of the running objects supported by the automation host server.  The "App:RunList?" property provides the associated names for each of these running objects.  

Here is an example from the C++ Library for determining if any run object is running:

bool BitwiseDevice::getIsRunning()

{

       char buffer[4096];

       if( App.getRunState(buffer,4096)==NULL )

               throw "[Unable_To_Retrieve_Run_State]" ;


       char *ptr = strtok( buffer, "{}," );

       bool onState = false;

       while ( ptr!= NULL && !onState )

       {

               onState = onState || (strncasecmp(ptr, "Stop", 4) != 0);

               ptr = strtok(NULL,"{},");

       }


       return onState ;

}


And here is the same function from the Python Library:

def getIsRunning(self) ->bool :
    response = self.QueryResponse("App:RunState?\n")
    if len(response) < 2:
        raise Exception("[Invalid_RunState_Response]")

    tokens = response[1:-1].split(",")
    return_value = False
    for itm in tokens:
        if itm != "Stop":
            return_value = True
            break

    return return_value



Use the Public BitWise Automation Git Repository

You can get started quickly by leveraging a community-based Git repository intended to provide automation programming resources.    We strongly recommend using this repository because we have incorporated best-practices for specific automation tasks in the programming and you will want to benefit from this.  We realize how important automation programming is for being successful using our equipment, so please contact us immediately if you have any questions.

For repository information, see Github Repository for Python and C++ Libraries 


See Also:

Connecting with PUTTY

Automation Commands

Common Device Automation Commands

Pattern Generator Automation Commands

DDR5 Stress Accessory Automation Commands

Github Repository for Python and C++ Libraries