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()); }