Click or drag to resize

12.6.2 Care and Feeding of your Progress Events

Progress Events provide a reporting mechanism by which client applications can track the progress of CommandList execution on the SP-ICE-3 Card.

An application can call CommandListAppendProgress during construction of a CommandList to place PROGRESS_ID commands at arbitrary points within the list which happen to be of interest to that application.

When the SP-ICE-3 Card encounters a PROGRESS_ID command during list execution, it generates a Progress Event which, subject to certain limitations, is then propagated back to the client application, carrying with it the ID passed as the command's parameter.

An example of how a client application can receive Progress Events by making use of ListAPIWaitForProgress, is shown below.

Progress Event Husbandry - Hints for the Handyman

The Progress Event mechanism has two known limitations.

The reporting rate of successive Progress Events is limited.

The SP-ICE-3 Card limits the rate at which successive Progress Events can be generated to a maximum of one every 500ms.

  • This is by design in order to avoid the potentially excessive overhead of generating large numbers of Progress Events in quick succession, since that would negatively impact the SP-ICE-3 Card's vector processing performance, and thus possibly lead to FifoUnderflow errors: see 12.7 Exceptions and Errors.

  • The ID parameters from PROGRESS_ID commands which are executed within 500ms of the preceding Progress Event are simply discarded.

  • This means that there can be no guarantee that any given PROGRESS_ID command will cause a Progress Event which is reported to the client application.

WAIT_START delays are not accounted for during Progress Event generation.

The SP-ICE-3 Card evaluates the elapsed time between potentially successive Progress Events while it is parsing the command list.

  • This means that it does not take account of the time which elapses while, for instance, a WAIT_START command is being executed, i.e. while waiting for the hardware signal.
    Instead the card simply assumes that WAIT_START will execute in 0ms.

  • Consider a CommandList constructed as follows:

    Waits between Progress Events in List.
    list.AppendWaitStart();
    list.AppendMarkAbs( X, Y );
    list.AppendProgress( 1 );
    
    list.AppendWaitStart();
    list.AppendMarkAbs( X, Y );
    list.AppendProgress( 2 );
    
    list.AppendWaitStart();
    list.AppendMarkAbs( X, Y );
    list.AppendProgress( 3 );
    
    list.AppendWaitStart();
    list.AppendMarkAbs( X, Y );
    list.AppendProgress( 4 );
  • Further assume that each MARK_ABS takes 200ms to execute.

  • Under these conditions, the card will report only the Progress Events with ID numbers 1 and 4.

    As mentioned above, the SP-ICE-3 Card effectively ignores the WAIT_START commands:

    Progress ID

    Action

    Explanation

    1

    reported

    happens to be the first progress command in the CommandList

    2

    skipped

    because the calculated elapsed time from #1 is only 200ms (i.e. < 500ms)

    3

    skipped

    because the calculated elapsed time from #1 is only 400ms (i.e. < 500ms)

    4

    reported

    because the calculated elapsed time from #1 is 600ms (i.e. > 500ms)

Planting and Harvesting your Progress Events, by example.
Progress Events example program.
public class ProgressEventCallbackExample
{
    // insert the IP address or Hostname of your SP-ICE-3 card here.
    private string _cardIP = "169.254.x.y";

    // Use listIDs 0..9 for marking squares.
    private const int _maxSquareListID = 9;

    // Just some random listID which is not in the above range.
    private const int _gotoCenterOfFieldListID = 99;

    // An initial "invalid" value, and not equal to any of the other listIDs used in this example.
    private int _lastDoneListID = -1;

    // The main thread waits on this event, which is signaled by the ListDoneCallback.
    private readonly AutoResetEvent _listDoneEvent = new AutoResetEvent( false );

    // ProgressCallback records the latest progress ID here.
    private int _lastProgressID = 0;

    // The ProgressUpdater waits on this event, which is signaled by the ProgressCallback.
    private readonly AutoResetEvent _progressEvent = new AutoResetEvent( false );


    private void ListDoneCallback( ClientAPI client, int listID )
    {
        // Record the listID locally in a thread-safe manner.
        Interlocked.Exchange( ref _lastDoneListID, listID );

        // Tell the main thread that we've been called!
        _listDoneEvent.Set();
    }

    private void ProgressCallback( ClientAPI client, int progressID )
    {
        // Record the progress ID locally in a thread-safe manner.
        Interlocked.Exchange( ref _lastProgressID, progressID );

        // Tell the Progress Updater thread that we've got a new progress ID for it to announce!
        _progressEvent.Set();
    }

    private void ProgressUpdater(CancellationToken cancellationToken) {
        while ( !cancellationToken.IsCancellationRequested )
        {
            if ( WaitHandle.WaitAny( new WaitHandle[] { _progressEvent, cancellationToken.WaitHandle } ) == 0 )
            {
                // For this demo, the ProgressUpdater simply announces the latest progress ID on the Console.
                Console.WriteLine( $"Last Progress ID received: {_lastProgressID}" );
            }
        }
    }

