package org.hihan.girinoscope.ui; import gnu.io.CommPortIdentifier; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.logging.ConsoleHandler; import java.util.logging.Level; import java.util.logging.Logger; import java.util.logging.SimpleFormatter; import javax.swing.AbstractAction; import javax.swing.AbstractButton; import javax.swing.Action; import javax.swing.ButtonGroup; import javax.swing.JCheckBoxMenuItem; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JToolBar; import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import javax.swing.UIManager; import javax.swing.UIManager.LookAndFeelInfo; import javax.swing.WindowConstants; import org.hihan.girinoscope.Native; import org.hihan.girinoscope.comm.Girino; import org.hihan.girinoscope.comm.Girino.Parameter; import org.hihan.girinoscope.comm.Girino.PrescalerInfo; import org.hihan.girinoscope.comm.Girino.TriggerEventMode; import org.hihan.girinoscope.comm.Girino.VoltageReference; import org.hihan.girinoscope.comm.Serial; import org.hihan.girinoscope.ui.images.Icon; @SuppressWarnings("serial") public class UI extends JFrame { private static final Logger logger = Logger.getLogger(UI.class.getName()); public static void main(String[] args) throws Exception { Logger rootLogger = Logger.getLogger("org.hihan.girinoscope"); rootLogger.setLevel(Level.WARNING); ConsoleHandler handler = new ConsoleHandler(); handler.setFormatter(new SimpleFormatter()); handler.setLevel(Level.ALL); rootLogger.addHandler(handler); SwingUtilities.invokeAndWait(new Runnable() { public void run() { Native.setBestLookAndFeel(); JFrame frame = new UI(); frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); frame.pack(); frame.setLocationRelativeTo(null); frame.setVisible(true); } }); } private Girino girino = new Girino(); private CommPortIdentifier portId; private Map<Parameter, Integer> parameters = Girino.getDefaultParameters(new HashMap<Parameter, Integer>()); private GraphPane graphPane; private StatusBar statusBar; private DataAcquisitionTask currentDataAcquisitionTask; private ExecutorService executor = Executors.newSingleThreadExecutor(); private class DataAcquisitionTask extends SwingWorker<Void, byte[]> { private CommPortIdentifier frozenPortId; private Map<Parameter, Integer> frozenParameters = new HashMap<Parameter, Integer>(); public DataAcquisitionTask() { startAcquiringAction.setEnabled(false); stopAcquiringAction.setEnabled(true); } @Override protected Void doInBackground() throws Exception { while (!isCancelled()) { updateConnection(); acquireData(); } return null; } private void updateConnection() throws Exception { synchronized (UI.this) { frozenPortId = portId; frozenParameters.putAll(parameters); } setStatus("blue", "Contacting Girino on %s...", frozenPortId.getName()); Future<Void> connection = executor.submit(new Callable<Void>() { @Override public Void call() throws Exception { girino.setConnection(frozenPortId, frozenParameters); return null; } }); try { connection.get(5, TimeUnit.SECONDS); } catch (TimeoutException e) { throw new TimeoutException("No Girino detected on " + frozenPortId.getName()); } catch (InterruptedException e) { connection.cancel(true); throw e; } } private void acquireData() throws Exception { setStatus("blue", "Acquiring data from %s...", frozenPortId.getName()); Future<byte[]> acquisition = null; boolean terminated; do { boolean updateConnection; synchronized (UI.this) { parameters.put(Parameter.THRESHOLD, graphPane.getThreshold()); parameters.put(Parameter.WAIT_DURATION, graphPane.getWaitDuration()); updateConnection = !getChanges(frozenParameters).isEmpty() || frozenPortId != portId; } if (updateConnection) { if (acquisition != null) { acquisition.cancel(true); } terminated = true; } else { try { if (acquisition == null) { acquisition = executor.submit(new Callable<byte[]>() { @Override public byte[] call() throws Exception { return girino.acquireData(); } }); } byte[] buffer = acquisition.get(1, TimeUnit.SECONDS); if (buffer != null) { publish(buffer); acquisition = null; terminated = false; } else { terminated = true; } } catch (TimeoutException e) { // Just to wake up regularly. terminated = false; } catch (InterruptedException e) { acquisition.cancel(true); throw e; } } } while (!terminated); } @Override protected void process(List<byte[]> buffer) { logger.log(Level.FINE, "{0} data buffer(s) to display.", buffer.size()); graphPane.setData(buffer.get(buffer.size() - 1)); } @Override protected void done() { startAcquiringAction.setEnabled(true); stopAcquiringAction.setEnabled(false); try { if (!isCancelled()) { get(); } setStatus("blue", "Done acquiring data from %s.", frozenPortId.getName()); } catch (ExecutionException e) { setStatus("red", e.getCause().getMessage()); } catch (Exception e) { setStatus("red", e.getMessage()); } } } private final Action startAcquiringAction = new AbstractAction("Start acquiring", Icon.get("media-record.png")) { { putValue(Action.SHORT_DESCRIPTION, "Start acquiring data from Girino."); } @Override public void actionPerformed(ActionEvent event) { synchronized (UI.this) { parameters.put(Parameter.THRESHOLD, graphPane.getThreshold()); parameters.put(Parameter.WAIT_DURATION, graphPane.getWaitDuration()); } currentDataAcquisitionTask = new DataAcquisitionTask(); currentDataAcquisitionTask.execute(); } }; private final Action stopAcquiringAction = new AbstractAction("Stop acquiring", Icon.get("media-playback-stop.png")) { { putValue(Action.SHORT_DESCRIPTION, "Stop acquiring data from Girino."); } @Override public void actionPerformed(ActionEvent event) { currentDataAcquisitionTask.cancel(true); } }; private final Action aboutAction = new AbstractAction("About Girinoscope", Icon.get("help-about.png")) { @Override public void actionPerformed(ActionEvent event) { new AboutDialog(UI.this).setVisible(true); } }; private final Action exitAction = new AbstractAction("Quit", Icon.get("application-exit.png")) { @Override public void actionPerformed(ActionEvent event) { dispose(); } }; public UI() { setTitle("Girinoscope"); setIconImage(Icon.getImage("icon.png")); setLayout(new BorderLayout()); graphPane = new GraphPane(parameters.get(Parameter.THRESHOLD), parameters.get(Parameter.WAIT_DURATION)); graphPane.setPreferredSize(new Dimension(800, 600)); add(graphPane, BorderLayout.CENTER); setJMenuBar(createMenuBar()); add(createToolBar(), BorderLayout.NORTH); statusBar = new StatusBar(); add(statusBar, BorderLayout.SOUTH); stopAcquiringAction.setEnabled(false); if (portId != null) { startAcquiringAction.setEnabled(true); } else { startAcquiringAction.setEnabled(false); setStatus("red", "No USB to serial adaptation port detected."); } } @Override public void dispose() { try { if (currentDataAcquisitionTask != null) { currentDataAcquisitionTask.cancel(true); } executor.shutdown(); girino.disconnect(); } catch (IOException e) { logger.log(Level.WARNING, "When disconnecting from Girino.", e); } super.dispose(); } private JMenuBar createMenuBar() { JMenuBar menuBar = new JMenuBar(); JMenu fileMenu = new JMenu("File"); fileMenu.add(exitAction); menuBar.add(fileMenu); JMenu toolMenu = new JMenu("Tools"); toolMenu.add(createSerialMenu()); toolMenu.add(createPrescalerMenu()); toolMenu.add(createTriggerEventMenu()); toolMenu.add(createVoltageReferenceMenu()); toolMenu.addSeparator(); toolMenu.add(createDataStrokeWidthMenu()); toolMenu.add(createThemeMenu()); menuBar.add(toolMenu); JMenu helpMenu = new JMenu("Help"); helpMenu.add(aboutAction); menuBar.add(helpMenu); return menuBar; } private JMenu createSerialMenu() { JMenu menu = new JMenu("Serial port"); ButtonGroup group = new ButtonGroup(); for (final CommPortIdentifier portId : Serial.enumeratePorts()) { Action setSerialPort = new AbstractAction(portId.getName()) { @Override public void actionPerformed(ActionEvent event) { UI.this.portId = portId; } }; AbstractButton button = new JCheckBoxMenuItem(setSerialPort); if (UI.this.portId == null) { button.doClick(); } group.add(button); menu.add(button); } return menu; } private JMenu createPrescalerMenu() { JMenu menu = new JMenu("Acquisition rate / Time frame"); ButtonGroup group = new ButtonGroup(); for (final PrescalerInfo info : PrescalerInfo.values()) { Action setPrescaler = new AbstractAction(info.description) { @Override public void actionPerformed(ActionEvent event) { synchronized (UI.this) { parameters.put(Parameter.PRESCALER, info.value); } String xFormat = info.timeframe > 0.005 ? "%.0f ms" : "%.1f ms"; Axis xAxis = new Axis(0, info.timeframe * 1000, xFormat); Axis yAxis = new Axis(-2.5, 2.5, 0.5, "%.2f V"); graphPane.setCoordinateSystem(xAxis, yAxis); } }; AbstractButton button = new JCheckBoxMenuItem(setPrescaler); if (info.reallyTooFast) { button.setForeground(Color.RED.darker()); } else if (info.tooFast) { button.setForeground(Color.ORANGE.darker()); } if (info.value == parameters.get(Parameter.PRESCALER)) { button.doClick(); } group.add(button); menu.add(button); } return menu; } private JMenu createTriggerEventMenu() { JMenu menu = new JMenu("Trigger event mode"); ButtonGroup group = new ButtonGroup(); for (final TriggerEventMode mode : TriggerEventMode.values()) { Action setPrescaler = new AbstractAction(mode.description) { @Override public void actionPerformed(ActionEvent event) { synchronized (UI.this) { parameters.put(Parameter.TRIGGER_EVENT, mode.value); } } }; AbstractButton button = new JCheckBoxMenuItem(setPrescaler); if (mode.value == parameters.get(Parameter.TRIGGER_EVENT)) { button.doClick(); } group.add(button); menu.add(button); } return menu; } private JMenu createVoltageReferenceMenu() { JMenu menu = new JMenu("Voltage reference"); ButtonGroup group = new ButtonGroup(); for (final VoltageReference reference : VoltageReference.values()) { Action setPrescaler = new AbstractAction(reference.description) { @Override public void actionPerformed(ActionEvent event) { synchronized (UI.this) { parameters.put(Parameter.VOLTAGE_REFERENCE, reference.value); } } }; AbstractButton button = new JCheckBoxMenuItem(setPrescaler); if (reference.value == parameters.get(Parameter.VOLTAGE_REFERENCE)) { button.doClick(); } group.add(button); menu.add(button); } return menu; } private JMenu createThemeMenu() { JMenu menu = new JMenu("Theme"); ButtonGroup group = new ButtonGroup(); for (final LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { Action setLnF = new AbstractAction(info.getName()) { @Override public void actionPerformed(ActionEvent event) { try { UIManager.setLookAndFeel(info.getClassName()); SwingUtilities.updateComponentTreeUI(getRootPane()); } catch (Exception e) { setStatus("red", "Failed to load {} LaF.", info.getName()); } } }; AbstractButton button = new JCheckBoxMenuItem(setLnF); group.add(button); menu.add(button); } return menu; } private JMenu createDataStrokeWidthMenu() { JMenu menu = new JMenu("Data stroke width"); ButtonGroup group = new ButtonGroup(); for (final int width : new int[] { 1, 2, 3 }) { Action setStrokeWidth = new AbstractAction(width + " px") { @Override public void actionPerformed(ActionEvent event) { graphPane.setDataStrokeWidth(width); } }; AbstractButton button = new JCheckBoxMenuItem(setStrokeWidth); if (width == 1) { button.doClick(); } group.add(button); menu.add(button); } return menu; } private JComponent createToolBar() { JToolBar toolBar = new JToolBar(); toolBar.setFloatable(false); final Component start = toolBar.add(startAcquiringAction); final Component stop = toolBar.add(stopAcquiringAction); start.addPropertyChangeListener("enabled", new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if (!start.isEnabled()) { stop.requestFocusInWindow(); } } }); stop.addPropertyChangeListener("enabled", new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if (!stop.isEnabled()) { start.requestFocusInWindow(); } } }); return toolBar; } private void setStatus(String color, String message, Object... arguments) { String formattedMessage = String.format(message != null ? message : "", arguments); final String htmlMessage = String.format("<html><font color=%s>%s</color></html>", color, formattedMessage); if (SwingUtilities.isEventDispatchThread()) { statusBar.setText(htmlMessage); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { statusBar.setText(htmlMessage); } }); } } private Map<Parameter, Integer> getChanges(Map<Parameter, Integer> oldParameters) { Map<Parameter, Integer> changes = new HashMap<Parameter, Integer>(); for (Map.Entry<Parameter, Integer> entry : parameters.entrySet()) { Parameter parameter = entry.getKey(); Integer newValue = entry.getValue(); if (!same(newValue, oldParameters.get(parameter))) { changes.put(parameter, newValue); } } for (Map.Entry<Parameter, Integer> entry : oldParameters.entrySet()) { Parameter parameter = entry.getKey(); if (!parameters.containsKey(parameter)) { changes.put(parameter, null); } } return changes; } private static boolean same(Object o1, Object o2) { if (o1 == o2) { return true; } else if (o1 == null || o2 == null) { return false; } else { return o1.equals(o2); } } }