diff --git a/app/src/processing/app/AbstractMonitor.java b/app/src/processing/app/AbstractMonitor.java index 18dc268b878..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", "74880", "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 917a47e8cca..de5688bd015 100644 --- a/app/src/processing/app/Editor.java +++ b/app/src/processing/app/Editor.java @@ -138,7 +138,8 @@ public boolean apply(Sketch sketch) { private static JMenu portMenu; static volatile AbstractMonitor serialMonitor; - + static AbstractMonitor serialPlotter; + final EditorHeader header; EditorStatus status; EditorConsole console; @@ -230,7 +231,7 @@ public void windowDeactivated(WindowEvent e) { //PdeKeywords keywords = new PdeKeywords(); //sketchbook = new Sketchbook(this); - + buildMenuBar(); // For rev 0120, placing things inside a JPanel @@ -787,6 +788,14 @@ private JMenu buildToolsMenu() { item.addActionListener(e -> handleSerial()); 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); @@ -1113,6 +1122,7 @@ private void selectSerialPort(String name) { } if (selection != null) selection.setState(true); //System.out.println(item.getLabel()); + BaseNoGui.selectSerialPort(name); if (serialMonitor != null) { try { @@ -1123,6 +1133,16 @@ private 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")); @@ -2389,6 +2409,9 @@ public void run() { if (serialMonitor != null) { serialMonitor.suspend(); } + if (serialPlotter != null) { + serialPlotter.suspend(); + } uploading = true; @@ -2420,6 +2443,7 @@ public void run() { toolbar.deactivate(EditorToolbar.EXPORT); resumeOrCloseSerialMonitor(); + resumeOrCloseSerialPlotter(); base.onBoardOrPortChange(); } } @@ -2439,6 +2463,8 @@ private void resumeOrCloseSerialMonitor() { } } try { + if (serialMonitor != null) + serialMonitor.resume(boardPort); if (boardPort == null) { serialMonitor.close(); handleSerial(); @@ -2448,7 +2474,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) @@ -2459,6 +2504,9 @@ public void run() { if (serialMonitor != null) { serialMonitor.suspend(); } + if (serialPlotter != null) { + serialPlotter.suspend(); + } uploading = true; @@ -2490,12 +2538,23 @@ public void run() { toolbar.deactivate(EditorToolbar.EXPORT); resumeOrCloseSerialMonitor(); + resumeOrCloseSerialPlotter(); + base.onBoardOrPortChange(); } } 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 @@ -2582,7 +2641,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); + + } private void handleBurnBootloader() { console.clear(); 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 9e418485568..8a3d5c046d7 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; + } +}