/**
 * The Dialog Class
 *
 * @author Frank Kunz The dialog class draws the basic dialog with a grid layout. The dialog
 *     consists of three main parts. A settings panel, a table panel and three buttons.
 */
public final class UARTProtocolAnalysisDialog extends BaseToolDialog<UARTDataSet>
    implements ExportAware<UARTDataSet> {
  // INNER TYPES

  /** Provides a combobox renderer for {@link UARTParity} values. */
  static final class UARTParityItemRenderer extends EnumItemRenderer<Parity> {
    // CONSTANTS

    private static final long serialVersionUID = 1L;

    // METHODS

    /**
     * @see
     *     nl.lxtreme.ols.client.diagram.settings.GeneralSettingsDialog.EnumItemRenderer#getDisplayValue(java.lang.Enum)
     */
    @Override
    protected String getDisplayValue(final Parity aValue) {
      String text = super.getDisplayValue(aValue);
      if (Parity.EVEN.equals(aValue)) {
        text = "Even parity";
      } else if (Parity.NONE.equals(aValue)) {
        text = "No parity";
      } else if (Parity.ODD.equals(aValue)) {
        text = "Odd parity";
      }
      return text;
    }
  }

  /** Provides a combobox renderer for {@link UARTStopBits} values. */
  static final class UARTStopBitsItemRenderer extends EnumItemRenderer<StopBits> {
    // CONSTANTS

    private static final long serialVersionUID = 1L;

    // METHODS

    /**
     * @see
     *     nl.lxtreme.ols.client.diagram.settings.GeneralSettingsDialog.EnumItemRenderer#getDisplayValue(java.lang.Enum)
     */
    @Override
    protected String getDisplayValue(final StopBits aValue) {
      String text = super.getDisplayValue(aValue);
      if (StopBits.ONE.equals(aValue)) {
        text = "1";
      } else if (StopBits.ONE_HALF.equals(aValue)) {
        text = "1.5";
      } else if (StopBits.TWO.equals(aValue)) {
        text = "2";
      }
      return text;
    }
  }

  /** Provides a combobox renderer for {@link BitOrder} values. */
  static final class UARTBitOrderItemRenderer extends EnumItemRenderer<BitOrder> {
    // CONSTANTS

    private static final long serialVersionUID = 1L;

    // METHODS

    /**
     * @see
     *     nl.lxtreme.ols.client.diagram.settings.GeneralSettingsDialog.EnumItemRenderer#getDisplayValue(java.lang.Enum)
     */
    @Override
    protected String getDisplayValue(final BitOrder aValue) {
      String text = super.getDisplayValue(aValue);
      if (BitOrder.LSB_FIRST.equals(aValue)) {
        text = "LSB first";
      } else if (BitOrder.MSB_FIRST.equals(aValue)) {
        text = "MSB first";
      }
      return text;
    }
  }

  /** Provides a combobox renderer for {@link BitEncoding} values. */
  static final class UARTBitEncodingItemRenderer extends EnumItemRenderer<BitEncoding> {
    // CONSTANTS

    private static final long serialVersionUID = 1L;

    // METHODS

    /**
     * @see
     *     nl.lxtreme.ols.client.diagram.settings.GeneralSettingsDialog.EnumItemRenderer#getDisplayValue(java.lang.Enum)
     */
    @Override
    protected String getDisplayValue(final BitEncoding aValue) {
      String text = super.getDisplayValue(aValue);
      if (BitEncoding.HIGH_IS_MARK.equals(aValue)) {
        text = "High is mark (1)";
      } else if (BitEncoding.HIGH_IS_SPACE.equals(aValue)) {
        text = "High is space (0)";
      }
      return text;
    }
  }

  /** Provides a combobox renderer for {@link BitLevel} values. */
  static final class UARTIdleLevelItemRenderer extends EnumItemRenderer<BitLevel> {
    // CONSTANTS

    private static final long serialVersionUID = 1L;

    // METHODS

    /**
     * @see
     *     nl.lxtreme.ols.client.diagram.settings.GeneralSettingsDialog.EnumItemRenderer#getDisplayValue(java.lang.Enum)
     */
    @Override
    protected String getDisplayValue(final BitLevel aValue) {
      String text = super.getDisplayValue(aValue);
      if (BitLevel.HIGH.equals(aValue)) {
        text = "High (start = L, stop = H)";
      } else if (BitLevel.LOW.equals(aValue)) {
        text = "Low (start = H, stop = L)";
      }
      return text;
    }
  }

  // CONSTANTS

  private static final long serialVersionUID = 1L;

  private static final Logger LOG = Logger.getLogger(UARTProtocolAnalysisDialog.class.getName());

  // VARIABLES

  private JComboBox rxd;
  private JComboBox txd;
  private JComboBox cts;
  private JComboBox rts;
  private JComboBox dtr;
  private JComboBox dsr;
  private JComboBox dcd;
  private JComboBox ri;
  private JComboBox parity;
  private JComboBox bits;
  private JComboBox stop;
  private JComboBox bitEncoding;
  private JComboBox bitOrder;
  private JComboBox idleLevel;
  private JCheckBox autoDetectBaudRate;
  private JComboBox baudrate;
  private JEditorPane outText;

  private RestorableAction runAnalysisAction;
  private Action closeAction;
  private Action exportAction;

  // CONSTRUCTORS

  /**
   * Creates a new UARTProtocolAnalysisDialog instance.
   *
   * @param aOwner the owner of this dialog;
   * @param aToolContext the tool context;
   * @param aContext the OSGi bundle context to use;
   * @param aTool the {@link UARTAnalyser} tool.
   */
  public UARTProtocolAnalysisDialog(
      final Window aOwner,
      final ToolContext aToolContext,
      final BundleContext aContext,
      final UARTAnalyser aTool) {
    super(aOwner, aToolContext, aContext, aTool);

    initDialog();

    setLocationRelativeTo(getOwner());
  }

  // METHODS

  /** {@inheritDoc} */
  @Override
  public void exportToFile(
      final File aOutputFile, final nl.lxtreme.ols.tool.base.ExportAware.ExportFormat aFormat)
      throws IOException {
    if (ExportFormat.HTML.equals(aFormat)) {
      storeToHtmlFile(aOutputFile, getLastResult());
    } else if (ExportFormat.CSV.equals(aFormat)) {
      storeToCsvFile(aOutputFile, getLastResult());
    }
  }

  /** {@inheritDoc} */
  @Override
  public void readPreferences(final UserSettings aSettings) {
    // Issue #114: avoid setting illegal values...
    setComboBoxIndex(this.rxd, aSettings, "rxd");
    setComboBoxIndex(this.txd, aSettings, "txd");
    setComboBoxIndex(this.cts, aSettings, "cts");
    setComboBoxIndex(this.rts, aSettings, "rts");
    setComboBoxIndex(this.dtr, aSettings, "dtr");
    setComboBoxIndex(this.dsr, aSettings, "dsr");
    setComboBoxIndex(this.dcd, aSettings, "dcd");
    setComboBoxIndex(this.ri, aSettings, "ri");

    this.parity.setSelectedIndex(aSettings.getInt("parity", this.parity.getSelectedIndex()));
    this.bits.setSelectedIndex(aSettings.getInt("bits", this.bits.getSelectedIndex()));
    this.stop.setSelectedIndex(aSettings.getInt("stop", this.stop.getSelectedIndex()));
    this.idleLevel.setSelectedIndex(
        aSettings.getInt("idle-state", this.idleLevel.getSelectedIndex()));
    this.bitEncoding.setSelectedIndex(
        aSettings.getInt("bit-encoding", this.bitEncoding.getSelectedIndex()));
    this.bitOrder.setSelectedIndex(aSettings.getInt("bit-order", this.bitOrder.getSelectedIndex()));
    this.baudrate.setSelectedItem(Integer.valueOf(aSettings.getInt("baudrate", 9600)));
    this.autoDetectBaudRate.setSelected(
        aSettings.getBoolean("auto-baudrate", this.autoDetectBaudRate.isSelected()));
  }

  /** {@inheritDoc} */
  @Override
  public void reset() {
    this.outText.setText(getEmptyHtmlPage());
    this.outText.setEditable(false);

    this.runAnalysisAction.restore();

    setControlsEnabled(true);

    this.exportAction.setEnabled(false);
  }

  /** @see nl.lxtreme.ols.api.Configurable#writePreferences(nl.lxtreme.ols.api.UserSettings) */
  @Override
  public void writePreferences(final UserSettings aSettings) {
    aSettings.putInt("rxd", this.rxd.getSelectedIndex());
    aSettings.putInt("txd", this.txd.getSelectedIndex());
    aSettings.putInt("cts", this.cts.getSelectedIndex());
    aSettings.putInt("rts", this.rts.getSelectedIndex());
    aSettings.putInt("dtr", this.dtr.getSelectedIndex());
    aSettings.putInt("dsr", this.dsr.getSelectedIndex());
    aSettings.putInt("dcd", this.dcd.getSelectedIndex());
    aSettings.putInt("ri", this.ri.getSelectedIndex());
    aSettings.putInt("parity", this.parity.getSelectedIndex());
    aSettings.putInt("bits", this.bits.getSelectedIndex());
    aSettings.putInt("stop", this.stop.getSelectedIndex());
    aSettings.putInt("idle-state", this.idleLevel.getSelectedIndex());
    aSettings.putInt("bit-encoding", this.bitEncoding.getSelectedIndex());
    aSettings.putInt("bit-order", this.bitOrder.getSelectedIndex());
    aSettings.putInt("baudrate", ((Integer) this.baudrate.getSelectedItem()).intValue());
    aSettings.putBoolean("auto-baudrate", this.autoDetectBaudRate.isSelected());
  }

  /** {@inheritDoc} */
  @Override
  protected void onToolEnded(final UARTDataSet aAnalysisResult) {
    try {
      final String htmlPage;
      if (aAnalysisResult != null) {
        htmlPage = toHtmlPage(null /* aFile */, aAnalysisResult);
      } else {
        htmlPage = getEmptyHtmlPage();
      }

      this.outText.setText(htmlPage);
      this.outText.setEditable(false);

      this.runAnalysisAction.restore();
    } catch (final IOException exception) {
      // Make sure to handle IO-interrupted exceptions properly!
      if (!HostUtils.handleInterruptedException(exception)) {
        // Should not happen in this situation!
        throw new RuntimeException(exception);
      }
    }
  }

  /** {@inheritDoc} */
  @Override
  protected void onToolStarted() {
    // NO-op
  }

  /** {@inheritDoc} */
  @Override
  protected void prepareToolTask(final ToolTask<UARTDataSet> aToolTask) {
    final UARTAnalyserTask toolTask = (UARTAnalyserTask) aToolTask;

    // The value at index zero is "Unused", so extracting one of all items
    // causes all "unused" values to be equivalent to -1, which is interpreted
    // as not used...
    toolTask.setRxdIndex(this.rxd.getSelectedIndex() - 1);
    toolTask.setTxdIndex(this.txd.getSelectedIndex() - 1);
    toolTask.setCtsIndex(this.cts.getSelectedIndex() - 1);
    toolTask.setRtsIndex(this.rts.getSelectedIndex() - 1);
    toolTask.setDcdIndex(this.dcd.getSelectedIndex() - 1);
    toolTask.setRiIndex(this.ri.getSelectedIndex() - 1);
    toolTask.setDsrIndex(this.dsr.getSelectedIndex() - 1);
    toolTask.setDtrIndex(this.dtr.getSelectedIndex() - 1);
    // Handle the auto detect option for baudrates...
    if (this.autoDetectBaudRate.isSelected()) {
      toolTask.setBaudRate(UARTAnalyserTask.AUTO_DETECT_BAUDRATE);
    } else {
      toolTask.setBaudRate(((Integer) this.baudrate.getSelectedItem()).intValue());
    }

    // Other properties...
    toolTask.setIdleLevel((BitLevel) this.idleLevel.getSelectedItem());
    toolTask.setBitEncoding((BitEncoding) this.bitEncoding.getSelectedItem());
    toolTask.setBitOrder((BitOrder) this.bitOrder.getSelectedItem());
    toolTask.setParity((Parity) this.parity.getSelectedItem());
    toolTask.setStopBits((StopBits) this.stop.getSelectedItem());
    toolTask.setBitCount(NumberUtils.smartParseInt((String) this.bits.getSelectedItem(), 8));
  }

  /**
   * set the controls of the dialog enabled/disabled
   *
   * @param aEnable status of the controls
   */
  @Override
  protected void setControlsEnabled(final boolean aEnable) {
    this.rxd.setEnabled(aEnable);
    this.txd.setEnabled(aEnable);
    this.cts.setEnabled(aEnable);
    this.rts.setEnabled(aEnable);
    this.dtr.setEnabled(aEnable);
    this.dsr.setEnabled(aEnable);
    this.dcd.setEnabled(aEnable);
    this.ri.setEnabled(aEnable);
    this.parity.setEnabled(aEnable);
    this.bits.setEnabled(aEnable);
    this.stop.setEnabled(aEnable);
    this.idleLevel.setEnabled(aEnable);
    this.bitEncoding.setEnabled(aEnable);
    this.bitOrder.setEnabled(aEnable);

    this.closeAction.setEnabled(aEnable);
    this.exportAction.setEnabled(aEnable);
  }

  /**
   * Creates the HTML template for exports to HTML.
   *
   * @param aExporter the HTML exporter instance to use, cannot be <code>null</code>.
   * @return a HTML exporter filled with the template, never <code>null</code>.
   */
  private HtmlExporter createHtmlTemplate(final HtmlExporter aExporter) {
    aExporter.addCssStyle("body { font-family: sans-serif; } ");
    aExporter.addCssStyle(
        "table { border-width: 1px; border-spacing: 0px; border-color: gray;"
            + " border-collapse: collapse; border-style: solid; margin-bottom: 15px; } ");
    aExporter.addCssStyle(
        "table th { border-width: 1px; padding: 2px; border-style: solid; border-color: gray;"
            + " background-color: #C0C0FF; text-align: left; font-weight: bold; font-family: sans-serif; } ");
    aExporter.addCssStyle(
        "table td { border-width: 1px; padding: 2px; border-style: solid; border-color: gray;"
            + " font-family: monospace; } ");
    aExporter.addCssStyle(".error { color: red; } ");
    aExporter.addCssStyle(".warning { color: orange; } ");
    aExporter.addCssStyle(".date { text-align: right; font-size: x-small; margin-bottom: 15px; } ");
    aExporter.addCssStyle(".w100 { width: 100%; } ");
    aExporter.addCssStyle(".w35 { width: 35%; } ");
    aExporter.addCssStyle(".w30 { width: 30%; } ");
    aExporter.addCssStyle(".w15 { width: 15%; } ");
    aExporter.addCssStyle(".w10 { width: 10%; } ");
    aExporter.addCssStyle(".w8 { width: 8%; } ");
    aExporter.addCssStyle(".w7 { width: 7%; } ");

    final Element body = aExporter.getBody();
    body.addChild(H1).addContent("UART Analysis results");
    body.addChild(HR);
    body.addChild(DIV).addAttribute("class", "date").addContent("Generated: ", "{date-now}");

    Element table, tr, thead, tbody;

    table = body.addChild(TABLE).addAttribute("class", "w100");

    tbody = table.addChild(TBODY);
    tr = tbody.addChild(TR);
    tr.addChild(TH).addAttribute("colspan", "2").addContent("Statistics");
    tr = tbody.addChild(TR);
    tr.addChild(TD).addAttribute("class", "w30").addContent("Decoded bytes");
    tr.addChild(TD).addContent("{decoded-bytes}");
    tr = tbody.addChild(TR);
    tr.addChild(TD).addAttribute("class", "w30").addContent("Detected bus errors");
    tr.addChild(TD).addContent("{detected-bus-errors}");
    tr = tbody.addChild(TR);
    tr.addChild(TD).addAttribute("class", "w30").addContent("Baudrate");
    tr.addChild(TD).addContent("{baudrate}");

    table = body.addChild(TABLE).addAttribute("class", "w100");
    thead = table.addChild(THEAD);
    tr = thead.addChild(TR);
    tr.addChild(TH).addAttribute("class", "w30").addAttribute("colspan", "2");
    tr.addChild(TH).addAttribute("class", "w35").addAttribute("colspan", "4").addContent("RxD");
    tr.addChild(TH).addAttribute("class", "w35").addAttribute("colspan", "4").addContent("TxD");
    tr = thead.addChild(TR);
    tr.addChild(TH).addAttribute("class", "w15").addContent("Index");
    tr.addChild(TH).addAttribute("class", "w15").addContent("Time");
    tr.addChild(TH).addAttribute("class", "w10").addContent("Hex");
    tr.addChild(TH).addAttribute("class", "w10").addContent("Bin");
    tr.addChild(TH).addAttribute("class", "w8").addContent("Dec");
    tr.addChild(TH).addAttribute("class", "w7").addContent("ASCII");
    tr.addChild(TH).addAttribute("class", "w10").addContent("Hex");
    tr.addChild(TH).addAttribute("class", "w10").addContent("Bin");
    tr.addChild(TH).addAttribute("class", "w8").addContent("Dec");
    tr.addChild(TH).addAttribute("class", "w7").addContent("ASCII");
    tbody = table.addChild(TBODY);
    tbody.addContent("{decoded-data}");

    return aExporter;
  }

  /** @return */
  private JPanel createPreviewPane() {
    final JPanel panTable = new JPanel(new GridLayout(1, 1, 0, 0));

    this.outText = new JEditorPane("text/html", getEmptyHtmlPage());
    this.outText.setEditable(false);

    panTable.add(new JScrollPane(this.outText));

    return panTable;
  }

  /** @return */
  private JPanel createSettingsPane() {
    final int channelCount = getData().getChannels();

    final Integer[] baudrates = new Integer[AsyncSerialDataDecoder.COMMON_BAUDRATES.length];
    for (int i = 0; i < baudrates.length; i++) {
      baudrates[i] = Integer.valueOf(AsyncSerialDataDecoder.COMMON_BAUDRATES[i]);
    }
    final String[] bitarray = new String[10];
    // allow symbol lengths between 5 and 14 bits...
    for (int i = 0; i < bitarray.length; i++) {
      bitarray[i] = String.format("%d", Integer.valueOf(i + 5));
    }

    final JPanel settings = new JPanel(new SpringLayout());

    SpringLayoutUtils.addSeparator(settings, "Settings");

    settings.add(createRightAlignedLabel("RxD"));
    this.rxd = SwingComponentUtils.createOptionalChannelSelector(channelCount);
    settings.add(this.rxd);

    settings.add(createRightAlignedLabel("TxD"));
    this.txd = SwingComponentUtils.createOptionalChannelSelector(channelCount);
    settings.add(this.txd);

    settings.add(createRightAlignedLabel("CTS"));
    this.cts = SwingComponentUtils.createOptionalChannelSelector(channelCount);
    settings.add(this.cts);

    settings.add(createRightAlignedLabel("RTS"));
    this.rts = SwingComponentUtils.createOptionalChannelSelector(channelCount);
    settings.add(this.rts);

    settings.add(createRightAlignedLabel("DTR"));
    this.dtr = SwingComponentUtils.createOptionalChannelSelector(channelCount);
    settings.add(this.dtr);

    settings.add(createRightAlignedLabel("DSR"));
    this.dsr = SwingComponentUtils.createOptionalChannelSelector(channelCount);
    settings.add(this.dsr);

    settings.add(createRightAlignedLabel("DCD"));
    this.dcd = SwingComponentUtils.createOptionalChannelSelector(channelCount);
    settings.add(this.dcd);

    settings.add(createRightAlignedLabel("RI"));
    this.ri = SwingComponentUtils.createOptionalChannelSelector(channelCount);
    settings.add(this.ri);

    settings.add(createRightAlignedLabel("Baudrate"));
    this.autoDetectBaudRate = new JCheckBox("Auto detect");
    settings.add(this.autoDetectBaudRate);

    settings.add(new JLabel(""));
    this.baudrate = new JComboBox(baudrates);
    // Issue #90: allow custom baudrates to be specified...
    this.baudrate.setEditable(true);
    this.baudrate.setSelectedIndex(0);
    settings.add(this.baudrate);

    this.autoDetectBaudRate.addItemListener(
        new ItemListener() {
          @Override
          public void itemStateChanged(final ItemEvent aEvent) {
            final JCheckBox cb = (JCheckBox) aEvent.getSource();
            UARTProtocolAnalysisDialog.this.baudrate.setEnabled(!cb.isSelected());
          }
        });

    settings.add(createRightAlignedLabel("Parity"));
    this.parity = new JComboBox(Parity.values());
    this.parity.setSelectedIndex(0);
    this.parity.setRenderer(new UARTParityItemRenderer());
    settings.add(this.parity);

    settings.add(createRightAlignedLabel("Bits"));
    this.bits = new JComboBox(bitarray);
    this.bits.setSelectedIndex(3);
    settings.add(this.bits);

    settings.add(createRightAlignedLabel("Stopbits"));
    this.stop = new JComboBox(StopBits.values());
    this.stop.setSelectedIndex(0);
    this.stop.setRenderer(new UARTStopBitsItemRenderer());
    settings.add(this.stop);

    settings.add(createRightAlignedLabel("Idle level"));
    this.idleLevel = new JComboBox(BitLevel.values());
    this.idleLevel.setSelectedIndex(0);
    this.idleLevel.setRenderer(new UARTIdleLevelItemRenderer());
    settings.add(this.idleLevel);

    settings.add(createRightAlignedLabel("Bit encoding"));
    this.bitEncoding = new JComboBox(BitEncoding.values());
    this.bitEncoding.setSelectedIndex(0);
    this.bitEncoding.setRenderer(new UARTBitEncodingItemRenderer());
    settings.add(this.bitEncoding);

    settings.add(createRightAlignedLabel("Bit order"));
    this.bitOrder = new JComboBox(BitOrder.values());
    this.bitOrder.setSelectedIndex(0);
    this.bitOrder.setRenderer(new UARTBitOrderItemRenderer());
    settings.add(this.bitOrder);

    SpringLayoutUtils.makeEditorGrid(settings, 10, 4);

    return settings;
  }

  /**
   * generate a HTML page
   *
   * @param empty if this is true an empty output is generated
   * @return String with HTML data
   */
  private String getEmptyHtmlPage() {
    final HtmlExporter exporter = createHtmlTemplate(ExportUtils.createHtmlExporter());
    return exporter.toString(
        new MacroResolver() {
          @Override
          public Object resolve(final String aMacro, final Element aParent) {
            if ("date-now".equals(aMacro)) {
              final DateFormat df = DateFormat.getDateInstance(DateFormat.LONG);
              return df.format(new Date());
            } else if ("decoded-bytes".equals(aMacro)
                || "detected-bus-errors".equals(aMacro)
                || "baudrate".equals(aMacro)) {
              return "-";
            } else if ("decoded-data".equals(aMacro)) {
              return null;
            }
            return null;
          }
        });
  }

  /** Initializes this dialog. */
  private void initDialog() {
    setMinimumSize(new Dimension(640, 480));

    final JComponent settingsPane = createSettingsPane();
    final JComponent previewPane = createPreviewPane();

    final JPanel contentPane = new JPanel(new GridBagLayout());
    contentPane.add(
        settingsPane,
        new GridBagConstraints(
            0,
            0,
            1,
            1,
            0.0,
            0.0,
            GridBagConstraints.NORTH,
            GridBagConstraints.NONE,
            new Insets(2, 0, 2, 0),
            0,
            0));
    contentPane.add(
        previewPane,
        new GridBagConstraints(
            1,
            0,
            1,
            1,
            1.0,
            1.0,
            GridBagConstraints.NORTH,
            GridBagConstraints.BOTH,
            new Insets(2, 0, 2, 0),
            0,
            0));

    final JButton runAnalysisButton = ToolUtils.createRunAnalysisButton(this);
    this.runAnalysisAction = (RestorableAction) runAnalysisButton.getAction();

    final JButton exportButton = ToolUtils.createExportButton(this);
    this.exportAction = exportButton.getAction();
    this.exportAction.setEnabled(false);

    final JButton closeButton = ToolUtils.createCloseButton();
    this.closeAction = closeButton.getAction();

    final JComponent buttons =
        SwingComponentUtils.createButtonPane(runAnalysisButton, exportButton, closeButton);

    SwingComponentUtils.setupWindowContentPane(this, contentPane, buttons, runAnalysisButton);
  }

  /**
   * exports the data to a CSV file
   *
   * @param aFile File object
   */
  private void storeToCsvFile(final File aFile, final UARTDataSet aDataSet) {
    try {
      final CsvExporter exporter = ExportUtils.createCsvExporter(aFile);

      exporter.setHeaders(
          "index",
          "start-time",
          "end-time",
          "event?",
          "event-type",
          "RxD event",
          "TxD event",
          "RxD data",
          "TxD data");

      final List<UARTData> decodedData = aDataSet.getData();
      for (int i = 0; i < decodedData.size(); i++) {
        final UARTData ds = decodedData.get(i);

        final String startTime = Unit.Time.format(aDataSet.getTime(ds.getStartSampleIndex()));
        final String endTime = Unit.Time.format(aDataSet.getTime(ds.getEndSampleIndex()));

        String eventType = null;
        String rxdEvent = null;
        String txdEvent = null;
        String rxdData = null;
        String txdData = null;

        switch (ds.getType()) {
          case UARTData.UART_TYPE_EVENT:
            eventType = ds.getEventName();
            break;

          case UARTData.UART_TYPE_RXEVENT:
            rxdEvent = ds.getEventName();
            break;

          case UARTData.UART_TYPE_TXEVENT:
            txdEvent = ds.getEventName();
            break;

          case UARTData.UART_TYPE_RXDATA:
            rxdData = Integer.toString(ds.getData());
            break;

          case UARTData.UART_TYPE_TXDATA:
            txdData = Integer.toString(ds.getData());
            break;

          default:
            break;
        }

        exporter.addRow(
            Integer.valueOf(i),
            startTime,
            endTime,
            Boolean.valueOf(ds.isEvent()),
            eventType,
            rxdEvent,
            txdEvent,
            rxdData,
            txdData);
      }

      exporter.close();
    } catch (final IOException exception) {
      // Make sure to handle IO-interrupted exceptions properly!
      if (!HostUtils.handleInterruptedException(exception)) {
        LOG.log(Level.WARNING, "CSV export failed!", exception);
      }
    }
  }

  /**
   * stores the data to a HTML file
   *
   * @param aFile file object
   */
  private void storeToHtmlFile(final File aFile, final UARTDataSet aDataSet) {
    try {
      toHtmlPage(aFile, aDataSet);
    } catch (final IOException exception) {
      // Make sure to handle IO-interrupted exceptions properly!
      if (!HostUtils.handleInterruptedException(exception)) {
        LOG.log(Level.WARNING, "HTML export failed!", exception);
      }
    }
  }

  /**
   * generate a HTML page
   *
   * @param empty if this is true an empty output is generated
   * @return String with HTML data
   */
  private String toHtmlPage(final File aFile, final UARTDataSet aDataSet) throws IOException {
    final int bitCount = Integer.parseInt((String) this.bits.getSelectedItem());
    final int bitAdder = ((bitCount % 4) != 0) ? 1 : 0;

    final MacroResolver macroResolver =
        new MacroResolver() {
          @Override
          public Object resolve(final String aMacro, final Element aParent) {
            if ("date-now".equals(aMacro)) {
              final DateFormat df = DateFormat.getDateInstance(DateFormat.LONG);
              return df.format(new Date());
            } else if ("decoded-bytes".equals(aMacro)) {
              return Integer.valueOf(aDataSet.getDecodedSymbols());
            } else if ("detected-bus-errors".equals(aMacro)) {
              return Integer.valueOf(aDataSet.getDetectedErrors());
            } else if ("baudrate".equals(aMacro)) {
              final String baudrate;
              if (aDataSet.getBaudRate() <= 0) {
                baudrate = "<span class='error'>Baudrate calculation failed!</span>";
              } else {
                baudrate =
                    String.format(
                        "%d (exact: %d)",
                        Integer.valueOf(aDataSet.getBaudRate()),
                        Integer.valueOf(aDataSet.getBaudRateExact()));
                if (!aDataSet.isBitLengthUsable()) {
                  return baudrate.concat(
                      " <span class='warning'>The baudrate may be wrong, use a higher samplerate to avoid this!</span>");
                }

                return baudrate;
              }
            } else if ("decoded-data".equals(aMacro)) {
              final List<UARTData> decodedData = aDataSet.getData();
              Element tr;

              for (int i = 0; i < decodedData.size(); i++) {
                final UARTData ds = decodedData.get(i);

                if (ds.isEvent()) {
                  String rxEventData = "";
                  String txEventData = "";

                  String bgColor;
                  if (UARTData.UART_TYPE_EVENT == ds.getType()) {
                    rxEventData = txEventData = ds.getEventName();
                    bgColor = "#e0e0e0";
                  } else if (UARTData.UART_TYPE_RXEVENT == ds.getType()) {
                    rxEventData = ds.getEventName();
                    bgColor = "#c0ffc0";
                  } else if (UARTData.UART_TYPE_TXEVENT == ds.getType()) {
                    txEventData = ds.getEventName();
                    bgColor = "#c0ffc0";
                  } else {
                    // unknown event
                    bgColor = "#ff8000";
                  }

                  if (txEventData.endsWith("_ERR") || rxEventData.endsWith("_ERR")) {
                    bgColor = "#ff8000";
                  }

                  tr =
                      aParent
                          .addChild(TR)
                          .addAttribute("style", "background-color: " + bgColor + ";");
                  tr.addChild(TD).addContent(String.valueOf(i));
                  tr.addChild(TD)
                      .addContent(Unit.Time.format(aDataSet.getTime(ds.getStartSampleIndex())));
                  tr.addChild(TD).addContent(rxEventData);
                  tr.addChild(TD);
                  tr.addChild(TD);
                  tr.addChild(TD);
                  tr.addChild(TD).addContent(txEventData);
                  tr.addChild(TD);
                  tr.addChild(TD);
                  tr.addChild(TD);
                } else {
                  String rxDataHex = "", rxDataBin = "", rxDataDec = "", rxDataASCII = "";
                  String txDataHex = "", txDataBin = "", txDataDec = "", txDataASCII = "";

                  // Normal data...
                  if (UARTData.UART_TYPE_RXDATA == ds.getType()) {
                    final int rxData = ds.getData();

                    rxDataHex = integerToHexString(rxData, (bitCount / 4) + bitAdder);
                    rxDataBin = integerToBinString(rxData, bitCount);
                    rxDataDec = String.valueOf(rxData);
                    rxDataASCII = toASCII((char) rxData);
                  } else
                  /* if ( UARTData.UART_TYPE_TXDATA == ds.getType() ) */
                  {
                    final int txData = ds.getData();

                    txDataHex = integerToHexString(txData, (bitCount / 4) + bitAdder);
                    txDataBin = integerToBinString(txData, bitCount);
                    txDataDec = String.valueOf(txData);
                    txDataASCII = toASCII(txData);
                  }

                  tr = aParent.addChild(TR);
                  tr.addChild(TD).addContent(String.valueOf(i));
                  tr.addChild(TD)
                      .addContent(Unit.Time.format(aDataSet.getTime(ds.getStartSampleIndex())));
                  tr.addChild(TD).addContent("0x", rxDataHex);
                  tr.addChild(TD).addContent("0b", rxDataBin);
                  tr.addChild(TD).addContent(rxDataDec);
                  tr.addChild(TD).addContent(rxDataASCII);
                  tr.addChild(TD).addContent("0x", txDataHex);
                  tr.addChild(TD).addContent("0b", txDataBin);
                  tr.addChild(TD).addContent(txDataDec);
                  tr.addChild(TD).addContent(txDataASCII);
                }
              }
            }
            return null;
          }
        };

    if (aFile == null) {
      final HtmlExporter exporter = createHtmlTemplate(ExportUtils.createHtmlExporter());
      return exporter.toString(macroResolver);
    } else {
      final HtmlFileExporter exporter =
          (HtmlFileExporter) createHtmlTemplate(ExportUtils.createHtmlExporter(aFile));
      exporter.write(macroResolver);
      exporter.close();
    }

    return null;
  }
}
示例#2
0
/** Denotes a front-end controller for the client. */
public final class ClientController implements ActionProvider, CaptureCallback, AnalysisCallback {
  // INNER TYPES

