From 3485f6dead552b2d933186cefa20efeb10ed5665 Mon Sep 17 00:00:00 2001
From: Pieter12345
Date: Fri, 15 Mar 2019 23:16:03 +0100
Subject: [PATCH] Add serial monitor send/receive encoding options
- Add "send as " dropdown menu.
- Add "receive as " dropdown menu.
Sending and receiving can be done in any encoding specified in StandardCharsets with the additional option to send comma-separated bytes and receive newline-separated bytes directly.
This fixes #4452 and offers an easy implementation for issue #4632.
---
.../processing/app/AbstractTextMonitor.java | 53 ++++++++++-
app/src/processing/app/EncodingOption.java | 77 +++++++++++++++
app/src/processing/app/SerialMonitor.java | 81 +++++++++++++++-
arduino-core/src/processing/app/Serial.java | 93 ++++++++++---------
4 files changed, 255 insertions(+), 49 deletions(-)
create mode 100644 app/src/processing/app/EncodingOption.java
diff --git a/app/src/processing/app/AbstractTextMonitor.java b/app/src/processing/app/AbstractTextMonitor.java
index 64a0928e55e..63e3509b822 100644
--- a/app/src/processing/app/AbstractTextMonitor.java
+++ b/app/src/processing/app/AbstractTextMonitor.java
@@ -40,6 +40,8 @@ public abstract class AbstractTextMonitor extends AbstractMonitor {
protected JButton clearButton;
protected JCheckBox autoscrollBox;
protected JCheckBox addTimeStampBox;
+ protected JComboBox sendEncoding;
+ protected JComboBox receiveEncoding;
protected JComboBox lineEndings;
protected JComboBox serialRates;
@@ -105,6 +107,44 @@ public void windowGainedFocus(WindowEvent e) {
minimumSize.setSize(minimumSize.getWidth() / 3, minimumSize.getHeight());
noLineEndingAlert.setMinimumSize(minimumSize);
+ String sendAs = tr("Send as") + " ";
+ sendEncoding = new JComboBox<>(new String[] {
+ sendAs + EncodingOption.SYSTEM_DEFAULT,
+ sendAs + EncodingOption.BYTES,
+ sendAs + EncodingOption.UTF_8,
+ sendAs + EncodingOption.UTF_16,
+ sendAs + EncodingOption.UTF_16BE,
+ sendAs + EncodingOption.UTF_16LE,
+ sendAs + EncodingOption.ISO_8859_1,
+ sendAs + EncodingOption.US_ASCII});
+ sendEncoding.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ PreferencesData.set("serial.send_encoding", sendEncoding.getItemAt(
+ sendEncoding.getSelectedIndex()).substring(sendAs.length()));
+ }
+ });
+ String sendEncodingStr = PreferencesData.get("serial.send_encoding");
+ if (sendEncodingStr != null) {
+ sendEncoding.setSelectedItem(sendAs + sendEncodingStr);
+ }
+ sendEncoding.setMaximumSize(sendEncoding.getMinimumSize());
+
+ String receiveAs = tr("Receive as") + " ";
+ receiveEncoding = new JComboBox<>(new String[] {
+ receiveAs + EncodingOption.SYSTEM_DEFAULT,
+ receiveAs + EncodingOption.BYTES,
+ receiveAs + EncodingOption.UTF_8,
+ receiveAs + EncodingOption.UTF_16,
+ receiveAs + EncodingOption.UTF_16BE,
+ receiveAs + EncodingOption.UTF_16LE,
+ receiveAs + EncodingOption.ISO_8859_1,
+ receiveAs + EncodingOption.US_ASCII});
+ String receiveEncodingStr = PreferencesData.get("serial.receive_encoding");
+ if (receiveEncodingStr != null) {
+ receiveEncoding.setSelectedItem(receiveAs + receiveEncodingStr);
+ }
+ receiveEncoding.setMaximumSize(receiveEncoding.getMinimumSize());
+
lineEndings = new JComboBox(new String[]{tr("No line ending"), tr("Newline"), tr("Carriage return"), tr("Both NL & CR")});
lineEndings.addActionListener((ActionEvent event) -> {
PreferencesData.setInteger("serial.line_ending", lineEndings.getSelectedIndex());
@@ -127,6 +167,10 @@ public void windowGainedFocus(WindowEvent e) {
pane.add(Box.createHorizontalGlue());
pane.add(noLineEndingAlert);
pane.add(Box.createRigidArea(new Dimension(8, 0)));
+ pane.add(sendEncoding);
+ pane.add(Box.createRigidArea(new Dimension(8, 0)));
+ pane.add(receiveEncoding);
+ pane.add(Box.createRigidArea(new Dimension(8, 0)));
pane.add(lineEndings);
pane.add(Box.createRigidArea(new Dimension(8, 0)));
pane.add(serialRates);
@@ -139,8 +183,7 @@ public void windowGainedFocus(WindowEvent e) {
}
@Override
- protected void onEnableWindow(boolean enable)
- {
+ protected void onEnableWindow(boolean enable) {
textArea.setEnabled(enable);
clearButton.setEnabled(enable);
scrollPane.setEnabled(enable);
@@ -148,6 +191,8 @@ protected void onEnableWindow(boolean enable)
sendButton.setEnabled(enable);
autoscrollBox.setEnabled(enable);
addTimeStampBox.setEnabled(enable);
+ sendEncoding.setEnabled(enable);
+ receiveEncoding.setEnabled(enable);
lineEndings.setEnabled(enable);
serialRates.setEnabled(enable);
}
@@ -165,6 +210,10 @@ public void onSerialRateChange(ActionListener listener) {
serialRates.addActionListener(listener);
}
+ public void onReceiveEncodingChange(ActionListener listener) {
+ receiveEncoding.addActionListener(listener);
+ }
+
@Override
public void message(String msg) {
SwingUtilities.invokeLater(() -> updateTextArea(msg));
diff --git a/app/src/processing/app/EncodingOption.java b/app/src/processing/app/EncodingOption.java
new file mode 100644
index 00000000000..3a277edbf10
--- /dev/null
+++ b/app/src/processing/app/EncodingOption.java
@@ -0,0 +1,77 @@
+package processing.app;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Represents the encoding option for decoding or encoding bytes that are
+ * read from or written to a stream.
+ */
+public enum EncodingOption {
+
+ /**
+ * The system default character set as returned by
+ * {@link Charset#defaultCharset()}.
+ */
+ SYSTEM_DEFAULT(Charset.defaultCharset()),
+
+ /**
+ * Comma separated unsigned byte representation.
+ */
+ BYTES(null),
+
+ UTF_8(StandardCharsets.UTF_8),
+ UTF_16(StandardCharsets.UTF_16),
+ UTF_16BE(StandardCharsets.UTF_16BE),
+ UTF_16LE(StandardCharsets.UTF_16LE),
+ ISO_8859_1(StandardCharsets.ISO_8859_1),
+ US_ASCII(StandardCharsets.US_ASCII);
+
+ private final Charset charset;
+
+ private EncodingOption(Charset charset) {
+ this.charset = charset;
+ }
+
+ public Charset getCharset() {
+ return this.charset;
+ }
+
+ @Override
+ public String toString() {
+ switch (this) {
+ case SYSTEM_DEFAULT:
+ case BYTES:
+ return this.name().replace('_', ' ').toLowerCase();
+ default:
+ return this.charset.name();
+ }
+ }
+
+ /**
+ * Gets the {@link EncodingOption} with the given name.
+ * The name match is case-insensitive and
+ * whitespaces/dashes are interpreted as '_'.
+ * @param name - The name of the {@link EncodingOption}.
+ * @return The matching {@link EncodingOption}
+ * or null when no such option exists.
+ */
+ public static EncodingOption forName(String name) {
+ if (name == null) {
+ return null;
+ }
+ try {
+ return EncodingOption.valueOf(
+ name.replace(' ', '_').replace('-', '_').toUpperCase());
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+// name = name.replace(' ', '_').replace('-', '_');
+// for (EncodingOption option : EncodingOption.values()) {
+// if (option.name().equalsIgnoreCase(name)) {
+// return option;
+// }
+// }
+// return null;
+ }
+}
diff --git a/app/src/processing/app/SerialMonitor.java b/app/src/processing/app/SerialMonitor.java
index d4f59019eae..e78498b8332 100644
--- a/app/src/processing/app/SerialMonitor.java
+++ b/app/src/processing/app/SerialMonitor.java
@@ -23,6 +23,8 @@
import java.awt.Color;
import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.nio.charset.Charset;
import static processing.app.I18n.tr;
@@ -32,6 +34,11 @@ public class SerialMonitor extends AbstractTextMonitor {
private Serial serial;
private int serialRate;
+ private static final EncodingOption DEFAULT_SEND_ENCODING =
+ EncodingOption.UTF_8;
+ private static final EncodingOption DEFAULT_RECEIVE_ENCODING =
+ EncodingOption.UTF_8;
+
public SerialMonitor(Base base, BoardPort port) {
super(base, port);
@@ -53,6 +60,18 @@ public SerialMonitor(Base base, BoardPort port) {
}
});
+ onReceiveEncodingChange((ActionEvent event) -> {
+ String receiveAs = tr("Receive as") + " ";
+ String selectedEncodingStr = receiveEncoding.getItemAt(
+ receiveEncoding.getSelectedIndex()).substring(receiveAs.length());
+ Charset selectedCharset =
+ EncodingOption.forName(selectedEncodingStr).getCharset();
+ if (serial.getCharset() != selectedCharset) {
+ serial.resetDecoding(selectedCharset);
+ PreferencesData.set("serial.receive_encoding", selectedEncodingStr);
+ }
+ });
+
onSendCommand((ActionEvent event) -> {
send(textField.getText());
textField.setText("");
@@ -76,11 +95,48 @@ private void send(String s) {
default:
break;
}
- if ("".equals(s) && lineEndings.getSelectedIndex() == 0 && !PreferencesData.has("runtime.line.ending.alert.notified")) {
+ if ("".equals(s) && lineEndings.getSelectedIndex() == 0
+ && !PreferencesData.has("runtime.line.ending.alert.notified")) {
noLineEndingAlert.setForeground(Color.RED);
PreferencesData.set("runtime.line.ending.alert.notified", "true");
}
- serial.write(s);
+ EncodingOption encodingOption =
+ EncodingOption.forName(PreferencesData.get("serial.send_encoding"));
+ if (encodingOption == null) {
+ encodingOption = DEFAULT_SEND_ENCODING;
+ }
+ Charset charSet = encodingOption.getCharset();
+ byte[] bytes;
+ if (charSet != null) {
+ bytes = s.getBytes(encodingOption.getCharset());
+ } else {
+ switch (encodingOption) {
+ case BYTES:
+ String[] split = s.split(",");
+ bytes = new byte[split.length];
+ for (int i = 0; i < split.length; i++) {
+ String valStr = split[i].trim();
+ try {
+ int val = Integer.parseInt(valStr);
+ if (val < 0x00 || val > 0xFF) {
+ this.message("\n[ERROR] Invalid byte value given: "
+ + val + ". Byte values are in range [0-255].\n");
+ return;
+ }
+ bytes[i] = (byte) val;
+ } catch (NumberFormatException e) {
+ this.message("\n[ERROR] Invalid byte value given: " + valStr
+ + ". Byte values are numbers in range [0-255].\n");
+ return;
+ }
+ }
+ break;
+ default:
+ throw new Error(
+ "Unsupported 'send as' encoding option: " + encodingOption);
+ }
+ }
+ serial.write(bytes);
}
}
@@ -90,10 +146,27 @@ public void open() throws Exception {
if (serial != null) return;
- serial = new Serial(getBoardPort().getAddress(), serialRate) {
+ EncodingOption encodingOption =
+ EncodingOption.forName(PreferencesData.get("serial.receive_encoding"));
+ if (encodingOption == null) {
+ encodingOption = DEFAULT_RECEIVE_ENCODING;
+ }
+ serial = new Serial(
+ getBoardPort().getAddress(), serialRate, encodingOption.getCharset()) {
@Override
protected void message(char buff[], int n) {
- addToUpdateBuffer(buff, n);
+ if (serial.getCharset() == null) {
+ if(buff.length != 0) {
+ StringBuilder strBuilder = new StringBuilder();
+ for (int i = 0; i < n; i++) {
+ strBuilder.append(buff[i] & 0xFF).append("\n");
+ }
+ addToUpdateBuffer(
+ strBuilder.toString().toCharArray(), strBuilder.length());
+ }
+ } else {
+ addToUpdateBuffer(buff, n);
+ }
}
};
}
diff --git a/arduino-core/src/processing/app/Serial.java b/arduino-core/src/processing/app/Serial.java
index 484ac11909b..a293caded0a 100644
--- a/arduino-core/src/processing/app/Serial.java
+++ b/arduino-core/src/processing/app/Serial.java
@@ -33,7 +33,6 @@
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
-import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
@@ -60,39 +59,28 @@ public class Serial implements SerialPortEventListener {
private CharBuffer outToMessage = CharBuffer.allocate(OUT_BUFFER_CAPACITY);
public Serial() throws SerialException {
- this(PreferencesData.get("serial.port"),
- PreferencesData.getInteger("serial.debug_rate", 9600),
- PreferencesData.getNonEmpty("serial.parity", "N").charAt(0),
- PreferencesData.getInteger("serial.databits", 8),
- PreferencesData.getFloat("serial.stopbits", 1),
- !BaseNoGui.getBoardPreferences().getBoolean("serial.disableRTS"),
- !BaseNoGui.getBoardPreferences().getBoolean("serial.disableDTR"));
+ this(PreferencesData.get("serial.port"), PreferencesData.getInteger("serial.debug_rate", 9600));
}
public Serial(int irate) throws SerialException {
- this(PreferencesData.get("serial.port"), irate,
- PreferencesData.getNonEmpty("serial.parity", "N").charAt(0),
- PreferencesData.getInteger("serial.databits", 8),
- PreferencesData.getFloat("serial.stopbits", 1),
- !BaseNoGui.getBoardPreferences().getBoolean("serial.disableRTS"),
- !BaseNoGui.getBoardPreferences().getBoolean("serial.disableDTR"));
+ this(PreferencesData.get("serial.port"), irate);
+ }
+
+ public Serial(String iname) throws SerialException {
+ this(iname, PreferencesData.getInteger("serial.debug_rate", 9600));
}
public Serial(String iname, int irate) throws SerialException {
- this(iname, irate, PreferencesData.getNonEmpty("serial.parity", "N").charAt(0),
- PreferencesData.getInteger("serial.databits", 8),
- PreferencesData.getFloat("serial.stopbits", 1),
- !BaseNoGui.getBoardPreferences().getBoolean("serial.disableRTS"),
- !BaseNoGui.getBoardPreferences().getBoolean("serial.disableDTR"));
+ this(iname, irate, Charset.defaultCharset());
}
- public Serial(String iname) throws SerialException {
- this(iname, PreferencesData.getInteger("serial.debug_rate", 9600),
- PreferencesData.getNonEmpty("serial.parity", "N").charAt(0),
+ public Serial(String iname, int irate, Charset charset) throws SerialException {
+ this(iname, irate, PreferencesData.getNonEmpty("serial.parity", "N").charAt(0),
PreferencesData.getInteger("serial.databits", 8),
PreferencesData.getFloat("serial.stopbits", 1),
!BaseNoGui.getBoardPreferences().getBoolean("serial.disableRTS"),
- !BaseNoGui.getBoardPreferences().getBoolean("serial.disableDTR"));
+ !BaseNoGui.getBoardPreferences().getBoolean("serial.disableDTR"),
+ charset);
}
public static boolean touchForCDCReset(String iname) throws SerialException {
@@ -116,12 +104,13 @@ public static boolean touchForCDCReset(String iname) throws SerialException {
}
}
- private Serial(String iname, int irate, char iparity, int idatabits, float istopbits, boolean setRTS, boolean setDTR) throws SerialException {
+ private Serial(String iname, int irate, char iparity, int idatabits,
+ float istopbits, boolean setRTS, boolean setDTR, Charset charset) throws SerialException {
//if (port != null) port.close();
//this.parent = parent;
//parent.attach(this);
- resetDecoding(StandardCharsets.UTF_8);
+ resetDecoding(charset);
int parity = SerialPort.PARITY_NONE;
if (iparity == 'E') parity = SerialPort.PARITY_EVEN;
@@ -175,24 +164,32 @@ public synchronized void serialEvent(SerialPortEvent serialEvent) {
if (serialEvent.isRXCHAR()) {
try {
byte[] buf = port.readBytes(serialEvent.getEventValue());
- int next = 0;
- while(next < buf.length) {
- while(next < buf.length && outToMessage.hasRemaining()) {
- int spaceInIn = inFromSerial.remaining();
- int copyNow = buf.length - next < spaceInIn ? buf.length - next : spaceInIn;
- inFromSerial.put(buf, next, copyNow);
- next += copyNow;
- inFromSerial.flip();
- bytesToStrings.decode(inFromSerial, outToMessage, false);
- inFromSerial.compact();
+ if (bytesToStrings == null) {
+ char[] chars = new char[buf.length];
+ for (int i = 0; i < buf.length; i++) {
+ chars[i] = (char) buf[i];
}
- outToMessage.flip();
- if(outToMessage.hasRemaining()) {
- char[] chars = new char[outToMessage.remaining()];
- outToMessage.get(chars);
- message(chars, chars.length);
+ message(chars, chars.length);
+ } else {
+ int next = 0;
+ while(next < buf.length) {
+ while(next < buf.length && outToMessage.hasRemaining()) {
+ int spaceInIn = inFromSerial.remaining();
+ int copyNow = buf.length - next < spaceInIn ? buf.length - next : spaceInIn;
+ inFromSerial.put(buf, next, copyNow);
+ next += copyNow;
+ inFromSerial.flip();
+ bytesToStrings.decode(inFromSerial, outToMessage, false);
+ inFromSerial.compact();
+ }
+ outToMessage.flip();
+ if(outToMessage.hasRemaining()) {
+ char[] chars = new char[outToMessage.remaining()];
+ outToMessage.get(chars);
+ message(chars, chars.length);
+ }
+ outToMessage.clear();
}
- outToMessage.clear();
}
} catch (SerialPortException e) {
errorMessage("serialEvent", e);
@@ -264,15 +261,25 @@ public void setRTS(boolean state) {
/**
* Reset the encoding used to convert the bytes coming in
- * before they are handed as Strings to {@Link #message(char[], int)}.
+ * before they are handed as char arrays to {@Link #message(char[], int)}.
+ * @param charset - The character set that will be used for conversion or null to not perform conversion,
+ * in which case bytes are simply cast to chars.
*/
public synchronized void resetDecoding(Charset charset) {
- bytesToStrings = charset.newDecoder()
+ bytesToStrings = charset == null ? null : charset.newDecoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE)
.replaceWith("\u2e2e");
}
+ /**
+ * Get the {@link Charset} used to convert the incoming bytes to chars.
+ * @return The {@link Charset} or null when no conversion is performed, in which case bytes are simply cast to chars.
+ */
+ public Charset getCharset() {
+ return bytesToStrings == null ? null : bytesToStrings.charset();
+ }
+
static public List list() {
return Arrays.asList(SerialPortList.getPortNames());
}