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.
The Progress Event mechanism has two known limitations.
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.
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:
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) |
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(); } }