  /** Provides a default tool context implementation. */
  static final class DefaultToolContext implements ToolContext {
    // VARIABLES

    private final int startSampleIdx;
    private final int endSampleIdx;

    // CONSTRUCTORS

    /**
     * Creates a new DefaultToolContext instance.
     *
     * @param aStartSampleIdx the starting sample index;
     * @param aEndSampleIdx the ending sample index.
     */
    public DefaultToolContext(final int aStartSampleIdx, final int aEndSampleIdx) {
      this.startSampleIdx = aStartSampleIdx;
      this.endSampleIdx = aEndSampleIdx;
    }

    /** @see nl.lxtreme.ols.api.tools.ToolContext#getEndSampleIndex() */
    @Override
    public int getEndSampleIndex() {
      return this.endSampleIdx;
    }

    /** @see nl.lxtreme.ols.api.tools.ToolContext#getLength() */
    @Override
    public int getLength() {
      return Math.max(0, this.endSampleIdx - this.startSampleIdx);
    }

    /** @see nl.lxtreme.ols.api.tools.ToolContext#getStartSampleIndex() */
    @Override
    public int getStartSampleIndex() {
      return this.startSampleIdx;
    }
  }

  // CONSTANTS

  private static final Logger LOG = Logger.getLogger(ClientController.class.getName());

