From 0f5eaac97e9b1edb88f7316d142c80addbf25f06 Mon Sep 17 00:00:00 2001 From: Henning Pohl Date: Thu, 10 Jul 2014 19:01:56 +0200 Subject: [PATCH] Adding plotting functionality to the editor --- app/src/processing/app/AbstractMonitor.java | 139 +--------- .../processing/app/AbstractTextMonitor.java | 165 ++++++++++++ app/src/processing/app/Editor.java | 155 ++++++++++- app/src/processing/app/EditorToolbar.java | 32 ++- app/src/processing/app/NetworkMonitor.java | 2 +- app/src/processing/app/SerialMonitor.java | 2 +- app/src/processing/app/SerialPlotter.java | 242 ++++++++++++++++++ .../app/helpers/CircularBuffer.java | 81 ++++++ app/src/processing/app/helpers/Ticks.java | 46 ++++ build/shared/lib/theme/buttons.gif | Bin 3331 -> 3780 bytes 10 files changed, 722 insertions(+), 142 deletions(-) create mode 100644 app/src/processing/app/AbstractTextMonitor.java create mode 100644 app/src/processing/app/SerialPlotter.java create mode 100644 app/src/processing/app/helpers/CircularBuffer.java create mode 100644 app/src/processing/app/helpers/Ticks.java diff --git a/app/src/processing/app/AbstractMonitor.java b/app/src/processing/app/AbstractMonitor.java index 710e8611576..1b07460b90c 100644 --- a/app/src/processing/app/AbstractMonitor.java +++ b/app/src/processing/app/AbstractMonitor.java @@ -3,6 +3,7 @@ import static processing.app.I18n._; import java.awt.BorderLayout; +import java.awt.Container; import java.awt.Dimension; import java.awt.Font; import java.awt.Rectangle; @@ -15,15 +16,10 @@ import javax.swing.AbstractAction; import javax.swing.Box; import javax.swing.BoxLayout; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.Timer; @@ -37,19 +33,11 @@ @SuppressWarnings("serial") public abstract class AbstractMonitor extends JFrame implements ActionListener { - protected final JLabel noLineEndingAlert; - protected TextAreaFIFO textArea; - protected JScrollPane scrollPane; - protected JTextField textField; - protected JButton sendButton; - protected JCheckBox autoscrollBox; - protected JComboBox lineEndings; - protected JComboBox serialRates; private boolean monitorEnabled; private boolean closed; - private Timer updateTimer; private StringBuffer updateBuffer; + private Timer updateTimer; private BoardPort boardPort; @@ -82,84 +70,10 @@ public void actionPerformed(ActionEvent event) { } })); - getContentPane().setLayout(new BorderLayout()); - - Font consoleFont = Theme.getFont("console.font"); - Font editorFont = PreferencesData.getFont("editor.font"); - Font font = new Font(consoleFont.getName(), consoleFont.getStyle(), editorFont.getSize()); - - textArea = new TextAreaFIFO(8000000); - textArea.setRows(16); - textArea.setColumns(40); - textArea.setEditable(false); - textArea.setFont(font); - - // don't automatically update the caret. that way we can manually decide - // whether or not to do so based on the autoscroll checkbox. - ((DefaultCaret) textArea.getCaret()).setUpdatePolicy(DefaultCaret.NEVER_UPDATE); - - scrollPane = new JScrollPane(textArea); - - getContentPane().add(scrollPane, BorderLayout.CENTER); - - JPanel upperPane = new JPanel(); - upperPane.setLayout(new BoxLayout(upperPane, BoxLayout.X_AXIS)); - upperPane.setBorder(new EmptyBorder(4, 4, 4, 4)); - - textField = new JTextField(40); - sendButton = new JButton(_("Send")); - - upperPane.add(textField); - upperPane.add(Box.createRigidArea(new Dimension(4, 0))); - upperPane.add(sendButton); - - getContentPane().add(upperPane, BorderLayout.NORTH); - - final JPanel pane = new JPanel(); - pane.setLayout(new BoxLayout(pane, BoxLayout.X_AXIS)); - pane.setBorder(new EmptyBorder(4, 4, 4, 4)); - - autoscrollBox = new JCheckBox(_("Autoscroll"), true); - - noLineEndingAlert = new JLabel(I18n.format(_("You've pressed {0} but nothing was sent. Should you select a line ending?"), _("Send"))); - noLineEndingAlert.setToolTipText(noLineEndingAlert.getText()); - noLineEndingAlert.setForeground(pane.getBackground()); - Dimension minimumSize = new Dimension(noLineEndingAlert.getMinimumSize()); - minimumSize.setSize(minimumSize.getWidth() / 3, minimumSize.getHeight()); - noLineEndingAlert.setMinimumSize(minimumSize); - - lineEndings = new JComboBox(new String[]{_("No line ending"), _("Newline"), _("Carriage return"), _("Both NL & CR")}); - lineEndings.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent event) { - PreferencesData.setInteger("serial.line_ending", lineEndings.getSelectedIndex()); - noLineEndingAlert.setForeground(pane.getBackground()); - } - }); - if (PreferencesData.get("serial.line_ending") != null) { - lineEndings.setSelectedIndex(PreferencesData.getInteger("serial.line_ending")); - } - lineEndings.setMaximumSize(lineEndings.getMinimumSize()); - - String[] serialRateStrings = {"300", "1200", "2400", "4800", "9600", "19200", "38400", "57600", "115200", "230400", "250000"}; - - serialRates = new JComboBox(); - for (String rate : serialRateStrings) { - serialRates.addItem(rate + " " + _("baud")); - } - - serialRates.setMaximumSize(serialRates.getMinimumSize()); - - pane.add(autoscrollBox); - pane.add(Box.createHorizontalGlue()); - pane.add(noLineEndingAlert); - pane.add(Box.createRigidArea(new Dimension(8, 0))); - pane.add(lineEndings); - pane.add(Box.createRigidArea(new Dimension(8, 0))); - pane.add(serialRates); - this.setMinimumSize(new Dimension(pane.getMinimumSize().width, this.getPreferredSize().height)); + onCreateWindow(getContentPane()); - getContentPane().add(pane, BorderLayout.SOUTH); + this.setMinimumSize(new Dimension(getContentPane().getMinimumSize().width, this.getPreferredSize().height)); pack(); @@ -185,18 +99,15 @@ public void actionPerformed(ActionEvent event) { monitorEnabled = true; closed = false; } + + protected abstract void onCreateWindow(Container mainPane); public void enableWindow(boolean enable) { - textArea.setEnabled(enable); - scrollPane.setEnabled(enable); - textField.setEnabled(enable); - sendButton.setEnabled(enable); - autoscrollBox.setEnabled(enable); - lineEndings.setEnabled(enable); - serialRates.setEnabled(enable); - + onEnableWindow(enable); monitorEnabled = enable; } + + protected abstract void onEnableWindow(boolean enable); // Puts the window in suspend state, closing the serial port // to allow other entity (the programmer) to use it @@ -220,15 +131,6 @@ public void resume(BoardPort boardPort) throws Exception { open(); } - public void onSerialRateChange(ActionListener listener) { - serialRates.addActionListener(listener); - } - - public void onSendCommand(ActionListener listener) { - textField.addActionListener(listener); - sendButton.addActionListener(listener); - } - protected void setPlacement(int[] location) { setBounds(location[0], location[1], location[2], location[3]); } @@ -246,16 +148,7 @@ protected int[] getPlacement() { return location; } - public void message(final String s) { - SwingUtilities.invokeLater(new Runnable() { - public void run() { - textArea.append(s); - if (autoscrollBox.isSelected()) { - textArea.setCaretPosition(textArea.getDocument().getLength()); - } - } - }); - } + public abstract void message(final String s); public boolean requiresAuthorization() { return false; @@ -295,21 +188,13 @@ private synchronized String consumeUpdateBuffer() { updateBuffer.setLength(0); return s; } - + public void actionPerformed(ActionEvent e) { String s = consumeUpdateBuffer(); - if (s.isEmpty()) { return; - } - - //System.out.println("gui append " + s.length()); - if (autoscrollBox.isSelected()) { - textArea.appendTrim(s); - textArea.setCaretPosition(textArea.getDocument().getLength()); } else { - textArea.appendNoTrim(s); + message(s); } } - } diff --git a/app/src/processing/app/AbstractTextMonitor.java b/app/src/processing/app/AbstractTextMonitor.java new file mode 100644 index 00000000000..09aba8c5b9d --- /dev/null +++ b/app/src/processing/app/AbstractTextMonitor.java @@ -0,0 +1,165 @@ +package processing.app; + +import static processing.app.I18n._; + +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import javax.swing.AbstractAction; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.KeyStroke; +import javax.swing.SwingUtilities; +import javax.swing.border.EmptyBorder; +import javax.swing.text.DefaultCaret; + +import cc.arduino.packages.BoardPort; +import processing.app.debug.TextAreaFIFO; +import processing.app.legacy.PApplet; + +@SuppressWarnings("serial") +public abstract class AbstractTextMonitor extends AbstractMonitor { + + protected JLabel noLineEndingAlert; + protected TextAreaFIFO textArea; + protected JScrollPane scrollPane; + protected JTextField textField; + protected JButton sendButton; + protected JCheckBox autoscrollBox; + protected JComboBox lineEndings; + protected JComboBox serialRates; + + public AbstractTextMonitor(BoardPort boardPort) { + super(boardPort); + } + + protected void onCreateWindow(Container mainPane) { + Font consoleFont = Theme.getFont("console.font"); + Font editorFont = PreferencesData.getFont("editor.font"); + Font font = new Font(consoleFont.getName(), consoleFont.getStyle(), editorFont.getSize()); + + mainPane.setLayout(new BorderLayout()); + + textArea = new TextAreaFIFO(8000000); + textArea.setRows(16); + textArea.setColumns(40); + textArea.setEditable(false); + textArea.setFont(font); + + // don't automatically update the caret. that way we can manually decide + // whether or not to do so based on the autoscroll checkbox. + ((DefaultCaret) textArea.getCaret()).setUpdatePolicy(DefaultCaret.NEVER_UPDATE); + + scrollPane = new JScrollPane(textArea); + + mainPane.add(scrollPane, BorderLayout.CENTER); + + JPanel upperPane = new JPanel(); + upperPane.setLayout(new BoxLayout(upperPane, BoxLayout.X_AXIS)); + upperPane.setBorder(new EmptyBorder(4, 4, 4, 4)); + + textField = new JTextField(40); + sendButton = new JButton(_("Send")); + + upperPane.add(textField); + upperPane.add(Box.createRigidArea(new Dimension(4, 0))); + upperPane.add(sendButton); + + mainPane.add(upperPane, BorderLayout.NORTH); + + final JPanel pane = new JPanel(); + pane.setLayout(new BoxLayout(pane, BoxLayout.X_AXIS)); + pane.setBorder(new EmptyBorder(4, 4, 4, 4)); + + autoscrollBox = new JCheckBox(_("Autoscroll"), true); + + noLineEndingAlert = new JLabel(I18n.format(_("You've pressed {0} but nothing was sent. Should you select a line ending?"), _("Send"))); + noLineEndingAlert.setToolTipText(noLineEndingAlert.getText()); + noLineEndingAlert.setForeground(pane.getBackground()); + Dimension minimumSize = new Dimension(noLineEndingAlert.getMinimumSize()); + minimumSize.setSize(minimumSize.getWidth() / 3, minimumSize.getHeight()); + noLineEndingAlert.setMinimumSize(minimumSize); + + lineEndings = new JComboBox(new String[]{_("No line ending"), _("Newline"), _("Carriage return"), _("Both NL & CR")}); + lineEndings.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent event) { + PreferencesData.setInteger("serial.line_ending", lineEndings.getSelectedIndex()); + noLineEndingAlert.setForeground(pane.getBackground()); + } + }); + if (PreferencesData.get("serial.line_ending") != null) { + lineEndings.setSelectedIndex(PreferencesData.getInteger("serial.line_ending")); + } + lineEndings.setMaximumSize(lineEndings.getMinimumSize()); + + String[] serialRateStrings = { + "300", "1200", "2400", "4800", "9600", + "19200", "38400", "57600", "115200", "230400", "250000" + }; + + serialRates = new JComboBox(); + for (String rate : serialRateStrings) { + serialRates.addItem(rate + " " + _("baud")); + } + + serialRates.setMaximumSize(serialRates.getMinimumSize()); + + pane.add(autoscrollBox); + pane.add(Box.createHorizontalGlue()); + pane.add(noLineEndingAlert); + pane.add(Box.createRigidArea(new Dimension(8, 0))); + pane.add(lineEndings); + pane.add(Box.createRigidArea(new Dimension(8, 0))); + pane.add(serialRates); + + mainPane.add(pane, BorderLayout.SOUTH); + } + + protected void onEnableWindow(boolean enable) + { + textArea.setEnabled(enable); + scrollPane.setEnabled(enable); + textField.setEnabled(enable); + sendButton.setEnabled(enable); + autoscrollBox.setEnabled(enable); + lineEndings.setEnabled(enable); + serialRates.setEnabled(enable); + } + + public void onSendCommand(ActionListener listener) { + textField.addActionListener(listener); + sendButton.addActionListener(listener); + } + + public void onSerialRateChange(ActionListener listener) { + serialRates.addActionListener(listener); + } + + public void message(final String s) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + textArea.append(s); + if (autoscrollBox.isSelected()) { + textArea.setCaretPosition(textArea.getDocument().getLength()); + } + } + }); + } +} \ No newline at end of file diff --git a/app/src/processing/app/Editor.java b/app/src/processing/app/Editor.java index 182b799eed1..54b29b5a9e4 100644 --- a/app/src/processing/app/Editor.java +++ b/app/src/processing/app/Editor.java @@ -137,7 +137,8 @@ public boolean apply(Sketch sketch) { static JMenu serialMenu; static AbstractMonitor serialMonitor; - + static AbstractMonitor serialPlotter; + EditorHeader header; EditorStatus status; EditorConsole console; @@ -235,7 +236,7 @@ public void windowDeactivated(WindowEvent e) { //PdeKeywords keywords = new PdeKeywords(); //sketchbook = new Sketchbook(this); - + buildMenuBar(); // For rev 0120, placing things inside a JPanel @@ -766,6 +767,14 @@ public void actionPerformed(ActionEvent e) { }); toolsMenu.add(item); + item = newJMenuItemShift(_("Serial Plotter"), 'L'); + item.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + handlePlotter(); + } + }); + toolsMenu.add(item); + addTools(toolsMenu, BaseNoGui.getToolsFolder()); File sketchbookTools = new File(BaseNoGui.getSketchbookFolder(), "tools"); addTools(toolsMenu, sketchbookTools); @@ -1108,6 +1117,7 @@ protected void selectSerialPort(String name) { } if (selection != null) selection.setState(true); //System.out.println(item.getLabel()); + BaseNoGui.selectSerialPort(name); if (serialMonitor != null) { try { @@ -1118,6 +1128,16 @@ protected void selectSerialPort(String name) { } } + if (serialPlotter != null) { + try { + serialPlotter.close(); + serialPlotter.setVisible(false); + } catch (Exception e) { + // ignore + } + } + + onBoardOrPortChange(); base.onBoardOrPortChange(); //System.out.println("set to " + get("serial.port")); @@ -2523,6 +2543,9 @@ public void run() { if (serialMonitor != null) { serialMonitor.suspend(); } + if (serialPlotter != null) { + serialPlotter.suspend(); + } uploading = true; @@ -2556,6 +2579,7 @@ public void run() { toolbar.deactivate(EditorToolbar.EXPORT); resumeOrCloseSerialMonitor(); + resumeOrCloseSerialPlotter(); base.onBoardOrPortChange(); } } @@ -2565,6 +2589,8 @@ private void resumeOrCloseSerialMonitor() { if (serialMonitor != null) { BoardPort boardPort = BaseNoGui.getDiscoveryManager().find(PreferencesData.get("serial.port")); try { + if (serialMonitor != null) + serialMonitor.resume(boardPort); if (boardPort == null) { serialMonitor.close(); handleSerial(); @@ -2574,7 +2600,26 @@ private void resumeOrCloseSerialMonitor() { } catch (Exception e) { statusError(e); } - } + } + } + + private void resumeOrCloseSerialPlotter() { + // Return the serial plotter window to its initial state + if (serialPlotter != null) { + BoardPort boardPort = BaseNoGui.getDiscoveryManager().find(PreferencesData.get("serial.port")); + try { + if (serialPlotter != null) + serialPlotter.resume(boardPort); + if (boardPort == null) { + serialPlotter.close(); + handlePlotter(); + } else { + serialPlotter.resume(boardPort); + } + } catch (Exception e) { + statusError(e); + } + } } // DAM: in Arduino, this is upload (with verbose output) @@ -2585,6 +2630,9 @@ public void run() { if (serialMonitor != null) { serialMonitor.suspend(); } + if (serialPlotter != null) { + serialPlotter.suspend(); + } uploading = true; @@ -2618,6 +2666,8 @@ public void run() { toolbar.deactivate(EditorToolbar.EXPORT); resumeOrCloseSerialMonitor(); + resumeOrCloseSerialPlotter(); + base.onBoardOrPortChange(); } } @@ -2658,6 +2708,15 @@ protected boolean handleExportCheckModified() { public void handleSerial() { + if(serialPlotter != null) { + if(serialPlotter.isClosed()) { + serialPlotter = null; + } else { + statusError(I18n.format("Serial monitor not available while plotter is open")); + return; + } + } + if (serialMonitor != null) { // The serial monitor already exists @@ -2738,7 +2797,97 @@ public void handleSerial() { } while (serialMonitor.requiresAuthorization() && !success); } + + public void handlePlotter() { + if(serialMonitor != null) { + if(serialMonitor.isClosed()) { + serialMonitor = null; + } else { + statusError(I18n.format("Plotter not available while serial monitor is open")); + return; + } + } + + if (serialPlotter != null) { + // The serial plotter already exists + + if (serialPlotter.isClosed()) { + // If it's closed, clear the refrence to the existing + // plotter and create a new one + serialPlotter = null; + } + else { + // If it's not closed, give it the focus + try { + serialPlotter.toFront(); + serialPlotter.requestFocus(); + return; + } catch (Exception e) { + // noop + } + } + } + + BoardPort port = Base.getDiscoveryManager().find(PreferencesData.get("serial.port")); + + if (port == null) { + statusError(I18n.format("Board at {0} is not available", PreferencesData.get("serial.port"))); + return; + } + + serialPlotter = new SerialPlotter(port); + serialPlotter.setIconImage(getIconImage()); + + // If currently uploading, disable the plotter (it will be later + // enabled when done uploading) + if (uploading) { + try { + serialPlotter.suspend(); + } catch (Exception e) { + statusError(e); + } + } + boolean success = false; + do { + if (serialPlotter.requiresAuthorization() && !PreferencesData.has(serialPlotter.getAuthorizationKey())) { + PasswordAuthorizationDialog dialog = new PasswordAuthorizationDialog(this, _("Type board password to access its console")); + dialog.setLocationRelativeTo(this); + dialog.setVisible(true); + + if (dialog.isCancelled()) { + statusNotice(_("Unable to open serial plotter")); + return; + } + + PreferencesData.set(serialPlotter.getAuthorizationKey(), dialog.getPassword()); + } + + try { + serialPlotter.open(); + serialPlotter.setVisible(true); + success = true; + } catch (ConnectException e) { + statusError(_("Unable to connect: is the sketch using the bridge?")); + } catch (JSchException e) { + statusError(_("Unable to connect: wrong password?")); + } catch (SerialException e) { + String errorMessage = e.getMessage(); + if (e.getCause() != null && e.getCause() instanceof SerialPortException) { + errorMessage += " (" + ((SerialPortException) e.getCause()).getExceptionType() + ")"; + } + statusError(errorMessage); + } catch (Exception e) { + statusError(e); + } finally { + if (serialPlotter.requiresAuthorization() && !success) { + PreferencesData.remove(serialPlotter.getAuthorizationKey()); + } + } + + } while (serialPlotter.requiresAuthorization() && !success); + + } protected void handleBurnBootloader() { console.clear(); diff --git a/app/src/processing/app/EditorToolbar.java b/app/src/processing/app/EditorToolbar.java index d007ed72413..06b1c7e31c1 100644 --- a/app/src/processing/app/EditorToolbar.java +++ b/app/src/processing/app/EditorToolbar.java @@ -38,12 +38,12 @@ public class EditorToolbar extends JComponent implements MouseInputListener, Key /** Rollover titles for each button. */ static final String title[] = { - _("Verify"), _("Upload"), _("New"), _("Open"), _("Save"), _("Serial Monitor") + _("Verify"), _("Upload"), _("New"), _("Open"), _("Save"), _("Serial Monitor"), _("Serial Plotter") }; /** Titles for each button when the shift key is pressed. */ static final String titleShift[] = { - _("Verify"), _("Upload Using Programmer"), _("New"), _("Open in Another Window"), _("Save As..."), _("Serial Monitor") + _("Verify"), _("Upload Using Programmer"), _("New"), _("Open in Another Window"), _("Save As..."), _("Serial Monitor"), _("Serial Plotter") }; static final int BUTTON_COUNT = title.length; @@ -65,6 +65,7 @@ public class EditorToolbar extends JComponent implements MouseInputListener, Key static final int SAVE = 4; static final int SERIAL = 5; + static final int PLOTTER = 6; static final int INACTIVE = 0; static final int ROLLOVER = 1; @@ -110,6 +111,7 @@ public EditorToolbar(Editor editor, JMenu menu) { which[buttonCount++] = OPEN; which[buttonCount++] = SAVE; which[buttonCount++] = SERIAL; + which[buttonCount++] = PLOTTER; currentRollover = -1; @@ -173,8 +175,11 @@ public void paintComponent(Graphics screen) { } // Serial button must be on the right - x1[SERIAL] = width - BUTTON_WIDTH - 14; - x2[SERIAL] = width - 14; + x1[SERIAL] = width - 2 * BUTTON_WIDTH - 14; + x2[SERIAL] = width - BUTTON_WIDTH - 14; + // Plotter button too + x1[PLOTTER] = width - BUTTON_WIDTH - 14; + x2[PLOTTER] = width - 14; } Graphics g = offscreen.getGraphics(); g.setColor(bgcolor); //getBackground()); @@ -201,12 +206,15 @@ public void paintComponent(Graphics screen) { if (currentRollover != -1) { int statusY = (BUTTON_HEIGHT + g.getFontMetrics().getAscent()) / 2; String status = shiftPressed ? titleShift[currentRollover] : title[currentRollover]; - if (currentRollover != SERIAL) - g.drawString(status, (buttonCount-1) * BUTTON_WIDTH + 3 * BUTTON_GAP, statusY); - else { - int statusX = x1[SERIAL] - BUTTON_GAP; - statusX -= g.getFontMetrics().stringWidth(status); - g.drawString(status, statusX, statusY); + switch (currentRollover) { + case SERIAL: + case PLOTTER: + int statusX = x1[SERIAL] - BUTTON_GAP; + statusX -= g.getFontMetrics().stringWidth(status); + g.drawString(status, statusX, statusY); + break; + default: + g.drawString(status, (buttonCount-1) * BUTTON_WIDTH + 3 * BUTTON_GAP, statusY); } } @@ -356,6 +364,10 @@ public void mousePressed(MouseEvent e) { case SERIAL: editor.handleSerial(); break; + + case PLOTTER: + editor.handlePlotter(); + break; } } diff --git a/app/src/processing/app/NetworkMonitor.java b/app/src/processing/app/NetworkMonitor.java index 716c9f0fc0b..4a3e4adeae3 100644 --- a/app/src/processing/app/NetworkMonitor.java +++ b/app/src/processing/app/NetworkMonitor.java @@ -22,7 +22,7 @@ import static processing.app.I18n._; @SuppressWarnings("serial") -public class NetworkMonitor extends AbstractMonitor implements MessageConsumer { +public class NetworkMonitor extends AbstractTextMonitor implements MessageConsumer { private static final int MAX_CONNECTION_ATTEMPTS = 5; diff --git a/app/src/processing/app/SerialMonitor.java b/app/src/processing/app/SerialMonitor.java index e4d1455b5dd..845c6a2f3a2 100644 --- a/app/src/processing/app/SerialMonitor.java +++ b/app/src/processing/app/SerialMonitor.java @@ -28,7 +28,7 @@ import static processing.app.I18n._; @SuppressWarnings("serial") -public class SerialMonitor extends AbstractMonitor { +public class SerialMonitor extends AbstractTextMonitor { private Serial serial; private int serialRate; diff --git a/app/src/processing/app/SerialPlotter.java b/app/src/processing/app/SerialPlotter.java new file mode 100644 index 00000000000..70de04579e0 --- /dev/null +++ b/app/src/processing/app/SerialPlotter.java @@ -0,0 +1,242 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +package processing.app; + +import cc.arduino.packages.BoardPort; +import processing.app.legacy.PApplet; + +import processing.app.debug.MessageConsumer; +import processing.app.helpers.*; +import static processing.app.I18n._; + +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.*; +import javax.swing.*; +import javax.swing.border.*; +import javax.swing.event.*; +import javax.swing.text.*; + +public class SerialPlotter extends AbstractMonitor { + private StringBuffer messageBuffer; + private CircularBuffer buffer; + private GraphPanel graphPanel; + private JComboBox serialRates; + + private Serial serial; + private int serialRate; + + private class GraphPanel extends JPanel { + private double minY, maxY, rangeY; + private Rectangle bounds; + private int xOffset; + private Font font; + private Color graphColor; + + public GraphPanel() { + font = Theme.getFont("console.font"); + graphColor = Theme.getColor("header.bgcolor"); + xOffset = 20; + } + + @Override + public void paintComponent(Graphics g1) { + Graphics2D g = (Graphics2D)g1; + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setFont(font); + super.paintComponent(g); + + bounds = g.getClipBounds(); + setBackground(Color.WHITE); + if(buffer.isEmpty()) { + return; + } + + minY = buffer.min(); + maxY = buffer.max(); + Ticks ticks = new Ticks(minY, maxY, 3); + minY = Math.min(minY, ticks.getTick(0)); + maxY = Math.max(maxY, ticks.getTick(ticks.getTickCount() - 1)); + rangeY = maxY - minY; + minY -= 0.05 * rangeY; + maxY += 0.05 * rangeY; + rangeY = maxY - minY; + + g.setStroke(new BasicStroke(1.0f)); + FontMetrics fm = g.getFontMetrics(); + for(int i = 0; i < ticks.getTickCount(); ++i) { + double tick = ticks.getTick(i); + Rectangle2D fRect = fm.getStringBounds(String.valueOf(tick), g); + xOffset = Math.max(xOffset, (int)fRect.getWidth() + 15); + + // draw tick + g.drawLine(xOffset - 5, (int)transformY(tick), xOffset + 2, (int)transformY(tick)); + // draw tick label + g.drawString(String.valueOf(tick), xOffset - (int)fRect.getWidth() - 10, transformY(tick) - (float)fRect.getHeight() * 0.5f + fm.getAscent()); + } + + g.drawLine(bounds.x + xOffset, bounds.y + 5, bounds.x + xOffset, bounds.y + bounds.height - 10); + + g.setTransform(AffineTransform.getTranslateInstance(xOffset, 0)); + float xstep = (float)(bounds.width - xOffset) / (float)buffer.capacity(); + + g.setColor(graphColor); + g.setStroke(new BasicStroke(0.75f)); + + for(int i = 0; i < buffer.size() - 1; ++i) { + g.drawLine( + (int)(i * xstep), (int)transformY(buffer.get(i)), + (int)((i + 1) * xstep), (int)transformY(buffer.get(i + 1)) + ); + } + } + + @Override + public Dimension getMinimumSize() { + return new Dimension(200, 100); + } + + @Override + public Dimension getPreferredSize() { + return new Dimension(500, 250); + } + + private float transformY(double rawY) { + return (float)(5 + (bounds.height - 10) * (1.0 - (rawY - minY) / rangeY)); + } + } + + public SerialPlotter(BoardPort port) { + super(port); + + serialRate = PreferencesData.getInteger("serial.debug_rate"); + serialRates.setSelectedItem(serialRate + " " + _("baud")); + onSerialRateChange(new ActionListener() { + public void actionPerformed(ActionEvent event) { + String wholeString = (String) serialRates.getSelectedItem(); + String rateString = wholeString.substring(0, wholeString.indexOf(' ')); + serialRate = Integer.parseInt(rateString); + PreferencesData.set("serial.debug_rate", rateString); + try { + close(); + Thread.sleep(100); // Wait for serial port to properly close + open(); + } catch (InterruptedException e) { + // noop + } catch (Exception e) { + System.err.println(e); + } + } + }); + + messageBuffer = new StringBuffer(); + } + + protected void onCreateWindow(Container mainPane) { + mainPane.setLayout(new BorderLayout()); + + Font consoleFont = Theme.getFont("console.font"); + Font editorFont = PreferencesData.getFont("editor.font"); + Font font = new Font(consoleFont.getName(), consoleFont.getStyle(), editorFont.getSize()); + + buffer = new CircularBuffer(500); + graphPanel = new GraphPanel(); + + mainPane.add(graphPanel, BorderLayout.CENTER); + + JPanel pane = new JPanel(); + pane.setLayout(new BoxLayout(pane, BoxLayout.X_AXIS)); + pane.setBorder(new EmptyBorder(4, 4, 4, 4)); + + String[] serialRateStrings = { + "300","1200","2400","4800","9600","14400", + "19200","28800","38400","57600","115200" + }; + + serialRates = new JComboBox(); + for (int i = 0; i < serialRateStrings.length; i++) + serialRates.addItem(serialRateStrings[i] + " " + _("baud")); + + serialRates.setMaximumSize(serialRates.getMinimumSize()); + + pane.add(Box.createRigidArea(new Dimension(8, 0))); + pane.add(serialRates); + + mainPane.add(pane, BorderLayout.SOUTH); + } + + protected void onEnableWindow(boolean enable) + { + serialRates.setEnabled(enable); + } + + public void onSerialRateChange(ActionListener listener) { + serialRates.addActionListener(listener); + } + + public void message(final String s) { + messageBuffer.append(s); + while(true) { + int linebreak = messageBuffer.indexOf("\n"); + if(linebreak == -1) { + break; + } + + String line = messageBuffer.substring(0, linebreak); + line = line.trim(); + messageBuffer.delete(0, linebreak + 1); + + try { + double value = Double.valueOf(line); + buffer.add(value); + } catch(NumberFormatException e) { + continue; // ignore lines that can't be cast to a number + } + } + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + SerialPlotter.this.repaint(); + }}); + } + + public void open() throws Exception { + super.open(); + + if (serial != null) return; + + serial = new Serial(getBoardPort().getAddress(), serialRate) { + @Override + protected void message(char buff[], int n) { + addToUpdateBuffer(buff, n); + } + }; + } + + public void close() throws Exception { + if (serial != null) { + super.close(); + int[] location = getPlacement(); + String locationStr = PApplet.join(PApplet.str(location), ","); + PreferencesData.set("last.serial.location", locationStr); + serial.dispose(); + serial = null; + } + } +} diff --git a/app/src/processing/app/helpers/CircularBuffer.java b/app/src/processing/app/helpers/CircularBuffer.java new file mode 100644 index 00000000000..6239822042d --- /dev/null +++ b/app/src/processing/app/helpers/CircularBuffer.java @@ -0,0 +1,81 @@ +package processing.app.helpers; + +import java.util.NoSuchElementException; + +public class CircularBuffer { + private double[] elements; + private int start = -1; + private int end = -1; + private int capacity; + + public void add(double num) { + end = (end + 1) % capacity; + elements[end] = num; + if(start == end || start == -1) { + start = (start + 1) % capacity; + } + } + + public double get(int index) { + if(index >= capacity) { + throw new IndexOutOfBoundsException(); + } + if(index >= size()) { + throw new IndexOutOfBoundsException(); + } + + return elements[(start + index) % capacity]; + } + + public boolean isEmpty() { + return start == -1 && end == -1; + } + + public void clear() { + start = end = -1; + } + + public CircularBuffer(int capacity) { + this.capacity = capacity; + elements = new double[capacity]; + } + + public double min() { + if(size() == 0) { + throw new NoSuchElementException(); + } + + double out = get(0); + for(int i = 1; i < size(); ++i) { + out = Math.min(out, get(i)); + } + + return out; + } + + public double max() { + if(size() == 0) { + throw new NoSuchElementException(); + } + + double out = get(0); + for(int i = 1; i < size(); ++i) { + out = Math.max(out, get(i)); + } + + return out; + } + + public int size() { + if(end == -1) { + return 0; + } + + return (end - start + capacity) % capacity + 1; + } + + public int capacity() { + return capacity; + } + +} diff --git a/app/src/processing/app/helpers/Ticks.java b/app/src/processing/app/helpers/Ticks.java new file mode 100644 index 00000000000..a4b32a2c310 --- /dev/null +++ b/app/src/processing/app/helpers/Ticks.java @@ -0,0 +1,46 @@ +package processing.app.helpers; + +public class Ticks { + private double tickMin; + private double tickMax; + private double tickStep; + private int tickCount; + + private double[] ticks; + + public Ticks(double min, double max, int tickCount) { + double range = max - min; + double exp = Math.floor(Math.log10(range / (tickCount - 1))); + double scale = Math.pow(10, exp); + + double rawTickStep = (range / (tickCount - 1)) / scale; + for(double potentialStep : new double[] {1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0}) { + if(potentialStep < rawTickStep) { + continue; + } + + tickStep = potentialStep * scale; + tickMin = tickStep * Math.floor(min / tickStep); + tickMax = tickMin + tickStep * (tickCount - 1); + if(tickMax >= max) { + break; + } + } + + tickCount -= (int)Math.floor((tickMax - max) / tickStep); + this.tickCount = tickCount; + + ticks = new double[tickCount]; + for(int i = 0; i < tickCount; ++i) { + ticks[i] = tickMin + i * tickStep; + } + } + + public double getTick(int i) { + return ticks[i]; + } + + public int getTickCount() { + return tickCount; + } +} diff --git a/build/shared/lib/theme/buttons.gif b/build/shared/lib/theme/buttons.gif index 4de0905d23dec4147b7d56b70b3a91950cd355e9..f9f0f1f2a86aba2df3a0b8cdee7d0c3f45d9a460 100644 GIT binary patch delta 2988 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 delta 2535 zcmV?NvqJ5fm?aa1rA#f#%c7j-!bidkl7`N)ezHk2mmMa)AL$7W-I7GZwal^rc z)lL+1(^68ggeGo1+ z-*pBRm4s+cXaK?$q5<(>hFU2hnieA#^pprJUI<@>GNE^#gA^)foP^9dSDbtL(FYeG zkx(JQOi?i6Mv4S!mBAyA5b`344dO_jOmGdi)Ob{~*ItfK9%P}ENUFHgbQz>qnhDc| zDbQ1Q+%XdyqRD^IWs+Coh-F~*$(Y`rch2Ubnggxa29vB?N!!Xdh}^0=i~<5C}-cPFh6&(gvxtdJ5%EbBK0F zevy<(=&JIyn5wCt>M3J$ehyosm&n~s9(*-s%e%*VP_Q-02xeiDkc;Y$>L3w#PdhD{d#-*-SS-E>@9f~N@Mh@`; zy4zEQL_z{oz8&=NioM+hX^iFqTeDzvdf0|jy zR}CCC*p<1&7*%FHM3z=*gJu}n?4H^)+?gJv6vZk>boWzDLH`7q*kyY?x7;R=1rvcb z;dJ;;Jq6|Xa4eJ5gslq z0Kn|CFW>?mug6Y%?YFyL`UnH;F8lx^Pz1d2yAvM~^2|5SQ1j65{(SV&Pfxw`4B?T$ z^#CM*`$O7u@BKsEW3N#8%v+B>?dq=&K=#3xKfw3!qd&hx?n}Q={_OA1|NH*qA49gc zz65_bgn;NXU_<@~!1F;*cF=3z`Xq?Hg^YlI#j_v=7q~k|c<_Uch+qJbw*W^#FnuH3 zT?svSLg7)cdiHgvjuL(5s;XX&1r{_7Hh5T%ig7&6kko39*Ix+ac`&umUJn7CdMHYYv2Y*{tR^S=qr?+Ebei_(TMN695rF z1Rv~~&uh{S2OcZ~04m@?EKu^D%*=l#A=?*CMNTt?|CDDx=}AI<&J&b}1i%`2xjQ2) zA`2;nU?Txg1`nQrj)wSVKq28qRK9Vi0%;`~hx*Z(I#Q%3b?80~*-(_uZVGnPK?5FO z1Vb2t1LYhDA$SC_Wcb4$xWIw9$`ukfWF8+J!GPxJ@F0tVEGg{ zK=%LTP$O&&kBJvp68qpRUf(3Lv}Mlcnk?z*Luu-=CP**0_zovde@(B^sIT@0x8_s4^0qu zL9Afq9YCTBJhp;Dn7!#T z5CQ>ryPq6vBLn-?X0~;_^^NaN9|YFp=5@I>O=BqtWD7I6ffxjmY<`fW#M4z+x{zLJt%NxszugkV9ykK%(hh4?E6R+y#N`;RHtzNE`;f@2v0q+rF-L&W|2) zvS)kjeD8LPPXKRMpTG=|kVB0pFL$idcn<>c!;NQZYV;dpT_qp}Y?Dg)ax%griv*4($3}xm+A`W{O6CIWNO=$Vf5(?0a#wUKrM}9*nfKvv7O4el$v1L)FgbqVkX94JBQK)=BNQ4a$Wb~(i5rKt9 xMr082XhkT6VMv8GD1=&;V!HNXF@}aTW@9+^hHw~%ayW-{Sci6ahb9pa06VV