    public void Run()
    {
        // The ProgressUpdater runs on a separate thread.
        var cancellationTokenSource = new CancellationTokenSource();
        var cancellationToken = cancellationTokenSource.Token;

        // Start up our ProgressUpdater which, for this demo, simply announces incoming Progress IDs.
        Task ProgressUpdaterTask = Task.Run(() => ProgressUpdater(cancellationToken), cancellationToken );

        using ( ClientAPI client = new ClientAPI( _cardIP ) )
        {
            try
            {
                client.System.ResetToDefaults();

                client.List.RegisterListDoneCallback( ListDoneCallback );

                client.List.RegisterProgressCallback( ProgressCallback );

                // For the purposes of this sample, make sure the scanners are at the origin so that
                // the expected execution time can be calculated correctly for ALL of the squares, including the very first.
                GotoCenterOfField( client, _gotoCenterOfFieldListID );


                for ( int listID = 0; listID <= _maxSquareListID; listID++ )
                {
                    // Progress ID can have any value in the range 0..(1 << 24)
                    // Other than that, the value can be completely arbitrary: whatever best suits your application.
                    int progressID = (listID * 100) + 69;

                    // Construct each list with a different size so that they will have differing execution times, and put a ProgressID into each list.
                    CommandList list = BuildSquare( out int expectedListExecutionTimeMs, 10000 + ( listID * 1000 ), progressID );

                    // Send the list to the card, and execute it.
                    MarkSquare( client, list, listID );

                    // Now wait for the card to tell us that it has finished executing the list.
                    // By the time we get here, the list has probably ALREADY started to execute, so we will
                    // not usually have to wait for the whole expectedListExecutionTime before the listDone event arrives.
                    // However, we also need to take the unpredictability of the network connection into account.
                    // Therefore we add a couple of seconds to the expectedListExecutionTime value, before using it as a timeout.
                    int waitForListDoneEventTimeoutMs = expectedListExecutionTimeMs + 2000;

                    Console.WriteLine( $"Waiting up to {waitForListDoneEventTimeoutMs} ms for list[{listID}] done." );

                    if ( !_listDoneEvent.WaitOne( waitForListDoneEventTimeoutMs ) )
                    {
                        throw new Exception( $"listDone[{listID}] not received within {waitForListDoneEventTimeoutMs} ms." );
                    }

                    // Retrieve the lastDoneListID value in a thread-safe manner, and
                    // make sure it's the one we were expecting.
                    if ( listID != Interlocked.CompareExchange( ref _lastDoneListID, listID, listID ) )
                    {
                        throw new Exception( $"listDone[{listID}] not received: got listID {_lastDoneListID} instead." );
                    }

                    // We did not wait in vain...
                    Console.WriteLine( $"List[{listID}] done. Execution stats: {client.List.GetLastExecutionStats()}." );
                    Console.WriteLine();
                }
            }
            finally
            {
                cancellationTokenSource.Cancel(); // Stop our ProgressUpdater Task
                ProgressUpdaterTask.Wait( 5000 ); // Shouldn't take anywhere near that long, of course!

                client.List.UnregisterProgressCallback( ProgressCallback );

                client.List.UnregisterListDoneCallback( ListDoneCallback );

            }
        }

        Console.WriteLine( "Press any key to quit." );
        Console.ReadKey();
    }

    private void MarkSquare( ClientAPI client, CommandList list, int listID )
    {
        Console.WriteLine( $"MarkSquare: listID {listID}" );

        client.List.Set( listID, list ); // Transfer the list to the card.

        client.List.Execute( listID ); // Tell the card to begin executing the list.
    }

    private CommandList BuildSquare( out int expectedExecutionTimeMs, double size, int progressID )
    {
        double jumpSpeed = 5;
        double markSpeed = 0.2;

        CommandList list = new CommandList();
        list.AppendJumpSpeed( jumpSpeed );
        list.AppendJumpDelay( 0 );
        list.AppendMarkSpeed( markSpeed );
        list.AppendMarkDelay( 0 );
        list.AppendPolyDelay( 0 );
        list.AppendLmFrequency( 1.0 / 50 );
        list.AppendLmWidth( 35 );

        double halfsize = size / 2.0;

        list.AppendJumpAbs( -halfsize, -halfsize );
        list.AppendMarkAbs( halfsize, -halfsize );
        list.AppendMarkAbs( halfsize, halfsize );
        list.AppendMarkAbs( -halfsize, halfsize );
        list.AppendMarkAbs( -halfsize, -halfsize );

        list.AppendProgress( progressID ); // here's the magic

        list.AppendJumpAbs( Point2D.Zero );

        // Work out approximately how long we expect execution of this list to take.
        double expectedExecutionTimeUs = 2.0 * Math.Sqrt( 2 ) * halfsize / jumpSpeed;
        expectedExecutionTimeUs += 4.0 * size / markSpeed;
        expectedExecutionTimeMs = (int)( 1e-3 * expectedExecutionTimeUs );

        return list;
    }

    private void GotoCenterOfField( ClientAPI client, int listID )
    {
        CommandList list = new CommandList();
        list.AppendJumpSpeed( 5 );
        list.AppendJumpDelay( 0 );
        list.AppendJumpAbs( Point2D.Zero );

        Console.WriteLine( $"GotoCenterOfField: {listID}" );

        client.List.Set( listID, list );
        client.List.Execute( listID );

        // Since we don't know where the scanners are coming from,
        // we simply assume that they will not take more than 10s to reach the field origin.
        if ( !_listDoneEvent.WaitOne( TimeSpan.FromSeconds( 10 ) ) )
            throw new Exception( $"listDone[{listID}] not received within 10s." );
        Console.WriteLine( $"List[{listID}] done. Execution stats: {client.List.GetLastExecutionStats()}." );
        Console.WriteLine();
    }

    private static void Main( string[] args )
    {
        ProgressEventCallbackExample p = new ProgressEventCallbackExample();
        p.Run();
    }
}