  // VARIABLES

  private final ActionManager actionManager;
  private final BundleContext bundleContext;
  private final DataContainer dataContainer;
  private final EventListenerList evenListeners;
  private final ProjectManager projectManager;
  private final Host host;

  private MainFrame mainFrame;

  private volatile DeviceController currentDevCtrl;

  // CONSTRUCTORS

  /** Creates a new ClientController instance. */
  public ClientController(
      final BundleContext aBundleContext, final Host aHost, final ProjectManager aProjectManager) {
    this.bundleContext = aBundleContext;
    this.host = aHost;
    this.projectManager = aProjectManager;

    this.dataContainer = new DataContainer(this.projectManager);
    this.actionManager = new ActionManager();
    this.evenListeners = new EventListenerList();

    fillActionManager(this.actionManager);
  }

  // METHODS

  /**
   * Adds a cursor change listener.
   *
   * @param aListener the listener to add, cannot be <code>null</code>.
   */
  public void addCursorChangeListener(final DiagramCursorChangeListener aListener) {
    this.evenListeners.add(DiagramCursorChangeListener.class, aListener);
  }

  /**
   * Adds the given device controller to this controller.
   *
   * @param aDeviceController the device controller to add, cannot be <code>null</code>.
   */
  public void addDevice(final DeviceController aDeviceController) {
    if (this.mainFrame != null) {
      if (this.mainFrame.addDeviceMenuItem(aDeviceController)) {
        this.currentDevCtrl = aDeviceController;
      }
    }

    updateActions();
  }

  /**
   * Adds the given exporter to this controller.
   *
   * @param aExporter the exporter to add, cannot be <code>null</code>.
   */
  public void addExporter(final Exporter aExporter) {
    if (this.mainFrame != null) {
      this.mainFrame.addExportMenuItem(aExporter.getName());
    }

    updateActions();
  }

  /**
   * Adds the given tool to this controller.
   *
   * @param aTool the tool to add, cannot be <code>null</code>.
   */
  public void addTool(final Tool aTool) {
    if (this.mainFrame != null) {
      this.mainFrame.addToolMenuItem(aTool.getName());
    }

    updateActions();
  }

  /** @see nl.lxtreme.ols.api.tools.AnalysisCallback#analysisAborted(java.lang.String) */
  @Override
  public void analysisAborted(final String aReason) {
    setStatus("Analysis aborted! " + aReason);

    updateActions();
  }

  /**
   * @see
   *     nl.lxtreme.ols.api.tools.AnalysisCallback#analysisComplete(nl.lxtreme.ols.api.data.CapturedData)
   */
  @Override
  public void analysisComplete(final CapturedData aNewCapturedData) {
    if (aNewCapturedData != null) {
      this.dataContainer.setCapturedData(aNewCapturedData);
    }
    if (this.mainFrame != null) {
      repaintMainFrame();
    }

    setStatus("");
    updateActions();
  }

  /** Cancels the current capturing (if in progress). */
  public void cancelCapture() {
    final DeviceController deviceController = getDeviceController();
    if (deviceController == null) {
      return;
    }

    deviceController.cancel();
  }

  /** @see nl.lxtreme.ols.api.devices.CaptureCallback#captureAborted(java.lang.String) */
  @Override
  public void captureAborted(final String aReason) {
    setStatus("Capture aborted! " + aReason);
    updateActions();
  }

  /**
   * @see
   *     nl.lxtreme.ols.api.devices.CaptureCallback#captureComplete(nl.lxtreme.ols.api.data.CapturedData)
   */
  @Override
  public void captureComplete(final CapturedData aCapturedData) {
    setCapturedData(aCapturedData);

    setStatus("Capture finished at {0,date,medium} {0,time,medium}.", new Date());

    updateActions();
  }

  /**
   * Captures the data of the current device controller.
   *
   * @param aParent the parent window to use, can be <code>null</code>.
   * @return <code>true</code> if the capture succeeded, <code>false</code> otherwise.
   * @throws IOException in case of I/O problems.
   */
  public boolean captureData(final Window aParent) {
    final DeviceController devCtrl = getDeviceController();
    if (devCtrl == null) {
      return false;
    }

    try {
      if (devCtrl.setupCapture(aParent)) {
        setStatus(
            "Capture from {0} started at {1,date,medium} {1,time,medium} ...",
            devCtrl.getName(), new Date());

        devCtrl.captureData(this);
        return true;
      }

      return false;
    } catch (IOException exception) {
      captureAborted("I/O problem: " + exception.getMessage());

      // Make sure to handle IO-interrupted exceptions properly!
      if (!HostUtils.handleInterruptedException(exception)) {
        exception.printStackTrace();
      }

      return false;
    } finally {
      updateActions();
    }
  }

  /** @see nl.lxtreme.ols.api.devices.CaptureCallback#captureStarted(int, int, int) */
  @Override
  public synchronized void captureStarted(
      final int aSampleRate, final int aChannelCount, final int aChannelMask) {
    final Runnable runner =
        new Runnable() {
          @Override
          public void run() {
            updateActions();
          }
        };

    if (SwingUtilities.isEventDispatchThread()) {
      runner.run();
    } else {
      SwingUtilities.invokeLater(runner);
    }
  }

  /** Clears all current cursors. */
  public void clearAllCursors() {
    for (int i = 0; i < CapturedData.MAX_CURSORS; i++) {
      this.dataContainer.setCursorPosition(i, null);
    }
    fireCursorChangedEvent(0, -1); // removed...

    updateActions();
  }

  /** Clears the current device controller. */
  public void clearDeviceController() {
    this.currentDevCtrl = null;
  }

  /**
   * Clears the current project, and start over as it were a new project, in which no captured data
   * is shown.
   */
  public void createNewProject() {
    this.projectManager.createNewProject();

    if (this.mainFrame != null) {
      this.mainFrame.repaint();
    }

    updateActions();
  }

  /** Exits the client application. */
  public void exit() {
    if (this.host != null) {
      this.host.exit();
    }
  }

  /**
   * Exports the current diagram to the given exporter.
   *
   * @param aExporter the exporter to export to, cannot be <code>null</code>.
   * @param aOutputStream the output stream to write the export to, cannot be <code>null</code>.
   * @throws IOException in case of I/O problems.
   */
  public void exportTo(final Exporter aExporter, final OutputStream aOutputStream)
      throws IOException {
    if (this.mainFrame != null) {
      aExporter.export(this.dataContainer, this.mainFrame.getDiagramScrollPane(), aOutputStream);
    }
  }

  /** @see nl.lxtreme.ols.client.ActionProvider#getAction(java.lang.String) */
  public Action getAction(final String aID) {
    return this.actionManager.getAction(aID);
  }

  /** @return the dataContainer */
  public DataContainer getDataContainer() {
    return this.dataContainer;
  }

  /**
   * Returns the current device controller.
   *
   * @return the current device controller, can be <code>null</code>.
   */
  public DeviceController getDeviceController() {
    return this.currentDevCtrl;
  }

  /**
   * Returns all current tools known to the OSGi framework.
   *
   * @return a collection of tools, never <code>null</code>.
   */
  public final Collection<DeviceController> getDevices() {
    final List<DeviceController> tools = new ArrayList<DeviceController>();
    synchronized (this.bundleContext) {
      try {
        final ServiceReference[] serviceRefs =
            this.bundleContext.getAllServiceReferences(DeviceController.class.getName(), null);
        for (ServiceReference serviceRef : serviceRefs) {
          tools.add((DeviceController) this.bundleContext.getService(serviceRef));
        }
      } catch (InvalidSyntaxException exception) {
        throw new RuntimeException(exception);
      }
    }
    return tools;
  }

  /**
   * Returns the exporter with the given name.
   *
   * @param aName the name of the exporter to return, cannot be <code>null</code>.
   * @return the exporter with the given name, can be <code>null</code> if no such exporter is
   *     found.
   * @throws IllegalArgumentException in case the given name was <code>null</code> or empty.
   */
  public Exporter getExporter(final String aName) throws IllegalArgumentException {
    if ((aName == null) || aName.trim().isEmpty()) {
      throw new IllegalArgumentException("Name cannot be null or empty!");
    }

    try {
      final ServiceReference[] serviceRefs =
          this.bundleContext.getAllServiceReferences(Exporter.class.getName(), null);
      final int count = (serviceRefs == null) ? 0 : serviceRefs.length;

      for (int i = 0; i < count; i++) {
        final Exporter exporter = (Exporter) this.bundleContext.getService(serviceRefs[i]);

        if (aName.equals(exporter.getName())) {
          return exporter;
        }
      }

      return null;
    } catch (InvalidSyntaxException exception) {
      throw new RuntimeException("getExporter failed!", exception);
    }
  }

  /**
   * Returns the names of all current available exporters.
   *
   * @return an array of exporter names, never <code>null</code>, but can be empty.
   */
  public String[] getExporterNames() {
    try {
      final ServiceReference[] serviceRefs =
          this.bundleContext.getAllServiceReferences(Exporter.class.getName(), null);
      final int count = serviceRefs == null ? 0 : serviceRefs.length;

      final String[] result = new String[count];

      for (int i = 0; i < count; i++) {
        final Exporter exporter = (Exporter) this.bundleContext.getService(serviceRefs[i]);

        result[i] = exporter.getName();
        this.bundleContext.ungetService(serviceRefs[i]);
      }

      return result;
    } catch (InvalidSyntaxException exception) {
      throw new RuntimeException("getAllExporterNames failed!", exception);
    }
  }

