/**
   * Wait for value of device to reach the desired value (within tolerance)
   *
   * @throws TimeoutException on timeout
   * @throws Exception on interruption or device read error
   */
  @Override
  public void await() throws TimeoutException, Exception {
    final WaitWithTimeout timeout = new WaitWithTimeout(this.timeout);

    // Fetch initial value with get-callback
    initial_value = VTypeHelper.toDouble(device.read(value_check_timeout));

    device.addListener(this);
    try {
      // Synchronize to avoid the following situation:
      // 1. not at desired value
      // 2. device changes and we would be notified
      // 3. ... but that's before we call wait, so we wait forever
      synchronized (this) {
        is_condition_met = isConditionMet();
        while (!is_condition_met) { // Wait for update from device
          if (timeout.waitUntilTimeout(this))
            throw new TimeoutException(
                "Timeout while waiting for " + device + " " + comparison + " " + desired_value);
          if (error != null) throw error;
        }
      }
    } finally {
      device.removeListener(this);
    }
  }
  /**
   * Execute one step of the loop
   *
   * @param context
   * @param device
   * @param condition
   * @param readback
   * @param value
   * @throws Exception
   */
  private void executeStep(
      final ScanContext context,
      final Device device,
      final NumericValueCondition condition,
      final Device readback,
      double value)
      throws Exception {
    logger.log(
        Level.INFO,
        "Loop setting {0} = {1}{2}",
        new Object[] {device.getAlias(), value, (condition != null ? " (waiting)" : "")});

    // Set device to value for current step of loop
    do_skip = false;
    synchronized (this) {
      thread = Thread.currentThread();
    }
    try {
      if (command.getCompletion())
        device.write(value, TimeDuration.ofSeconds(command.getTimeout()));
      else device.write(value);

      // .. wait for device to reach value
      if (condition != null) {
        condition.setDesiredValue(value);
        condition.await();
      }

      // Log the device's value?
      if (context.isAutomaticLogMode()) {
        final DataLog log = context.getDataLog().get();
        final long serial = log.getNextScanDataSerial();
        log.log(readback.getAlias(), VTypeHelper.createSample(serial, readback.read()));
      }
    } catch (InterruptedException ex) { // Ignore if 'next' was requested
      if (!do_skip) throw ex;
    } finally {
      synchronized (this) {
        thread = null;
      }
    }

    // Execute loop body or show some estimate of progress
    // (not including nested commands)
    if (do_skip) context.workPerformed(implementation.size());
    else context.execute(implementation);

    // If there are no commands that inc. the work units, do it yourself
    if (implementation.size() <= 0) context.workPerformed(1);
  }
 /** Trigger another check of device's value {@inheritDoc} */
 @Override
 public void deviceChanged(final Device device) {
   synchronized (this) {
     try {
       if (Double.isNaN(initial_value)) initial_value = VTypeHelper.toDouble(device.read());
       is_condition_met = isConditionMet();
     } catch (Exception ex) {
       is_condition_met = false;
       error = ex;
     }
     // Notify await() so it can check again.
     notifyAll();
   }
 }
  /**
   * Simulate one step in the loop iteration
   *
   * @param context {@link SimulationContext}
   * @param device {@link SimulatedDevice} that the loop modifies
   * @param value Value of the loop variable for this iteration
   * @throws Exception on error
   */
  private void simulateStep(
      final SimulationContext context, final SimulatedDevice device, final double value)
      throws Exception {
    // Get previous value
    final double original = VTypeHelper.toDouble(device.read());

    // Estimate execution time
    final double time_estimate = command.getWait() ? device.getChangeTimeEstimate(value) : 0.0;

    // Show command
    final StringBuilder buf = new StringBuilder();
    buf.append("Loop '").append(command.getDeviceName()).append("' = ").append(value);
    command.appendConditionDetail(buf);
    if (!Double.isNaN(original)) buf.append(" [was ").append(original).append("]");
    context.logExecutionStep(context.getMacros().resolveMacros(buf.toString()), time_estimate);

    // Set to (simulated) new value
    device.write(value);

    // Simulate loop body
    context.simulate(implementation);
  }
 /**
  * Determine if the condition is currently met
  *
  * @return <code>true</code> if condition is met
  * @throws Exception on error reading from the device
  */
 public boolean isConditionMet() throws Exception {
   final double value = VTypeHelper.toDouble(device.read());
   // Note that these need to fail "safe" if any of the values are NaN
   switch (comparison) {
     case EQUALS:
       return Math.abs(desired_value - value) <= tolerance;
     case AT_LEAST:
       return value >= desired_value;
     case ABOVE:
       return value > desired_value;
     case AT_MOST:
       return value <= desired_value;
     case BELOW:
       return value < desired_value;
     case INCREASE_BY:
       return value >= initial_value + desired_value;
     case DECREASE_BY:
       return value <= initial_value - desired_value;
     default:
       throw new Error("Condition not implemented: " + comparison);
   }
 }