package pif.arduino; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.Arrays; import org.apache.log4j.Logger; import pif.arduino.tools.JlineConsole; import pif.arduino.tools.hexTools; /** * goal of this class is : * - to handle incoming data to display them in raw, ascii or hex format * - interpret command line to get data to send from raw or hex format * - handle specific commands * it must be associated to a peer which handle incoming and outgoing data * and specific commands * @author pif * */ public class Console extends Thread { Logger logger = Logger.getLogger(Console.class); protected ConsolePeer peer; protected boolean raw = false; static public final String PROMPT = "> "; /* * console must be "attached" to a class which handle commands and data, thru * this interface */ public interface ConsolePeer { /** * called when console reads data to send to peer * @param data raw bytes to send */ public void onOutgoingData(byte data[]); /** * called when console receives command it can't handle itself * @param command command line, without '!' prefix */ public boolean onCommand(String command); /** * called when console is closed (Ctrl-D or !exit command, or unrecoverable error) * @param status 0 for normal exit, error code else */ public void onDisconnect(int status); } public Console(ConsolePeer peer) { this(peer, false); } public Console(ConsolePeer peer, boolean raw) { this.raw = raw; if (raw) { displayMode = MODE_RAW; } this.peer = peer; } /** * console where incoming data are displayed and command input comes from * wraps JLineConsole or System.in/out according to raw mode */ protected class MyConsole { // jline version if not raw JlineConsole jline; // input if raw mode BufferedReader input; public MyConsole(boolean raw) throws IOException { if (raw) { input = new BufferedReader(new InputStreamReader(System.in)); } else { jline = new JlineConsole(); jline.setHistoryEnabled(true); jline.setExpandEvents(false); jline.setPrompt(PROMPT); } } public String readLine() throws IOException { if (raw) { return input.readLine(); } else { return jline.readLine(); } } public void insertString(String str) throws IOException { if (raw) { System.out.print(str); } else { jline.insertString(str); } } } protected MyConsole console; // current display mode static final byte MODE_RAW = 0; static final byte MODE_ASCII = 1; static final byte MODE_HEX = 2; protected byte displayMode = MODE_ASCII; // current line mode static final String LINE_CR = "\r"; static final String LINE_LF = "\n"; static final String LINE_CRLF = "\r\n"; static final String LINE_NONE = ""; protected String lineMode = LINE_NONE; /* * peer sends data it receives thru this method */ public void onIncomingData(byte data[], int length) { String toDisplay; switch(displayMode) { case MODE_ASCII: StringBuffer sb = new StringBuffer(length); for (int i = 0; i < length; i++) { if (data[i] == '\r') { sb.append("\\r"); } else if(data[i] == '\n' || (data[i] >= 32 && data[i] < 128)) { sb.append((char)data[i]); } else { sb.append("\033[7m[" + hexTools.toHex(data[i]) + "]\033[0m"); } } toDisplay = sb.toString(); break; case MODE_HEX: toDisplay = hexTools.toHexDump(data, length); break; default: toDisplay = new String(data, 0, length); } try { console.insertString(toDisplay); } catch (IOException e) { logger.error("Couldn't display incoming data", e); } } public void insertString(String str) throws IOException { console.insertString(str); } byte outgoingBuffer[] = null; void setBuffer(String line) { outgoingBuffer = (line + lineMode).getBytes(); } void send() { peer.onOutgoingData(outgoingBuffer); } void handleCommand(String line) { if (peer.onCommand(line)) { return; } if (line.equals("cr")) { lineMode = LINE_CR; } else if (line.equals("lf")) { lineMode = LINE_LF; } else if (line.equals("crlf")) { lineMode = LINE_CRLF; } else if (line.equals("none")) { lineMode = LINE_NONE; } else if (line.equals("hex")) { displayMode = MODE_HEX; } else if (line.equals("ascii")) { displayMode = MODE_ASCII; } else if (line.equals("raw")) { displayMode = MODE_RAW; } else if (line.startsWith("x ")) { byte[] data = readHexData(line.substring(2)); logger.debug(hexTools.toHexDump(data)); if (data != null) { peer.onOutgoingData(data); } } else if (line.equals("help") || line.equals("?")) { help(); } else { logger.warn("Unknown command " + line + ". Type !help or !? for help"); } } /** * convert a string to a byte array. * string must contains a list of hex values, with optional spaces and ascii sections * example : 0 123456 7 8 9ab 'ab c' 0123 * is converted in hex values [ 00 12 34 56 07 08 9a 0b 61 62 20 63 01 23 ] * @param data * @return */ public byte[] readHexData(String data) { byte[] result = new byte[data.length()]; int len = 0; // -1 = reading ascii // 1 = waiting for high part of a byte value // 2 = waiting for low part of a byte value // TODO 0d123 => input decimal value // TODO 0w123 or 0w-123 or => word big endian value // TODO 0l123 or 0l-123 => long big endian value // for these options, detect d, w or l when state==1 => state = 3 / 4 / 5 // + flag (-3 / -4 / -5 ?) to handle negative value // + must stop on space or quote short state = 1; byte currentByte = 0; for(int i = 0; i < data.length(); i++) { char c = data.charAt(i); if (c == '\'') { // end of ascii mode if (state == -1) { state = 1; currentByte = 0; } else { if (state == 2) { // already read beginning of a byte => consider it as a 1 digit value result[len++] = currentByte; } state = -1; } continue; } // ascii mode => take char as a byte. if (state == -1) { result[len++] = (byte)c; continue; } if (c == ' ') { // explicit separation between values // event if they have 1 digit if (state == 2) { result[len++] = currentByte; state = 1; } continue; } // else, must be a hex digit byte h; if (c >= '0' && c <= '9') { h = (byte)(c - '0'); } else if (c >= 'a' && c <= 'f') { h = (byte)(c - 'a' + 10); } else if (c >= 'A' && c <= 'F') { h = (byte)(c - 'A' + 10); } else { logger.error(String.format("unexpected character '%c' at position %d", c, i)); return null; } if (state == 1) { currentByte = h; state = 2; } else { currentByte = (byte)(currentByte << 4 | h); result[len++] = currentByte; state = 1; } } if (state == 2) { // already read beginning of a byte => consider it as a 1 digit value result[len++] = currentByte; } return Arrays.copyOf(result, len); } public void run() { try { console = new MyConsole(raw); } catch (IOException e) { logger.error("Can't initialize console", e); peer.onDisconnect(1); return; } String inputLine; try { while((inputLine = console.readLine()) != null) { try { if (inputLine.length() > 0 && inputLine.charAt(0) == '!') { if (inputLine.length() == 1) { // resend last buffer if (outgoingBuffer != null) { peer.onOutgoingData(outgoingBuffer); } } else if (inputLine.charAt(1) == '!') { // specific case when user wants to send raw data beginning by '!', // he must double it. setBuffer(inputLine.substring(1)); send(); } else { handleCommand(inputLine.substring(1)); if ("!exit".equals(inputLine)) { logger.debug("Exit console"); peer.onDisconnect(0); return; } } } else { setBuffer(inputLine); send(); } } catch (Exception e) { logger.info("Exception in console loop", e); // e.printStackTrace(); } } logger.debug("EOF"); peer.onDisconnect(0); } catch (IOException e) { logger.error("Exception with input, cancelling console", e); peer.onDisconnect(1); // e.printStackTrace(); } } void help() { // TODO String help = "Any byte incomming from peer is displayed in a format according to display mode (see hex, rax and ascii command below)" + "Any character entered in console are filled in a sending buffer, excepted if line begins with '!'.\n" + " When hitting enter, end of line characters are appended, and buffer is sent to peer.\n" + " End of line character depends on current line mode, which is 'none' by default (thus no character).\n" + " See cr, lf, .. commands balowto modify this mode.\n" + " To send explicitly a line beginning by '!', double it ('!!')\n" + "Lines begining with a '!' start a command :" + " !exit (or Ctrl-D) : exit console program\n" + " ! : resend last sending buffer, including its end of line characters (even if line mode has been modified)\n" + " !hex : incoming bytes are displayed in hex, in same format than hexdump -C\n" + " !ascii : printable characters are displayed raw, other ones are displayed in [hh] format (default mode)\n" + " !raw : all incomming bytes are displayed raw (default mode if -raw command line option was set)\n" + " !cr, !lf, !crlf, !none : set 'end of line' mode, respectivly to '\r', '\n', '\r\n', nothing\n" + " !x : rest of input line is interpreted in a intuitive (?) way mixing hex values and raw text\n" + " Example : 0 123456 7 8 9ab 'ab c' 0123 becames hex bytes [ 00 12 34 56 07 08 9a 0b 61 62 20 63 01 23 ]\n" + " Caution : this not does NOT append end of line characters, whatever current line mode"; System.out.println(help); } }