  /**
   * Returns the current project's filename.
   *
   * @return a project filename, as file object, can be <code>null</code>.
   */
  public File getProjectFilename() {
    return this.projectManager.getCurrentProject().getFilename();
  }

  /**
   * Returns all current tools known to the OSGi framework.
   *
   * @return a collection of tools, never <code>null</code>.
   */
  public final Collection<Tool> getTools() {
    final List<Tool> tools = new ArrayList<Tool>();
    synchronized (this.bundleContext) {
      try {
        final ServiceReference[] serviceRefs =
            this.bundleContext.getAllServiceReferences(Tool.class.getName(), null);
        for (ServiceReference serviceRef : serviceRefs) {
          tools.add((Tool) this.bundleContext.getService(serviceRef));
        }
      } catch (InvalidSyntaxException exception) {
        throw new RuntimeException(exception);
      }
    }
    return tools;
  }

  /**
   * Goes to the current cursor position of the cursor with the given index.
   *
   * @param aCursorIdx the index of the cursor to go to, >= 0 && < 10.
   */
  public void gotoCursorPosition(final int aCursorIdx) {
    if ((this.mainFrame != null) && this.dataContainer.isCursorsEnabled()) {
      final Long cursorPosition = this.dataContainer.getCursorPosition(aCursorIdx);
      if (cursorPosition != null) {
        this.mainFrame.gotoPosition(cursorPosition.longValue());
      }
    }
  }

  /** Goes to the current cursor position of the first available cursor. */
  public void gotoFirstAvailableCursor() {
    if ((this.mainFrame != null) && this.dataContainer.isCursorsEnabled()) {
      for (int c = 0; c < CapturedData.MAX_CURSORS; c++) {
        if (this.dataContainer.isCursorPositionSet(c)) {
          final Long cursorPosition = this.dataContainer.getCursorPosition(c);
          if (cursorPosition != null) {
            this.mainFrame.gotoPosition(cursorPosition.longValue());
          }
          break;
        }
      }
    }
  }

  /** Goes to the current cursor position of the last available cursor. */
  public void gotoLastAvailableCursor() {
    if ((this.mainFrame != null) && this.dataContainer.isCursorsEnabled()) {
      for (int c = CapturedData.MAX_CURSORS - 1; c >= 0; c--) {
        if (this.dataContainer.isCursorPositionSet(c)) {
          final Long cursorPosition = this.dataContainer.getCursorPosition(c);
          if (cursorPosition != null) {
            this.mainFrame.gotoPosition(cursorPosition.longValue());
          }
          break;
        }
      }
    }
  }

  /** Goes to the position of the trigger. */
  public void gotoTriggerPosition() {
    if ((this.mainFrame != null) && this.dataContainer.hasTriggerData()) {
      final long position = this.dataContainer.getTriggerPosition();
      this.mainFrame.gotoPosition(position);
    }
  }

  /**
   * Returns whether there is a device selected or not.
   *
   * @return <code>true</code> if there is a device selected, <code>false</code> if no device is
   *     selected.
   */
  public synchronized boolean isDeviceSelected() {
    return this.currentDevCtrl != null;
  }

  /**
   * Returns whether the current device is setup at least once.
   *
   * @return <code>true</code> if the current device is setup, <code>false</code> otherwise.
   * @see #isDeviceSelected()
   */
  public synchronized boolean isDeviceSetup() {
    return (this.currentDevCtrl != null) && this.currentDevCtrl.isSetup();
  }

  /**
   * Returns whether or not the current project is changed.
   *
   * @return <code>true</code> if the current project is changed, <code>false</code> if the current
   *     project is not changed.
   */
  public boolean isProjectChanged() {
    return this.projectManager.getCurrentProject().isChanged();
  }

  /**
   * Loads an OLS data file from the given file.
   *
   * @param aFile the file to load as OLS data, cannot be <code>null</code>.
   * @throws IOException in case of I/O problems.
   */
  public void openDataFile(final File aFile) throws IOException {
    final FileReader reader = new FileReader(aFile);

    try {
      final Project tempProject = this.projectManager.createTemporaryProject();
      OlsDataHelper.read(tempProject, reader);

      setChannelLabels(tempProject.getChannelLabels());
      setCapturedData(tempProject.getCapturedData());
      setCursorData(tempProject.getCursorPositions(), tempProject.isCursorsEnabled());
    } finally {
      reader.close();

      zoomToFit();

      updateActions();
    }
  }

  /**
   * Opens the project denoted by the given file.
   *
   * @param aFile the project file to open, cannot be <code>null</code>.
   * @throws IOException in case of I/O problems.
   */
  public void openProjectFile(final File aFile) throws IOException {
    FileInputStream fis = new FileInputStream(aFile);

    this.projectManager.loadProject(fis);

    final Project project = this.projectManager.getCurrentProject();
    project.setFilename(aFile);

    zoomToFit();
  }

  /**
   * Removes the cursor denoted by the given cursor index.
   *
   * @param aCursorIdx the index of the cursor to remove, >= 0 && < 10.
   */
  public void removeCursor(final int aCursorIdx) {
    if (this.mainFrame != null) {
      this.dataContainer.setCursorPosition(aCursorIdx, null);
      fireCursorChangedEvent(aCursorIdx, -1); // removed...
    }

    updateActions();
  }

  /**
   * Removes a cursor change listener.
   *
   * @param aListener the listener to remove, cannot be <code>null</code>.
   */
  public void removeCursorChangeListener(final DiagramCursorChangeListener aListener) {
    this.evenListeners.remove(DiagramCursorChangeListener.class, aListener);
  }

  /**
   * Removes the given device from the list of devices.
   *
   * @param aDeviceController the device to remove, cannot be <code>null</code>.
   */
  public void removeDevice(final DeviceController aDeviceController) {
    if (this.currentDevCtrl == aDeviceController) {
      this.currentDevCtrl = null;
    }

    if (this.mainFrame != null) {
      this.mainFrame.removeDeviceMenuItem(aDeviceController.getName());
    }

    updateActions();
  }

  /**
   * Removes the given exporter from the list of exporters.
   *
   * @param aExporter the exporter to remove, cannot be <code>null</code>.
   */
  public void removeExporter(final Exporter aExporter) {
    if (this.mainFrame != null) {
      this.mainFrame.removeExportMenuItem(aExporter.getName());
    }

    updateActions();
  }

  /**
   * Removes the given tool from the list of tools.
   *
   * @param aTool the tool to remove, cannot be <code>null</code>.
   */
  public void removeTool(final Tool aTool) {
    if (this.mainFrame != null) {
      this.mainFrame.removeToolMenuItem(aTool.getName());
    }

    updateActions();
  }

  /**
   * Repeats the capture with the current settings.
   *
   * @param aParent the parent window to use, can be <code>null</code>.
   */
  public boolean repeatCaptureData(final Window aParent) {
    final DeviceController devCtrl = getDeviceController();
    if (devCtrl == null) {
      return false;
    }

    try {
      setStatus(
          "Capture from {0} started at {1,date,medium} {1,time,medium} ...",
          devCtrl.getName(), new Date());

      devCtrl.captureData(this);

      return true;
    } catch (IOException exception) {
      captureAborted("I/O problem: " + exception.getMessage());

      exception.printStackTrace();

      // Make sure to handle IO-interrupted exceptions properly!
      HostUtils.handleInterruptedException(exception);

      return false;
    } finally {
      updateActions();
    }
  }

  /**
   * Runs the tool denoted by the given name.
   *
   * @param aToolName the name of the tool to run, cannot be <code>null</code>;
   * @param aParent the parent window to use, can be <code>null</code>.
   */
  public void runTool(final String aToolName, final Window aParent) {
    if (LOG.isLoggable(Level.INFO)) {
      LOG.log(Level.INFO, "Running tool: \"{0}\" ...", aToolName);
    }

    final Tool tool = findToolByName(aToolName);
    if (tool == null) {
      JOptionPane.showMessageDialog(
          aParent, "No such tool found: " + aToolName, "Error ...", JOptionPane.ERROR_MESSAGE);
    } else {
      final ToolContext context = createToolContext();
      tool.process(aParent, this.dataContainer, context, this);
    }

    updateActions();
  }

  /** @see nl.lxtreme.ols.api.devices.CaptureCallback#samplesCaptured(java.util.List) */
  @Override
  public void samplesCaptured(final List<Sample> aSamples) {
    if (this.mainFrame != null) {
      this.mainFrame.sampleCaptured(aSamples);
    }
    updateActions();
  }

  /**
   * Saves an OLS data file to the given file.
   *
   * @param aFile the file to save the OLS data to, cannot be <code>null</code>.
   * @throws IOException in case of I/O problems.
   */
  public void saveDataFile(final File aFile) throws IOException {
    final FileWriter writer = new FileWriter(aFile);
    try {
      final Project tempProject = this.projectManager.createTemporaryProject();
      tempProject.setCapturedData(this.dataContainer);

      OlsDataHelper.write(tempProject, writer);
    } finally {
      writer.flush();
      writer.close();
    }
  }

  /**
   * Saves the current project to the given file.
   *
   * @param aFile the file to save the project information to, cannot be <code>null</code>.
   * @throws IOException in case of I/O problems.
   */
  public void saveProjectFile(final String aName, final File aFile) throws IOException {
    FileOutputStream out = null;
    try {
      final Project project = this.projectManager.getCurrentProject();
      project.setFilename(aFile);
      project.setName(aName);

      out = new FileOutputStream(aFile);
      this.projectManager.saveProject(out);
    } finally {
      HostUtils.closeResource(out);
    }
  }

  /**
   * Sets whether or not cursors are enabled.
   *
   * @param aState <code>true</code> if the cursors should be enabled, <code>false</code> otherwise.
   */
  public void setCursorMode(final boolean aState) {
    this.dataContainer.setCursorEnabled(aState);
    // Reflect the change directly on the diagram...
    repaintMainFrame();

    updateActions();
  }

  /**
   * Sets the cursor position of the cursor with the given index.
   *
   * @param aCursorIdx the index of the cursor to set, >= 0 && < 10;
   * @param aLocation the mouse location on screen where the cursor should become, cannot be <code>
   *     null</code>.
   */
  public void setCursorPosition(final int aCursorIdx, final Point aLocation) {
    // Implicitly enable cursor mode, the user already had made its
    // intensions clear that he want to have this by opening up the
    // context menu anyway...
    setCursorMode(true);

    if (this.mainFrame != null) {
      // Convert the mouse-position to a sample index...
      final long sampleIdx = this.mainFrame.convertMousePositionToSampleIndex(aLocation);

      this.dataContainer.setCursorPosition(aCursorIdx, Long.valueOf(sampleIdx));

      fireCursorChangedEvent(aCursorIdx, aLocation.x);
    }

    updateActions();
  }

  /**
   * Sets the current device controller to the given value.
   *
   * @param aDeviceName the name of the device controller to set, cannot be <code>null</code>.
   */
  public synchronized void setDeviceController(final String aDeviceName) {
    if (LOG.isLoggable(Level.INFO)) {
      final String name = (aDeviceName == null) ? "no device" : aDeviceName;
      LOG.log(Level.INFO, "Setting current device controller to: \"{0}\" ...", name);
    }

    final Collection<DeviceController> devices = getDevices();
    for (DeviceController device : devices) {
      if (aDeviceName.equals(device.getName())) {
        this.currentDevCtrl = device;
      }
    }

    updateActions();
  }

  /** @param aMainFrame the mainFrame to set */
  public void setMainFrame(final MainFrame aMainFrame) {
    if (this.mainFrame != null) {
      this.projectManager.removePropertyChangeListener(this.mainFrame);
    }
    if (aMainFrame != null) {
      this.projectManager.addPropertyChangeListener(aMainFrame);
    }

    this.mainFrame = aMainFrame;
  }

  /**
   * Sets a status message.
   *
   * @param aMessage the message to set;
   * @param aMessageArgs the (optional) message arguments.
   */
  public final void setStatus(final String aMessage, final Object... aMessageArgs) {
    if (this.mainFrame != null) {
      this.mainFrame.setStatus(aMessage, aMessageArgs);
    }
  }

  /** Shows the "about OLS" dialog on screen. the parent window to use, can be <code>null</code>. */
  public void showAboutBox() {
    MainFrame.showAboutBox(this.host.getVersion());
  }

  /**
   * Shows a dialog with all running OSGi bundles.
   *
   * @param aOwner the owning window to use, can be <code>null</code>.
   */
  public void showBundlesDialog(final Window aOwner) {
    BundlesDialog dialog = new BundlesDialog(aOwner, this.bundleContext);
    if (dialog.showDialog()) {
      dialog.dispose();
      dialog = null;
    }
  }

  /**
   * Shows the label-editor dialog on screen.
   *
   * <p>Display the diagram labels dialog. Will block until the dialog is closed again.
   *
   * @param aParent the parent window to use, can be <code>null</code>.
   */
  public void showLabelsDialog(final Window aParent) {
    if (this.mainFrame != null) {
      DiagramLabelsDialog dialog =
          new DiagramLabelsDialog(aParent, this.dataContainer.getChannelLabels());
      if (dialog.showDialog()) {
        final String[] channelLabels = dialog.getChannelLabels();
        setChannelLabels(channelLabels);
      }

      dialog.dispose();
      dialog = null;
    }
  }

  /**
   * Shows the settings-editor dialog on screen.
   *
   * <p>Display the diagram settings dialog. Will block until the dialog is closed again.
   *
   * @param aParent the parent window to use, can be <code>null</code>.
   */
  public void showModeSettingsDialog(final Window aParent) {
    if (this.mainFrame != null) {
      ModeSettingsDialog dialog = new ModeSettingsDialog(aParent, getDiagramSettings());
      if (dialog.showDialog()) {
        updateDiagramSettings(dialog.getDiagramSettings());
      }

      dialog.dispose();
      dialog = null;
    }
  }

  /** @param aOwner */
  public void showPreferencesDialog(final Window aParent) {
    GeneralSettingsDialog dialog = new GeneralSettingsDialog(aParent, getDiagramSettings());
    if (dialog.showDialog()) {
      updateDiagramSettings(dialog.getDiagramSettings());
    }

    dialog.dispose();
    dialog = null;
  }

  /** @see nl.lxtreme.ols.api.ProgressCallback#updateProgress(int) */
  @Override
  public void updateProgress(final int aPercentage) {
    if (this.mainFrame != null) {
      this.mainFrame.setProgress(aPercentage);
    }
  }

  /** Zooms in to the maximum zoom level. */
  public void zoomDefault() {
    if (this.mainFrame != null) {
      this.mainFrame.zoomDefault();
    }

    updateActions();
  }

  /** Zooms in with a factor of 2.0. */
  public void zoomIn() {
    if (this.mainFrame != null) {
      this.mainFrame.zoomIn();
    }

    updateActions();
  }

  /** Zooms out with a factor of 2.0. */
  public void zoomOut() {
    if (this.mainFrame != null) {
      this.mainFrame.zoomOut();
    }

    updateActions();
  }

  /** Zooms to fit the diagram to the current window dimensions. */
  public void zoomToFit() {
    if (this.mainFrame != null) {
      this.mainFrame.zoomToFit();
    }

    updateActions();
  }

  /**
   * Returns the current main frame.
   *
   * @return the main frame, can be <code>null</code>.
   */
  final MainFrame getMainFrame() {
    return this.mainFrame;
  }

  /**
   * Creates the tool context denoting the range of samples that should be analysed by a tool.
   *
   * @return a tool context, never <code>null</code>.
   */
  private ToolContext createToolContext() {
    int startOfDecode = -1;
    int endOfDecode = -1;

    final int dataLength = this.dataContainer.getValues().length;
    if (this.dataContainer.isCursorsEnabled()) {
      if (this.dataContainer.isCursorPositionSet(0)) {
        final Long cursor1 = this.dataContainer.getCursorPosition(0);
        startOfDecode = this.dataContainer.getSampleIndex(cursor1.longValue()) - 1;
      }
      if (this.dataContainer.isCursorPositionSet(1)) {
        final Long cursor2 = this.dataContainer.getCursorPosition(1);
        endOfDecode = this.dataContainer.getSampleIndex(cursor2.longValue()) + 1;
      }
    } else {
      startOfDecode = 0;
      endOfDecode = dataLength;
    }

    startOfDecode = Math.max(0, startOfDecode);
    if ((endOfDecode < 0) || (endOfDecode >= dataLength)) {
      endOfDecode = dataLength - 1;
    }

    return new DefaultToolContext(startOfDecode, endOfDecode);
  }

  /** @param aActionManager */
  private void fillActionManager(final ActionManager aActionManager) {
    aActionManager.add(new NewProjectAction(this));
    aActionManager.add(new OpenProjectAction(this));
    aActionManager.add(new SaveProjectAction(this)).setEnabled(false);
    aActionManager.add(new SaveProjectAsAction(this)).setEnabled(false);
    aActionManager.add(new OpenDataFileAction(this));
    aActionManager.add(new SaveDataFileAction(this)).setEnabled(false);
    aActionManager.add(new ExitAction(this));

    aActionManager.add(new CaptureAction(this));
    aActionManager.add(new CancelCaptureAction(this)).setEnabled(false);
    aActionManager.add(new RepeatCaptureAction(this)).setEnabled(false);

    aActionManager.add(new ZoomInAction(this)).setEnabled(false);
    aActionManager.add(new ZoomOutAction(this)).setEnabled(false);
    aActionManager.add(new ZoomDefaultAction(this)).setEnabled(false);
    aActionManager.add(new ZoomFitAction(this)).setEnabled(false);

    aActionManager.add(new GotoTriggerAction(this)).setEnabled(false);
    for (int c = 0; c < CapturedData.MAX_CURSORS; c++) {
      aActionManager.add(new GotoNthCursorAction(this, c)).setEnabled(false);
    }
    aActionManager.add(new GotoFirstCursorAction(this)).setEnabled(false);
    aActionManager.add(new GotoLastCursorAction(this)).setEnabled(false);
    aActionManager.add(new ClearCursors(this)).setEnabled(false);
    aActionManager.add(new SetCursorModeAction(this));
    for (int c = 0; c < CapturedData.MAX_CURSORS; c++) {
      aActionManager.add(new SetCursorAction(this, c));
    }

    aActionManager.add(new ShowGeneralSettingsAction(this));
    aActionManager.add(new ShowModeSettingsAction(this));
    aActionManager.add(new ShowDiagramLabelsAction(this));

    aActionManager.add(new HelpAboutAction(this));
    aActionManager.add(new ShowBundlesAction(this));
  }

  /**
   * Searches for the tool with the given name.
   *
   * @param aToolName the name of the tool to search for, cannot be <code>null</code>.
   * @return the tool with the given name, can be <code>null</code> if no such tool can be found.
   */
  private Tool findToolByName(final String aToolName) {
    Tool tool = null;

    final Collection<Tool> tools = getTools();
    for (Tool _tool : tools) {
      if (aToolName.equals(_tool.getName())) {
        tool = _tool;
        break;
      }
    }
    return tool;
  }

  /**
   * @param aCursorIdx
   * @param aMouseXpos
   */
  private void fireCursorChangedEvent(final int aCursorIdx, final int aMouseXpos) {
    final DiagramCursorChangeListener[] listeners =
        this.evenListeners.getListeners(DiagramCursorChangeListener.class);
    for (final DiagramCursorChangeListener listener : listeners) {
      if (aMouseXpos >= 0) {
        listener.cursorChanged(aCursorIdx, aMouseXpos);
      } else {
        listener.cursorRemoved(aCursorIdx);
      }
    }
  }

  /**
   * Returns the current diagram settings.
   *
   * @return the current diagram settings, can be <code>null</code> if there is no main frame to
   *     take the settings from.
   */
  private DiagramSettings getDiagramSettings() {
    return this.mainFrame != null ? this.mainFrame.getDiagramSettings() : null;
  }

  /** Dispatches a request to repaint the entire main frame. */
  private void repaintMainFrame() {
    SwingUtilities.invokeLater(
        new Runnable() {
          @Override
          public void run() {
            ClientController.this.mainFrame.repaint();
          }
        });
  }

  /**
   * Sets the captured data and zooms the view to show all the data.
   *
   * @param aCapturedData the new captured data to set, cannot be <code>null</code>.
   */
  private void setCapturedData(final CapturedData aCapturedData) {
    this.dataContainer.setCapturedData(aCapturedData);

    if (this.mainFrame != null) {
      this.mainFrame.zoomToFit();
    }
  }

  /**
   * Set the channel labels.
   *
   * @param aChannelLabels the channel labels to set, cannot be <code>null</code>.
   */
  private void setChannelLabels(final String[] aChannelLabels) {
    if (aChannelLabels != null) {
      this.dataContainer.setChannelLabels(aChannelLabels);
      this.mainFrame.setChannelLabels(aChannelLabels);
    }
  }

  /**
   * @param aCursorData the cursor positions to set, cannot be <code>null</code>;
   * @param aCursorsEnabled <code>true</code> if cursors should be enabled, <code>false</code> if
   *     they should be disabled.
   */
  private void setCursorData(final Long[] aCursorData, final boolean aCursorsEnabled) {
    this.dataContainer.setCursorEnabled(aCursorsEnabled);
    for (int i = 0; i < CapturedData.MAX_CURSORS; i++) {
      this.dataContainer.setCursorPosition(i, aCursorData[i]);
    }
  }

  /** Synchronizes the state of the actions to the current state of this host. */
  private void updateActions() {
    final DeviceController currentDeviceController = getDeviceController();

    final boolean deviceControllerSet = currentDeviceController != null;
    final boolean deviceCapturing = deviceControllerSet && currentDeviceController.isCapturing();
    final boolean deviceSetup =
        deviceControllerSet && !deviceCapturing && currentDeviceController.isSetup();

    getAction(CaptureAction.ID).setEnabled(deviceControllerSet);
    getAction(CancelCaptureAction.ID).setEnabled(deviceCapturing);
    getAction(RepeatCaptureAction.ID).setEnabled(deviceSetup);

    final boolean projectChanged = this.projectManager.getCurrentProject().isChanged();
    final boolean projectSavedBefore =
        this.projectManager.getCurrentProject().getFilename() != null;
    final boolean dataAvailable = this.dataContainer.hasCapturedData();

    getAction(SaveProjectAction.ID).setEnabled(projectChanged);
    getAction(SaveProjectAsAction.ID).setEnabled(projectSavedBefore && projectChanged);
    getAction(SaveDataFileAction.ID).setEnabled(dataAvailable);

    getAction(ZoomInAction.ID).setEnabled(dataAvailable);
    getAction(ZoomOutAction.ID).setEnabled(dataAvailable);
    getAction(ZoomDefaultAction.ID).setEnabled(dataAvailable);
    getAction(ZoomFitAction.ID).setEnabled(dataAvailable);

    final boolean triggerEnable = dataAvailable && this.dataContainer.hasTriggerData();
    getAction(GotoTriggerAction.ID).setEnabled(triggerEnable);

    // Update the cursor actions accordingly...
    final boolean enableCursors = dataAvailable && this.dataContainer.isCursorsEnabled();

    for (int c = 0; c < CapturedData.MAX_CURSORS; c++) {
      final boolean enabled = enableCursors && this.dataContainer.isCursorPositionSet(c);
      getAction(GotoNthCursorAction.getID(c)).setEnabled(enabled);
    }

    getAction(GotoFirstCursorAction.ID).setEnabled(enableCursors);
    getAction(GotoLastCursorAction.ID).setEnabled(enableCursors);

    getAction(SetCursorModeAction.ID).setEnabled(dataAvailable);
    getAction(SetCursorModeAction.ID)
        .putValue(Action.SELECTED_KEY, Boolean.valueOf(this.dataContainer.isCursorsEnabled()));

    boolean anyCursorSet = false;
    for (int c = 0; c < CapturedData.MAX_CURSORS; c++) {
      final boolean cursorPositionSet = this.dataContainer.isCursorPositionSet(c);
      anyCursorSet |= cursorPositionSet;

      final Action action = getAction(SetCursorAction.getCursorId(c));
      action.setEnabled(dataAvailable);
      action.putValue(Action.SELECTED_KEY, Boolean.valueOf(cursorPositionSet));
    }

    getAction(ClearCursors.ID).setEnabled(enableCursors && anyCursorSet);
  }

  /**
   * Should be called after the diagram settings are changed. This method will cause the settings to
   * be set on the main frame and writes them to the preference store.
   *
   * @param aSettings the (new/changed) diagram settings to set, cannot be <code>null</code>.
   */
  private void updateDiagramSettings(final DiagramSettings aSettings) {
    if (this.mainFrame != null) {
      this.mainFrame.setDiagramSettings(aSettings);
      repaintMainFrame();
    }
  }
}
示例#3
0
/** Provides a device profile. */
public final class DeviceProfile implements Cloneable, Comparable<DeviceProfile> {
  // INNER TYPES

  /** The various capture clock sources. */
  public static enum CaptureClockSource {
    INTERNAL,
    EXTERNAL_FALLING,
    EXTERNAL_RISING;
  }

  /** The various interfaces of the device. */
  public static enum DeviceInterface {
    SERIAL,
    NETWORK,
    USB;
  }

  /** The various numbering schemes. */
  public static enum NumberingScheme {
    DEFAULT,
    INSIDE,
    OUTSIDE;
  }

  /** The various types of triggers. */
  public static enum TriggerType {
    SIMPLE,
    COMPLEX;
  }

  // CONSTANTS

  /** The short (single word) type of the device described in this profile */
  public static final String DEVICE_TYPE = "device.type";
  /** A longer description of the device */
  public static final String DEVICE_DESCRIPTION = "device.description";
  /** The device interface, currently SERIAL only */
  public static final String DEVICE_INTERFACE = "device.interface";
  /** The device's native clockspeed, in Hertz. */
  public static final String DEVICE_CLOCKSPEED = "device.clockspeed";
  /**
   * Whether or not double-data-rate is supported by the device (also known as the "demux"-mode).
   */
  public static final String DEVICE_SUPPORTS_DDR = "device.supports_ddr";
  /** Supported sample rates in Hertz, separated by comma's */
  public static final String DEVICE_SAMPLERATES = "device.samplerates";
  /** What capture clocks are supported */
  public static final String DEVICE_CAPTURECLOCK = "device.captureclock";
  /** The supported capture sizes, in bytes */
  public static final String DEVICE_CAPTURESIZES = "device.capturesizes";
  /** Whether or not the noise filter is supported */
  public static final String DEVICE_FEATURE_NOISEFILTER = "device.feature.noisefilter";
  /** Whether or not Run-Length encoding is supported */
  public static final String DEVICE_FEATURE_RLE = "device.feature.rle";
  /** Whether or not a testing mode is supported. */
  public static final String DEVICE_FEATURE_TEST_MODE = "device.feature.testmode";
  /** Whether or not triggers are supported */
  public static final String DEVICE_FEATURE_TRIGGERS = "device.feature.triggers";
  /** The number of trigger stages */
  public static final String DEVICE_TRIGGER_STAGES = "device.trigger.stages";
  /** Whether or not "complex" triggers are supported */
  public static final String DEVICE_TRIGGER_COMPLEX = "device.trigger.complex";
  /** The total number of channels usable for capturing */
  public static final String DEVICE_CHANNEL_COUNT = "device.channel.count";
  /**
   * The number of channels groups, together with the channel count determines the channels per
   * group
   */
  public static final String DEVICE_CHANNEL_GROUPS = "device.channel.groups";
  /** Whether the capture size is limited by the enabled channel groups */
  public static final String DEVICE_CAPTURESIZE_BOUND = "device.capturesize.bound";
  /** What channel numbering schemes are supported by the device. */
  public static final String DEVICE_CHANNEL_NUMBERING_SCHEMES = "device.channel.numberingschemes";
  /**
   * Is a delay after opening the port and device detection needed? (0 = no delay, >0 = delay in
   * milliseconds)
   */
  public static final String DEVICE_OPEN_PORT_DELAY = "device.open.portdelay";
  /** The receive timeout (100 = default, in milliseconds) */
  public static final String DEVICE_RECEIVE_TIMEOUT = "device.receive.timeout";
  /**
   * Which metadata keys correspond to this device profile? Value is a comma-separated list of
   * (double quoted) names.
   */
  public static final String DEVICE_METADATA_KEYS = "device.metadata.keys";
  /**
   * In which order are samples sent back from the device? If <code>true</code> then last sample
   * first, if <code>false</code> then first sample first.
   */
  public static final String DEVICE_SAMPLE_REVERSE_ORDER = "device.samples.reverseOrder";
  /** In case of a serial port, does the DTR-line need to be high (= true) or low (= false)? */
  public static final String DEVICE_OPEN_PORT_DTR = "device.open.portdtr";

  /** Filename of the actual file picked up by Felix's FileInstall. */
  public static final String FELIX_FILEINSTALL_FILENAME = "felix.fileinstall.filename";
  /** Service PID of this device profile. */
  private static final String FELIX_SERVICE_PID = "service.pid";
  /** Factory Service PID of this device profile. */
  private static final String FELIX_SERVICE_FACTORY_PID = "service.factoryPid";

  /** All the profile keys that are supported. */
  private static final List<String> KNOWN_KEYS =
      Arrays.asList(
          new String[] {
            DEVICE_TYPE,
            DEVICE_DESCRIPTION,
            DEVICE_INTERFACE,
            DEVICE_CLOCKSPEED,
            DEVICE_SUPPORTS_DDR,
            DEVICE_SAMPLERATES,
            DEVICE_CAPTURECLOCK,
            DEVICE_CAPTURESIZES,
            DEVICE_FEATURE_NOISEFILTER,
            DEVICE_FEATURE_RLE,
            DEVICE_FEATURE_TEST_MODE,
            DEVICE_FEATURE_TRIGGERS,
            DEVICE_TRIGGER_STAGES,
            DEVICE_TRIGGER_COMPLEX,
            DEVICE_CHANNEL_COUNT,
            DEVICE_CHANNEL_GROUPS,
            DEVICE_CAPTURESIZE_BOUND,
            DEVICE_CHANNEL_NUMBERING_SCHEMES,
            DEVICE_OPEN_PORT_DELAY,
            DEVICE_METADATA_KEYS,
            DEVICE_SAMPLE_REVERSE_ORDER,
            DEVICE_OPEN_PORT_DTR,
            DEVICE_RECEIVE_TIMEOUT,
            FELIX_FILEINSTALL_FILENAME
          });

  private static final List<String> IGNORED_KEYS =
      Arrays.asList(new String[] {FELIX_SERVICE_PID, FELIX_SERVICE_FACTORY_PID});

  private static final Logger LOG = Logger.getLogger(DeviceProfile.class.getName());

  // VARIABLES

  private final ConcurrentMap<String, String> properties;

  // CONSTRUCTORS

  /** Creates a new DeviceProfile. */
  public DeviceProfile() {
    this.properties = new ConcurrentHashMap<String, String>();
  }

  // METHODS

  /**
   * @param aFilename
   * @return
   */
  static final File createFile(final String aFilename) {
    if (aFilename == null) {
      throw new IllegalArgumentException("Filename cannot be null!");
    }
    return new File(aFilename.replaceAll("^file:", ""));
  }

  /**
   * Returns a deep copy of this device profile, including all properties.
   *
   * @return a deep copy of this device profile, never <code>null</code>.
   * @see java.lang.Object#clone()
   */
  @Override
  public DeviceProfile clone() {
    try {
      DeviceProfile clone = (DeviceProfile) super.clone();
      clone.properties.putAll(this.properties);
      return clone;
    } catch (CloneNotSupportedException exception) {
      throw new IllegalStateException(exception);
    }
  }

  /** {@inheritDoc} */
  @Override
  public int compareTo(DeviceProfile aProfile) {
    // Issue #123: allow device profiles to be sorted alphabetically...
    int result = getDescription().compareTo(aProfile.getDescription());
    if (result == 0) {
      result = getType().compareTo(aProfile.getType());
    }
    return result;
  }

  /** {@inheritDoc} */
  @Override
  public boolean equals(final Object aObject) {
    if (this == aObject) {
      return true;
    }
    if ((aObject == null) || !(aObject instanceof DeviceProfile)) {
      return false;
    }

    final DeviceProfile other = (DeviceProfile) aObject;
    return this.properties.equals(other.properties);
  }

  /**
   * Returns the capture clock sources supported by the device.
   *
   * @return an array of capture clock sources, never <code>null</code>.
   */
  public CaptureClockSource[] getCaptureClock() {
    final String rawValue = this.properties.get(DEVICE_CAPTURECLOCK);
    final String[] values = rawValue.split(",\\s*");
    final CaptureClockSource[] result = new CaptureClockSource[values.length];
    for (int i = 0; i < values.length; i++) {
      result[i] = CaptureClockSource.valueOf(values[i].trim());
    }
    return result;
  }

  /**
   * Returns all supported capture sizes.
   *
   * @return an array of capture sizes, in bytes, never <code>null</code>.
   */
  public Integer[] getCaptureSizes() {
    final String rawValue = this.properties.get(DEVICE_CAPTURESIZES);
    final String[] values = rawValue.split(",\\s*");
    final List<Integer> result = new ArrayList<Integer>();
    for (String value : values) {
      result.add(Integer.valueOf(value.trim()));
    }
    Collections.sort(
        result, NumberUtils.<Integer>createNumberComparator(false /* aSortAscending */));
    return result.toArray(new Integer[result.size()]);
  }

  /**
   * Returns the total number of capture channels.
   *
   * @return a capture channel count, greater than 0.
   */
  public int getChannelCount() {
    final String value = this.properties.get(DEVICE_CHANNEL_COUNT);
    return Integer.parseInt(value);
  }

  /**
   * Returns the total number of channel groups.
   *
   * @return a channel group count, greater than 0.
   */
  public int getChannelGroupCount() {
    final String value = this.properties.get(DEVICE_CHANNEL_GROUPS);
    return Integer.parseInt(value);
  }

  /**
   * Returns all supported channel numbering schemes.
   *
   * @return an array of numbering schemes, never <code>null</code>.
   */
  public NumberingScheme[] getChannelNumberingSchemes() {
    final String rawValue = this.properties.get(DEVICE_CHANNEL_NUMBERING_SCHEMES);
    final String[] values = rawValue.split(",\\s*");
    final NumberingScheme[] result = new NumberingScheme[values.length];
    for (int i = 0; i < result.length; i++) {
      result[i] = NumberingScheme.valueOf(values[i].trim());
    }
    return result;
  }

  /**
   * Returns the (maximum) capture clock of the device.
   *
   * @return a capture clock, in Hertz.
   */
  public int getClockspeed() {
    final String value = this.properties.get(DEVICE_CLOCKSPEED);
    return Integer.parseInt(value);
  }

  /**
   * Returns the description of the device this profile denotes.
   *
   * @return a device description, never <code>null</code>.
   */
  public String getDescription() {
    final String result = this.properties.get(DEVICE_DESCRIPTION);
    return result == null ? "" : (String) result;
  }

  /**
   * Returns the metadata keys that allow identification of this device profile.
   *
   * <p>Note: if the returned array contains an empty string value (not <code>null</code>, but
   * <code>""</code>!), it means that this profile can be used for <em>all</em> devices.
   *
   * @return an array of metadata keys this profile supports, never <code>null</code>.
   */
  public String[] getDeviceMetadataKeys() {
    final String rawValue = this.properties.get(DEVICE_METADATA_KEYS);
    return StringUtils.tokenizeQuotedStrings(rawValue, ", ");
  }

  /**
   * Returns the interface over which the device communicates.
   *
   * @return the device interface, never <code>null</code>.
   */
  public DeviceInterface getInterface() {
    final String value = this.properties.get(DEVICE_INTERFACE);
    return DeviceInterface.valueOf(value);
  }

  /**
   * Returns the maximum capture size for the given number of <em>enabled</em> channel groups.
   *
   * <p>If the maximum capture size is bound to the number of enabled channel(group)s, this method
   * will divide the maximum possible capture size by the given group count, otherwise the maximum
   * capture size will be returned.
   *
   * @param aChannelGroups the number of channel groups that should be enabled, > 0 && < channel
   *     group count.
   * @return a maximum capture size, in bytes, or -1 if no maximum could be determined, or the given
   *     parameter was <tt>0</tt>.
   * @see #isCaptureSizeBoundToEnabledChannels()
   * @see #getChannelGroupCount()
   */
  public int getMaximumCaptureSizeFor(final int aChannelGroups) {
    final Integer[] sizes = getCaptureSizes();
    if ((sizes == null) || (sizes.length == 0) || (aChannelGroups == 0)) {
      return -1;
    }

    final int maxSize = sizes[0].intValue();
    if (isCaptureSizeBoundToEnabledChannels()) {
      int indication = maxSize / aChannelGroups;

      // Issue #58: Search the best matching value...
      Integer result = null;
      for (int i = sizes.length - 1; i >= 0; i--) {
        if (sizes[i].compareTo(Integer.valueOf(indication)) <= 0) {
          result = sizes[i];
        }
      }

      return (result == null) ? indication : result.intValue();
    }

    return maxSize;
  }

  /**
   * Returns the delay between opening the port to the device and starting the device detection
   * cycle.
   *
   * @return a delay, in milliseconds, >= 0.
   */
  public int getOpenPortDelay() {
    final String value = this.properties.get(DEVICE_OPEN_PORT_DELAY);
    return Integer.parseInt(value);
  }

  /**
   * Returns the (optional) receive timeout.
   *
   * <p>WARNING: if no receive timeout is used, the communication essentially results in a
   * non-blocking I/O operation which can not be cancelled!
   *
   * @return the receive timeout, in ms, or <code>null</code> when no receive timeout should be
   *     used.
   */
  public Integer getReceiveTimeout() {
    final String value = this.properties.get(DEVICE_RECEIVE_TIMEOUT);
    if (value == null) {
      return null;
    }
    int timeout = Integer.parseInt(value);
    return (timeout <= 0) ? null : Integer.valueOf(timeout);
  }

  /**
   * Returns all supported sample rates.
   *
   * @return an array of sample rates, in Hertz, never <code>null</code>.
   */
  public Integer[] getSampleRates() {
    final String rawValue = this.properties.get(DEVICE_SAMPLERATES);
    final String[] values = rawValue.split(",\\s*");
    final SortedSet<Integer> result =
        new TreeSet<Integer>(
            NumberUtils.<Integer>createNumberComparator(false /* aSortAscending */));
    for (String value : values) {
      result.add(Integer.valueOf(value.trim()));
    }

    return result.toArray(new Integer[result.size()]);
  }

  /**
   * Returns the total number of trigger stages (in the complex trigger mode).
   *
   * @return a trigger stage count, greater than 0.
   */
  public int getTriggerStages() {
    final String value = this.properties.get(DEVICE_TRIGGER_STAGES);
    return Integer.parseInt(value);
  }

  /**
   * Returns the device type this profile denotes.
   *
   * @return a device type name, never <code>null</code>.
   */
  public String getType() {
    final String result = this.properties.get(DEVICE_TYPE);
    return result == null ? "<unknown>" : result;
  }

  /** {@inheritDoc} */
  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = (prime * result) + ((this.properties == null) ? 0 : this.properties.hashCode());
    return result;
  }

  /**
   * Returns whether or not the capture size is bound to the number of channels.
   *
   * @return <code>true</code> if the capture size is bound to the number of channels, <code>false
   *     </code> otherwise.
   */
  public boolean isCaptureSizeBoundToEnabledChannels() {
    final String value = this.properties.get(DEVICE_CAPTURESIZE_BOUND);
    return Boolean.parseBoolean(value);
  }

  /**
   * Returns whether or not the device supports "complex" triggers.
   *
   * @return <code>true</code> if complex triggers are supported by the device, <code>false</code>
   *     otherwise.
   */
  public boolean isComplexTriggersSupported() {
    final String value = this.properties.get(DEVICE_TRIGGER_COMPLEX);
    return Boolean.parseBoolean(value);
  }

  /**
   * Returns whether or not the device supports "double-data rate" sampling, also known as
   * "demux"-sampling.
   *
   * @return <code>true</code> if DDR is supported by the device, <code>false</code> otherwise.
   */
  public boolean isDoubleDataRateSupported() {
    final String value = this.properties.get(DEVICE_SUPPORTS_DDR);
    return Boolean.parseBoolean(value);
  }

  /**
   * Returns whether or not the device supports a noise filter.
   *
   * @return <code>true</code> if a noise filter is present in the device, <code>false</code>
   *     otherwise.
   */
  public boolean isNoiseFilterSupported() {
    final String value = this.properties.get(DEVICE_FEATURE_NOISEFILTER);
    return Boolean.parseBoolean(value);
  }

  /**
   * Returns whether upon opening the DTR line needs to be high (= <code>true</code>) or low (=
   * <code>false</code>).
   *
   * <p>This method has no meaning if the used interface is <em>not</em> {@link
   * DeviceInterface#SERIAL}.
   *
   * @return <code>true</code> if the DTR line needs to be set upon opening the serial port, <code>
   *     false</code> if the DTR line needs to be reset upon opening the serial port.
   */
  public boolean isOpenPortDtr() {
    final String value = this.properties.get(DEVICE_OPEN_PORT_DTR);
    return Boolean.parseBoolean(value);
  }

  /**
   * Returns whether or not the device supports RLE (Run-Length Encoding).
   *
   * @return <code>true</code> if a RLE encoder is present in the device, <code>false</code>
   *     otherwise.
   */
  public boolean isRleSupported() {
    final String value = this.properties.get(DEVICE_FEATURE_RLE);
    return Boolean.parseBoolean(value);
  }

  /**
   * Returns whether the device send its samples in "reverse" order.
   *
   * @return <code>true</code> if samples are send in reverse order (= last sample first), <code>
   *     false</code> otherwise.
   */
  public boolean isSamplesInReverseOrder() {
    final String rawValue = this.properties.get(DEVICE_SAMPLE_REVERSE_ORDER);
    return Boolean.parseBoolean(rawValue);
  }

  /**
   * Returns whether or not the device supports a testing mode.
   *
   * @return <code>true</code> if testing mode is supported by the device, <code>false</code>
   *     otherwise.
   */
  public boolean isTestModeSupported() {
    final String value = this.properties.get(DEVICE_FEATURE_TEST_MODE);
    return Boolean.parseBoolean(value);
  }

  /**
   * Returns whether or not the device supports triggers.
   *
   * @return <code>true</code> if the device supports triggers, <code>false</code> otherwise.
   */
  public boolean isTriggerSupported() {
    final String value = this.properties.get(DEVICE_FEATURE_TRIGGERS);
    return Boolean.parseBoolean(value);
  }

  /** {@inheritDoc} */
  @Override
  public String toString() {
    return getType();
  }

  /**
   * Returns the configuration file picked up by Felix's FileInstall bundle.
   *
   * @return a configuration file, never <code>null</code>.
   */
  final File getConfigurationFile() {
    final String value = this.properties.get(FELIX_FILEINSTALL_FILENAME);
    assert value != null : "Internal error: no fileinstall filename?!";
    return createFile(value);
  }

  /** @return the properties of this device profile, never <code>null</code>. */
  final Dictionary<String, String> getProperties() {
    return new Hashtable<String, String>(this.properties);
  }

  /** @param aProperties the updated properties. */
  @SuppressWarnings("rawtypes")
  final void setProperties(final Dictionary aProperties) {
    final Map<String, String> newProps = new HashMap<String, String>();

    Enumeration keys = aProperties.keys();
    while (keys.hasMoreElements()) {
      final String key = (String) keys.nextElement();
      if (!KNOWN_KEYS.contains(key) && !IGNORED_KEYS.contains(key)) {
        LOG.log(Level.WARNING, "Unknown/unsupported profile key: " + key);
        continue;
      }

      final String value = aProperties.get(key).toString();
      newProps.put(key, value.trim());
    }

    // Verify whether all known keys are defined...
    final List<String> checkedKeys = new ArrayList<String>(KNOWN_KEYS);
    checkedKeys.removeAll(newProps.keySet());
    if (!checkedKeys.isEmpty()) {
      throw new IllegalArgumentException(
          "Profile settings not complete! Missing keys are: " + checkedKeys.toString());
    }

    this.properties.putAll(newProps);

    LOG.log(
        Level.INFO,
        "New device profile settings applied for {1} ({0}) ...", //
        new Object[] {getType(), getDescription()});
  }
}
/**
 * The Dialog Class
 *
 * @author Frank Kunz The dialog class draws the basic dialog with a grid layout. The dialog
 *     consists of three main parts. A settings panel, a table panel and three buttons.
 */
public final class UARTProtocolAnalysisDialog
    extends BaseAsyncToolDialog<UARTDataSet, UARTAnalyserWorker> {
  // INNER TYPES

  /** Provides a combobox renderer for {@link UARTParity} values. */
  static final class UARTParityItemRenderer extends EnumItemRenderer<UARTParity> {
    // CONSTANTS

    private static final long serialVersionUID = 1L;

    // METHODS

    /**
     * @see
     *     nl.lxtreme.ols.client.diagram.settings.GeneralSettingsDialog.EnumItemRenderer#getDisplayValue(java.lang.Enum)
     */
    @Override
    protected String getDisplayValue(final UARTParity aValue) {
      String text = super.getDisplayValue(aValue);
      if (UARTParity.EVEN.equals(aValue)) {
        text = "Even parity";
      } else if (UARTParity.NONE.equals(aValue)) {
        text = "No parity";
      } else if (UARTParity.ODD.equals(aValue)) {
        text = "Odd parity";
      }
      return text;
    }
  }

  /** Provides a combobox renderer for {@link UARTStopBits} values. */
  static final class UARTStopBitsItemRenderer extends EnumItemRenderer<UARTStopBits> {
    // CONSTANTS

    private static final long serialVersionUID = 1L;

    // METHODS

    /**
     * @see
     *     nl.lxtreme.ols.client.diagram.settings.GeneralSettingsDialog.EnumItemRenderer#getDisplayValue(java.lang.Enum)
     */
    @Override
    protected String getDisplayValue(final UARTStopBits aValue) {
      String text = super.getDisplayValue(aValue);
      if (UARTStopBits.STOP_1.equals(aValue)) {
        text = "1";
      } else if (UARTStopBits.STOP_15.equals(aValue)) {
        text = "1.5";
      } else if (UARTStopBits.STOP_2.equals(aValue)) {
        text = "2";
      }
      return text;
    }
  }

  // CONSTANTS

  private static final long serialVersionUID = 1L;

  private static final Logger LOG = Logger.getLogger(UARTProtocolAnalysisDialog.class.getName());

  // VARIABLES

  private JComboBox rxd;
  private JComboBox txd;
  private JComboBox cts;
  private JComboBox rts;
  private JComboBox dtr;
  private JComboBox dsr;
  private JComboBox dcd;
  private JComboBox ri;
  private JComboBox parity;
  private JComboBox bits;
  private JComboBox stop;
  private JCheckBox inv;
  private JEditorPane outText;

  private RestorableAction runAnalysisAction;
  private Action exportAction;
  private Action closeAction;

  // CONSTRUCTORS

  /**
   * @param aOwner
   * @param aName
   */
  public UARTProtocolAnalysisDialog(final Window aOwner, final String aName) {
    super(aOwner, aName);

    initDialog();

    setLocationRelativeTo(getOwner());
  }

  // METHODS

  /**
   * This is the UART protocol decoder core The decoder scans for a decode start event like CS high
   * to low edge or the trigger of the captured data. After this the decoder starts to decode the
   * data by the selected mode, number of bits and bit order. The decoded data are put to a JTable
   * object directly.
   */
  @Override
  public void onToolWorkerReady(final UARTDataSet aAnalysisResult) {
    super.onToolWorkerReady(aAnalysisResult);

    try {
      final String htmlPage;
      if (aAnalysisResult != null) {
        htmlPage = toHtmlPage(null /* aFile */, aAnalysisResult);
        this.exportAction.setEnabled(!aAnalysisResult.isEmpty());
      } else {
        htmlPage = getEmptyHtmlPage();
        this.exportAction.setEnabled(false);
      }

      this.outText.setText(htmlPage);
      this.outText.setEditable(false);

      this.runAnalysisAction.restore();
    } catch (final IOException exception) {
      // Make sure to handle IO-interrupted exceptions properly!
      if (!HostUtils.handleInterruptedException(exception)) {
        // Should not happen in this situation!
        throw new RuntimeException(exception);
      }
    }
  }

  /** @see nl.lxtreme.ols.api.Configurable#readPreferences(nl.lxtreme.ols.api.UserSettings) */
  @Override
  public void readPreferences(final UserSettings aSettings) {
    this.rxd.setSelectedIndex(aSettings.getInt("rxd", this.rxd.getSelectedIndex()));
    this.txd.setSelectedIndex(aSettings.getInt("txd", this.txd.getSelectedIndex()));
    this.cts.setSelectedIndex(aSettings.getInt("cts", this.cts.getSelectedIndex()));
    this.rts.setSelectedIndex(aSettings.getInt("rts", this.rts.getSelectedIndex()));
    this.dtr.setSelectedIndex(aSettings.getInt("dtr", this.dtr.getSelectedIndex()));
    this.dsr.setSelectedIndex(aSettings.getInt("dsr", this.dsr.getSelectedIndex()));
    this.dcd.setSelectedIndex(aSettings.getInt("dcd", this.dcd.getSelectedIndex()));
    this.ri.setSelectedIndex(aSettings.getInt("ri", this.ri.getSelectedIndex()));
    this.parity.setSelectedIndex(aSettings.getInt("parity", this.parity.getSelectedIndex()));
    this.bits.setSelectedIndex(aSettings.getInt("bits", this.bits.getSelectedIndex()));
    this.stop.setSelectedIndex(aSettings.getInt("stop", this.stop.getSelectedIndex()));
    this.inv.setSelected(aSettings.getBoolean("inverted", this.inv.isSelected()));
  }

  /** @see nl.lxtreme.ols.tool.base.ToolDialog#reset() */
  @Override
  public void reset() {
    this.outText.setText(getEmptyHtmlPage());
    this.outText.setEditable(false);

    this.exportAction.setEnabled(false);

    this.runAnalysisAction.restore();

    setControlsEnabled(true);
  }

  /** @see nl.lxtreme.ols.api.Configurable#writePreferences(nl.lxtreme.ols.api.UserSettings) */
  @Override
  public void writePreferences(final UserSettings aSettings) {
    aSettings.putInt("rxd", this.rxd.getSelectedIndex());
    aSettings.putInt("txd", this.txd.getSelectedIndex());
    aSettings.putInt("cts", this.cts.getSelectedIndex());
    aSettings.putInt("rts", this.rts.getSelectedIndex());
    aSettings.putInt("dtr", this.dtr.getSelectedIndex());
    aSettings.putInt("dsr", this.dsr.getSelectedIndex());
    aSettings.putInt("dcd", this.dcd.getSelectedIndex());
    aSettings.putInt("ri", this.ri.getSelectedIndex());
    aSettings.putInt("parity", this.parity.getSelectedIndex());
    aSettings.putInt("bits", this.bits.getSelectedIndex());
    aSettings.putInt("stop", this.stop.getSelectedIndex());
    aSettings.putBoolean("inverted", this.inv.isSelected());
  }

  /**
   * set the controls of the dialog enabled/disabled
   *
   * @param aEnable status of the controls
   */
  @Override
  protected void setControlsEnabled(final boolean aEnable) {
    this.rxd.setEnabled(aEnable);
    this.txd.setEnabled(aEnable);
    this.cts.setEnabled(aEnable);
    this.rts.setEnabled(aEnable);
    this.dtr.setEnabled(aEnable);
    this.dsr.setEnabled(aEnable);
    this.dcd.setEnabled(aEnable);
    this.ri.setEnabled(aEnable);
    this.parity.setEnabled(aEnable);
    this.bits.setEnabled(aEnable);
    this.stop.setEnabled(aEnable);
    this.inv.setEnabled(aEnable);

    this.closeAction.setEnabled(aEnable);
  }

  /**
   * @see
   *     nl.lxtreme.ols.tool.base.BaseAsyncToolDialog#setupToolWorker(nl.lxtreme.ols.tool.base.BaseAsyncToolWorker)
   */
  @Override
  protected void setupToolWorker(final UARTAnalyserWorker aToolWorker) {
    // The value at index zero is "Unused", so extracting one of all items
    // causes all "unused" values to be equivalent to -1, which is interpreted
    // as not used...
    aToolWorker.setRxdIndex(this.rxd.getSelectedIndex() - 1);
    aToolWorker.setTxdIndex(this.txd.getSelectedIndex() - 1);
    aToolWorker.setCtsIndex(this.cts.getSelectedIndex() - 1);
    aToolWorker.setRtsIndex(this.rts.getSelectedIndex() - 1);
    aToolWorker.setDcdIndex(this.dcd.getSelectedIndex() - 1);
    aToolWorker.setRiIndex(this.ri.getSelectedIndex() - 1);
    aToolWorker.setDsrIndex(this.dsr.getSelectedIndex() - 1);
    aToolWorker.setDtrIndex(this.dtr.getSelectedIndex() - 1);

    // Other properties...
    aToolWorker.setInverted(this.inv.isSelected());
    aToolWorker.setParity((UARTParity) this.parity.getSelectedItem());
    aToolWorker.setStopBits((UARTStopBits) this.stop.getSelectedItem());
    aToolWorker.setBitCount(NumberUtils.smartParseInt((String) this.bits.getSelectedItem(), 8));
  }

  /**
   * exports the data to a CSV file
   *
   * @param aFile File object
   */
  @Override
  protected void storeToCsvFile(final File aFile, final UARTDataSet aDataSet) {
    try {
      final CsvExporter exporter = ExportUtils.createCsvExporter(aFile);

      exporter.setHeaders(
          "index",
          "start-time",
          "end-time",
          "event?",
          "event-type",
          "RxD event",
          "TxD event",
          "RxD data",
          "TxD data");

      final List<UARTData> decodedData = aDataSet.getData();
      for (int i = 0; i < decodedData.size(); i++) {
        final UARTData ds = decodedData.get(i);

        final String startTime = aDataSet.getDisplayTime(ds.getStartSampleIndex());
        final String endTime = aDataSet.getDisplayTime(ds.getEndSampleIndex());

        String eventType = null;
        String rxdEvent = null;
        String txdEvent = null;
        String rxdData = null;
        String txdData = null;

        switch (ds.getType()) {
          case UARTData.UART_TYPE_EVENT:
            eventType = ds.getEventName();
            break;

          case UARTData.UART_TYPE_RXEVENT:
            rxdEvent = ds.getEventName();
            break;

          case UARTData.UART_TYPE_TXEVENT:
            txdEvent = ds.getEventName();
            break;

          case UARTData.UART_TYPE_RXDATA:
            rxdData = Integer.toString(ds.getData());
            break;

          case UARTData.UART_TYPE_TXDATA:
            txdData = Integer.toString(ds.getData());
            break;

          default:
            break;
        }

        exporter.addRow(
            Integer.valueOf(i),
            startTime,
            endTime,
            Boolean.valueOf(ds.isEvent()),
            eventType,
            rxdEvent,
            txdEvent,
            rxdData,
            txdData);
      }

      exporter.close();
    } catch (final IOException exception) {
      // Make sure to handle IO-interrupted exceptions properly!
      if (!HostUtils.handleInterruptedException(exception)) {
        LOG.log(Level.WARNING, "CSV export failed!", exception);
      }
    }
  }

  /**
   * stores the data to a HTML file
   *
   * @param aFile file object
   */
  @Override
  protected void storeToHtmlFile(final File aFile, final UARTDataSet aDataSet) {
    try {
      toHtmlPage(aFile, aDataSet);
    } catch (final IOException exception) {
      // Make sure to handle IO-interrupted exceptions properly!
      if (!HostUtils.handleInterruptedException(exception)) {
        LOG.log(Level.WARNING, "HTML export failed!", exception);
      }
    }
  }

  /**
   * Creates the HTML template for exports to HTML.
   *
   * @param aExporter the HTML exporter instance to use, cannot be <code>null</code>.
   * @return a HTML exporter filled with the template, never <code>null</code>.
   */
  private HtmlExporter createHtmlTemplate(final HtmlExporter aExporter) {
    aExporter.addCssStyle("body { font-family: sans-serif; } ");
    aExporter.addCssStyle(
        "table { border-width: 1px; border-spacing: 0px; border-color: gray;"
            + " border-collapse: collapse; border-style: solid; margin-bottom: 15px; } ");
    aExporter.addCssStyle(
        "table th { border-width: 1px; padding: 2px; border-style: solid; border-color: gray;"
            + " background-color: #C0C0FF; text-align: left; font-weight: bold; font-family: sans-serif; } ");
    aExporter.addCssStyle(
        "table td { border-width: 1px; padding: 2px; border-style: solid; border-color: gray;"
            + " font-family: monospace; } ");
    aExporter.addCssStyle(".error { color: red; } ");
    aExporter.addCssStyle(".warning { color: orange; } ");
    aExporter.addCssStyle(".date { text-align: right; font-size: x-small; margin-bottom: 15px; } ");
    aExporter.addCssStyle(".w100 { width: 100%; } ");
    aExporter.addCssStyle(".w35 { width: 35%; } ");
    aExporter.addCssStyle(".w30 { width: 30%; } ");
    aExporter.addCssStyle(".w15 { width: 15%; } ");
    aExporter.addCssStyle(".w10 { width: 10%; } ");
    aExporter.addCssStyle(".w8 { width: 8%; } ");
    aExporter.addCssStyle(".w7 { width: 7%; } ");

    final Element body = aExporter.getBody();
    body.addChild(H1).addContent("UART Analysis results");
    body.addChild(HR);
    body.addChild(DIV).addAttribute("class", "date").addContent("Generated: ", "{date-now}");

    Element table, tr, thead, tbody;

    table = body.addChild(TABLE).addAttribute("class", "w100");

    tbody = table.addChild(TBODY);
    tr = tbody.addChild(TR);
    tr.addChild(TH).addAttribute("colspan", "2").addContent("Statistics");
    tr = tbody.addChild(TR);
    tr.addChild(TD).addAttribute("class", "w30").addContent("Decoded bytes");
    tr.addChild(TD).addContent("{decoded-bytes}");
    tr = tbody.addChild(TR);
    tr.addChild(TD).addAttribute("class", "w30").addContent("Detected bus errors");
    tr.addChild(TD).addContent("{detected-bus-errors}");
    tr = tbody.addChild(TR);
    tr.addChild(TD).addAttribute("class", "w30").addContent("Baudrate");
    tr.addChild(TD).addContent("{baudrate}");

    table = body.addChild(TABLE).addAttribute("class", "w100");
    thead = table.addChild(THEAD);
    tr = thead.addChild(TR);
    tr.addChild(TH).addAttribute("class", "w30").addAttribute("colspan", "2");
    tr.addChild(TH).addAttribute("class", "w35").addAttribute("colspan", "4").addContent("RxD");
    tr.addChild(TH).addAttribute("class", "w35").addAttribute("colspan", "4").addContent("TxD");
    tr = thead.addChild(TR);
    tr.addChild(TH).addAttribute("class", "w15").addContent("Index");
    tr.addChild(TH).addAttribute("class", "w15").addContent("Time");
    tr.addChild(TH).addAttribute("class", "w10").addContent("Hex");
    tr.addChild(TH).addAttribute("class", "w10").addContent("Bin");
    tr.addChild(TH).addAttribute("class", "w8").addContent("Dec");
    tr.addChild(TH).addAttribute("class", "w7").addContent("ASCII");
    tr.addChild(TH).addAttribute("class", "w10").addContent("Hex");
    tr.addChild(TH).addAttribute("class", "w10").addContent("Bin");
    tr.addChild(TH).addAttribute("class", "w8").addContent("Dec");
    tr.addChild(TH).addAttribute("class", "w7").addContent("ASCII");
    tbody = table.addChild(TBODY);
    tbody.addContent("{decoded-data}");

    return aExporter;
  }

  /** @return */
  private JPanel createPreviewPane() {
    final JPanel panTable = new JPanel(new GridLayout(1, 1, 0, 0));

    this.outText = new JEditorPane("text/html", getEmptyHtmlPage());
    this.outText.setEditable(false);

    panTable.add(new JScrollPane(this.outText));

    return panTable;
  }

  /** @return */
  private JPanel createSettingsPane() {
    final String channels[] = new String[33];
    channels[0] = "Unused";
    for (int i = 0; i < 32; i++) {
      channels[i + 1] = new String("Channel " + i);
    }

    final JPanel settings = new JPanel(new SpringLayout());

    SpringLayoutUtils.addSeparator(settings, "Settings");

    settings.add(createRightAlignedLabel("RxD"));
    this.rxd = new JComboBox(channels);
    this.rxd.setSelectedIndex(0);
    settings.add(this.rxd);

    settings.add(createRightAlignedLabel("TxD"));
    this.txd = new JComboBox(channels);
    this.txd.setSelectedIndex(0);
    settings.add(this.txd);

    settings.add(createRightAlignedLabel("CTS"));
    this.cts = new JComboBox(channels);
    this.cts.setSelectedIndex(0);
    settings.add(this.cts);

    settings.add(createRightAlignedLabel("RTS"));
    this.rts = new JComboBox(channels);
    this.rts.setSelectedIndex(0);
    settings.add(this.rts);

    settings.add(createRightAlignedLabel("DTR"));
    this.dtr = new JComboBox(channels);
    this.dtr.setSelectedIndex(0);
    settings.add(this.dtr);

    settings.add(createRightAlignedLabel("DSR"));
    this.dsr = new JComboBox(channels);
    this.dsr.setSelectedIndex(0);
    settings.add(this.dsr);

    settings.add(createRightAlignedLabel("DCD"));
    this.dcd = new JComboBox(channels);
    this.dcd.setSelectedIndex(0);
    settings.add(this.dcd);

    settings.add(createRightAlignedLabel("RI"));
    this.ri = new JComboBox(channels);
    this.ri.setSelectedIndex(0);
    settings.add(this.ri);

    settings.add(createRightAlignedLabel("Parity"));
    this.parity = new JComboBox(UARTParity.values());
    this.parity.setSelectedIndex(0);
    this.parity.setRenderer(new UARTParityItemRenderer());
    settings.add(this.parity);

    settings.add(createRightAlignedLabel("Bits"));
    final String[] bitarray = new String[4];
    for (int i = 0; i < bitarray.length; i++) {
      bitarray[i] = String.format("%d", Integer.valueOf(i + 5));
    }
    this.bits = new JComboBox(bitarray);
    this.bits.setSelectedIndex(3);
    settings.add(this.bits);

    settings.add(createRightAlignedLabel("Stopbits"));
    this.stop = new JComboBox(UARTStopBits.values());
    this.stop.setSelectedIndex(0);
    this.stop.setRenderer(new UARTStopBitsItemRenderer());
    settings.add(this.stop);

    this.inv = new JCheckBox();
    this.inv.setSelected(false);
    settings.add(createRightAlignedLabel("Invert?"));
    settings.add(this.inv);

    SpringLayoutUtils.makeEditorGrid(settings, 10, 4);

    return settings;
  }

  /**
   * generate a HTML page
   *
   * @param empty if this is true an empty output is generated
   * @return String with HTML data
   */
  private String getEmptyHtmlPage() {
    final HtmlExporter exporter = createHtmlTemplate(ExportUtils.createHtmlExporter());
    return exporter.toString(
        new MacroResolver() {
          @Override
          public Object resolve(final String aMacro, final Element aParent) {
            if ("date-now".equals(aMacro)) {
              final DateFormat df = DateFormat.getDateInstance(DateFormat.LONG);
              return df.format(new Date());
            } else if ("decoded-bytes".equals(aMacro)
                || "detected-bus-errors".equals(aMacro)
                || "baudrate".equals(aMacro)) {
              return "-";
            } else if ("decoded-data".equals(aMacro)) {
              return null;
            }
            return null;
          }
        });
  }

  /** Initializes this dialog. */
  private void initDialog() {
    setMinimumSize(new Dimension(640, 480));

    final JComponent settingsPane = createSettingsPane();
    final JComponent previewPane = createPreviewPane();

    final JPanel contentPane = new JPanel(new GridBagLayout());
    contentPane.add(
        settingsPane,
        new GridBagConstraints(
            0,
            0,
            1,
            1,
            0.0,
            0.0,
            GridBagConstraints.NORTH,
            GridBagConstraints.NONE,
            new Insets(2, 0, 2, 0),
            0,
            0));
    contentPane.add(
        previewPane,
        new GridBagConstraints(
            1,
            0,
            1,
            1,
            1.0,
            1.0,
            GridBagConstraints.NORTH,
            GridBagConstraints.BOTH,
            new Insets(2, 0, 2, 0),
            0,
            0));

    final JButton runAnalysisButton = createRunAnalysisButton();
    this.runAnalysisAction = (RestorableAction) runAnalysisButton.getAction();

    final JButton exportButton = createExportButton();
    this.exportAction = exportButton.getAction();
    this.exportAction.setEnabled(false);

    final JButton closeButton = createCloseButton();
    this.closeAction = closeButton.getAction();

    final JComponent buttons =
        SwingComponentUtils.createButtonPane(runAnalysisButton, exportButton, closeButton);

    SwingComponentUtils.setupDialogContentPane(this, contentPane, buttons, runAnalysisButton);
  }

  /**
   * generate a HTML page
   *
   * @param empty if this is true an empty output is generated
   * @return String with HTML data
   */
  private String toHtmlPage(final File aFile, final UARTDataSet aDataSet) throws IOException {
    final int bitCount = Integer.parseInt((String) this.bits.getSelectedItem());
    final int bitAdder = (bitCount % 4 != 0) ? 1 : 0;

    final MacroResolver macroResolver =
        new MacroResolver() {
          @Override
          public Object resolve(final String aMacro, final Element aParent) {
            if ("date-now".equals(aMacro)) {
              final DateFormat df = DateFormat.getDateInstance(DateFormat.LONG);
              return df.format(new Date());
            } else if ("decoded-bytes".equals(aMacro)) {
              return Integer.valueOf(aDataSet.getDecodedSymbols());
            } else if ("detected-bus-errors".equals(aMacro)) {
              return Integer.valueOf(aDataSet.getDetectedErrors());
            } else if ("baudrate".equals(aMacro)) {
              final String baudrate;
              if (aDataSet.getBitLength() <= 0) {
                baudrate = "<span class='error'>Baudrate calculation failed!</span>";
              } else {
                baudrate =
                    String.format(
                        "%d (exact: %d)",
                        Integer.valueOf(aDataSet.getBaudRate()),
                        Integer.valueOf(aDataSet.getBaudRateExact()));
                if (aDataSet.getBitLength() < 15) {
                  return baudrate.concat(
                      " <span class='warning'>The baudrate may be wrong, use a higher samplerate to avoid this!</span>");
                }

                return baudrate;
              }
            } else if ("decoded-data".equals(aMacro)) {
              final List<UARTData> decodedData = aDataSet.getData();
              Element tr;

              for (int i = 0; i < decodedData.size(); i++) {
                final UARTData ds = decodedData.get(i);

                if (ds.isEvent()) {
                  String rxEventData = "";
                  String txEventData = "";

                  String bgColor;
                  if (UARTData.UART_TYPE_EVENT == ds.getType()) {
                    rxEventData = txEventData = ds.getEventName();
                    bgColor = "#e0e0e0";
                  } else if (UARTData.UART_TYPE_RXEVENT == ds.getType()) {
                    rxEventData = ds.getEventName();
                    bgColor = "#c0ffc0";
                  } else if (UARTData.UART_TYPE_TXEVENT == ds.getType()) {
                    txEventData = ds.getEventName();
                    bgColor = "#c0ffc0";
                  } else {
                    // unknown event
                    bgColor = "#ff8000";
                  }

                  if (txEventData.endsWith("_ERR") || rxEventData.endsWith("_ERR")) {
                    bgColor = "#ff8000";
                  }

                  tr =
                      aParent
                          .addChild(TR)
                          .addAttribute("style", "background-color: " + bgColor + ";");
                  tr.addChild(TD).addContent(String.valueOf(i));
                  tr.addChild(TD).addContent(aDataSet.getDisplayTime(ds.getStartSampleIndex()));
                  tr.addChild(TD).addContent(rxEventData);
                  tr.addChild(TD);
                  tr.addChild(TD);
                  tr.addChild(TD);
                  tr.addChild(TD).addContent(txEventData);
                  tr.addChild(TD);
                  tr.addChild(TD);
                  tr.addChild(TD);
                } else {
                  String rxDataHex = "", rxDataBin = "", rxDataDec = "", rxDataASCII = "";
                  String txDataHex = "", txDataBin = "", txDataDec = "", txDataASCII = "";

                  // Normal data...
                  if (UARTData.UART_TYPE_RXDATA == ds.getType()) {
                    final int rxData = ds.getData();

                    rxDataHex =
                        "0x" + DisplayUtils.integerToHexString(rxData, bitCount / 4 + bitAdder);
                    rxDataBin = "0b" + DisplayUtils.integerToBinString(rxData, bitCount);
                    rxDataDec = String.valueOf(rxData);
                    if ((rxData >= 32) && (bitCount == 8)) {
                      rxDataASCII = String.valueOf((char) rxData);
                    }
                  } else
                  /* if ( UARTData.UART_TYPE_TXDATA == ds.getType() ) */
                  {
                    final int txData = ds.getData();

                    txDataHex =
                        "0x" + DisplayUtils.integerToHexString(txData, bitCount / 4 + bitAdder);
                    txDataBin = "0b" + DisplayUtils.integerToBinString(txData, bitCount);
                    txDataDec = String.valueOf(txData);
                    if ((txData >= 32) && (bitCount == 8)) {
                      txDataASCII = String.valueOf((char) txData);
                    }
                  }

                  tr = aParent.addChild(TR);
                  tr.addChild(TD).addContent(String.valueOf(i));
                  tr.addChild(TD).addContent(aDataSet.getDisplayTime(ds.getStartSampleIndex()));
                  tr.addChild(TD).addContent(rxDataHex);
                  tr.addChild(TD).addContent(rxDataBin);
                  tr.addChild(TD).addContent(rxDataDec);
                  tr.addChild(TD).addContent(rxDataASCII);
                  tr.addChild(TD).addContent(txDataHex);
                  tr.addChild(TD).addContent(txDataBin);
                  tr.addChild(TD).addContent(txDataDec);
                  tr.addChild(TD).addContent(txDataASCII);
                }
              }
            }
            return null;
          }
        };

    if (aFile == null) {
      final HtmlExporter exporter = createHtmlTemplate(ExportUtils.createHtmlExporter());
      return exporter.toString(macroResolver);
    } else {
      final HtmlFileExporter exporter =
          (HtmlFileExporter) createHtmlTemplate(ExportUtils.createHtmlExporter(aFile));
      exporter.write(macroResolver);
      exporter.close();
    }

    return null;
  }
}