package oculusPrime.commport; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import jssc.SerialPort; import jssc.SerialPortEvent; import jssc.SerialPortEventListener; import jssc.SerialPortException; import jssc.SerialPortList; import oculusPrime.*; public class ArduinoPower implements SerialPortEventListener { public static final double FIRMWARE_VERSION_REQUIRED = 0.956; // trailing zeros ignored! public static final int DEVICEHANDSHAKEDELAY = 2000; public static final int DEAD_TIME_OUT = 15000; public static final int ALLOW_FOR_RESET = 10000; public static final int ERROR_TIME_OUT = (int) Util.ONE_MINUTE; public static final int WATCHDOG_DELAY = 5000; public static final int RESET_DELAY = 12 * (int) Util.ONE_HOUR; // xaxxonpower clock rollover is ~18 hrs private static final int HOST_HEARTBEAT_DELAY = (int) Util.ONE_MINUTE; public static final int BAUD = 115200; public static final String FIRMWARE_ID = "oculusPower"; public static final byte INITIATESHUTDOWN= 'p'; public static final byte CONFIRMSHUTDOWN= 'w'; public static final byte GET_VERSION = '7'; public static final byte READERROR = 's'; public static final byte STATUSTOEEPROM = 'A'; public static final byte TESTFORCEERRORCODE = '9'; public static final byte CLEARALLWARNINGERRORS = 'B'; public static final byte READCAPACITY = '6'; public static final byte PING = 'X'; public static final byte GET_PRODUCT = 'x'; public static final byte READ_SAFECURRENT = 'G'; public static final byte READ_DOCKVOLTAGE = 'E'; public static final int COMM_LOST = -99; public static final int LOWBATTPERCENTAGE = 30; // same or less than firmware var: gettingLowCell to enable capacity recalc protected Application application = null; protected State state = State.getReference(); protected static SerialPort serialPort = null; protected volatile boolean isconnected = false; protected static long lastRead; protected long lastReset; protected long lastHostHeartBeat; protected byte[] buffer = new byte[256]; protected int buffSize = 0; protected static Settings settings = Settings.getReference(); protected String portname = settings.readSetting(ManualSettings.powerport); // protected String version = null; private double firmwareversion = 0; // errors public static Map<Integer, String> pwrerr = new HashMap<Integer, String>(); public static final int WARNING_ONLY_BELOW = 40; public static final int RESET_REQUIRED_ABOVE= 19; public static final int FORCE_UNDOCK_ABOVE = 79; public static final int ERROR_NO_BATTERY_CONNECTED = 3; public static final List<Integer> IGNORE_ERROR = Arrays.asList(1,4,7,ERROR_NO_BATTERY_CONNECTED); // log only, suppress gui warnings: private volatile List<Byte> commandList = new ArrayList<>(); private volatile boolean commandlock = false; private boolean batterypresent = true; public ArduinoPower(Application app) { application = app; state.set(State.values.powerport, portname); state.set(State.values.dockstatus, AutoDock.UNKNOWN); state.set(State.values.batterylife, AutoDock.UNKNOWN); Map<String, Integer> temp = new HashMap<String, Integer>(); // (reversed declaration for easier transcribing from firmware) // ERROR CODES temp.put("ERROR_NO_ERROR_ALL_IS_WELL",0); // EROR CODES, WARNING (1-19): temp.put("ERROR_ATTEMPT_ZERO_CURRENT_WHEN_ACTIVE", 1); temp.put("ERROR_HIGH_CURRENT_SHUTDOWN", 2); temp.put("ERROR_NO_BATTERY_CONNECTED", 3); temp.put("ERROR_WALL_BRICK_LOW_VOLTAGE", 4); temp.put("ERROR_OVER_DISCHARGED_PACK", 5); temp.put("ERROR_PACK_DRAINED_TO_ZERO_PERCENT", 6); // (MOVED, was 22) temp.put("ERROR_NO_HOST_DETECTED", 7); // (MOVED, was 23) //ERROR CODES, WARNING SAFE CHARGE (20-39): // nothing here at this revision level //ERROR CODES, FATAL NO CHARGE (40-59): temp.put("ERROR_SEVERELY_UNBALANCED_PACK", 41); temp.put("ERROR_MAX_PWM_BUT_LOW_CURRENT", 42); temp.put("ERROR_OVERCHARGED_CELL", 43); temp.put("ERROR_REGULATOR_CANT_FIND_TARGET", 44); temp.put("ERROR_UNABLE_TO_COMPLETE_CHARGE", 45); temp.put("ERROR_CHARGE_TIMED_OUT", 46); //ERROR CODES, FATAL SAFE CHARGE (60-79): temp.put("ERROR_POWER_SOFT_SWITCH_FAIL", 61); temp.put("ERROR_EEPROM_ROLLOVER", 62); //ERROR CODES, FATAL NO CHARGE FORCE UNDOCK (80+): temp.put("ERROR_CURRENT_OFFSET_TOO_HIGH", 81); temp.put("ERROR_UNEXPECTED_CHARGE_CURRENT", 82); temp.put("ERROR_CONSISTENT_OVER_CHARGE", 83); // java only, not used by firmware: temp.put("ERROR_NO_COMM_WITH_POWER_PCB", COMM_LOST); // -99, java only // inverse: for(Map.Entry<String, Integer> entry : temp.entrySet()){ pwrerr.put(entry.getValue(), entry.getKey()); } // new CommandSender().start(); if(!settings.readSetting(ManualSettings.powerport).equals(Settings.DISABLED)) connect(); if (isconnected) { new CommandSender().start(); checkFirmWareVersion(); initialize(); new WatchDog().start(); } } public void initialize() { Util.debug("initialize", this); batterypresent = true; sendCommand(READERROR); sendCommand(READCAPACITY); sendCommand(READ_SAFECURRENT); sendCommand(READ_DOCKVOLTAGE); } private void connect() { isconnected = false; String[] portNames = SerialPortList.getPortNames(); if (portNames.length == 0) { state.delete(State.values.powerport); return; } String otherdevice = ""; if (state.exists(State.values.motorport.toString())) otherdevice = state.get(State.values.motorport); for (int i=0; i<portNames.length; i++) { // if (portNames[i].matches("/dev/tty(USB|ACM).+") && !portNames[i].equals(otherdevice)) { if (portNames[i].matches("/dev/ttyUSB.+") && !portNames[i].equals(otherdevice)) { try { Util.log("querying port "+portNames[i], this); PowerLogger.append("querying port "+portNames[i], this); serialPort = new SerialPort(portNames[i]); serialPort.openPort(); serialPort.setParams(BAUD, 8, 1, 0); Thread.sleep(DEVICEHANDSHAKEDELAY); serialPort.readBytes(); // clear serial buffer serialPort.writeBytes(new byte[]{GET_PRODUCT, 13}); // query device Thread.sleep(100); // some delay is required byte[] buffer = serialPort.readBytes(); if (buffer == null) { // no response, move on to next port serialPort.closePort(); continue; } String device = new String(); for (int n=0; n<buffer.length; n++) { if((int)buffer[n] == 13 || (int)buffer[n] == 10) { break; } if(Character.isLetter((char) buffer[n])) device += (char) buffer[n]; } if (device.length() == 0) break; if (device.trim().startsWith("id")) device = device.substring(2, device.length()); Util.debug(device+" "+portNames[i], this); if (device.equals(FIRMWARE_ID)) { Util.log("power board connected to "+portNames[i], this); PowerLogger.append("power board connected to "+portNames[i], this); lastRead = lastReset = System.currentTimeMillis(); isconnected = true; state.set(State.values.powerport, portNames[i]); serialPort.addEventListener(this, SerialPort.MASK_RXCHAR);//Add SerialPortEventListener break; // job done, don't read any more ports } serialPort.closePort(); } catch (Exception e) { PowerLogger.append("can't connect to port: " + e.getMessage(), this); Util.log("can't connect to port: " + e.getMessage(), this); state.delete(State.values.powerport); } } } } /** inner class to check if getting responses in timely manor */ public class WatchDog extends Thread { oculusPrime.State state = oculusPrime.State.getReference(); public WatchDog() { this.setDaemon(true); } public void run() { lastHostHeartBeat = System.currentTimeMillis(); while (true) { long now = System.currentTimeMillis(); // Util.debug("run....", this); // if (now - lastReset > RESET_DELAY && isconnected) PowerLogger.append(FIRMWARE_ID+" past reset delay", this); if (state.exists(oculusPrime.State.values.powererror)) { final String msg = "power PCB code: " + state.get(oculusPrime.State.values.powererror); application.message(msg, null, null); application.messageGrabber(msg, ""); Util.log(msg, this); PowerLogger.append(msg, this); } if (now - lastRead > DEAD_TIME_OUT && isconnected) { timeoutlogmessages(); reset(); Util.delay(ALLOW_FOR_RESET); } if (now - lastReset > RESET_DELAY && isconnected && !state.getBoolean(oculusPrime.State.values.autodocking) ){ application.message("power PCB periodic reset", "battery", "resetting"); Util.log("power PCB periodic reset", this); PowerLogger.append("power PCB periodic reset", this); reset(); Util.delay(ALLOW_FOR_RESET); } if (now - lastHostHeartBeat > HOST_HEARTBEAT_DELAY && isconnected) { sendCommand((byte) PING); // no response expected lastHostHeartBeat = now; } if (now - lastRead > ERROR_TIME_OUT && !isconnected) { // comm with pcb lost! if (state.exists(oculusPrime.State.values.powererror.toString())){ String err = state.get(oculusPrime.State.values.powererror); if (!err.matches(".*"+COMM_LOST+"$")) { // only set once // one last reset try reset(); long start = System.currentTimeMillis(); while (!isconnected && System.currentTimeMillis() - start < ALLOW_FOR_RESET) Util.delay(10); if (!isconnected) { // still not connected after waiting for reset state.set(oculusPrime.State.values.powererror, err + "," + COMM_LOST); } } } else { // one last reset try reset(); long start = System.currentTimeMillis(); while (!isconnected && System.currentTimeMillis() - start < ALLOW_FOR_RESET) Util.delay(10); if (!isconnected) // still not connected after waiting for reset state.set(oculusPrime.State.values.powererror, COMM_LOST); } } Util.delay(WATCHDOG_DELAY); } } } private void timeoutlogmessages() { state.set(oculusPrime.State.values.batterylife, "TIMEOUT"); application.message("power PCB timeout", "battery", "timeout"); Util.log("power PCB timeout", this); PowerLogger.append("power PCB timeout", this); } public void reset() { new Thread(new Runnable() { public void run() { writeStatusToEeprom(); // this may throw error if port lost Util.delay(100); Util.log("resetting Power board", this); PowerLogger.append("resetting Power board",this); disconnect(); connect(); initialize(); } }).start(); } private void checkFirmWareVersion() { if (!isconnected) return; firmwareversion = 0; sendCommand(GET_VERSION); long start = System.currentTimeMillis(); while(firmwareversion == 0 && System.currentTimeMillis() - start < 10000) { Util.delay(100); } if (firmwareversion == 0) { String msg = "failed to determine current "+FIRMWARE_ID+" firmware version"; Util.log("error, "+msg, this); PowerLogger.append(msg, this); state.set(State.values.guinotify, msg); return; } if (firmwareversion != FIRMWARE_VERSION_REQUIRED) { if (state.get(State.values.osarch).equals(Application.ARM)) {// TODO: add ARM avrdude to package! String msg = "current power firmware: "+firmwareversion+ " out of date! Update to: "+FIRMWARE_VERSION_REQUIRED; // state.set(State.values.guinotify, msg); Util.log(msg, this); PowerLogger.append(msg, this); return; } Util.log("Required "+FIRMWARE_ID+" firmware version is "+FIRMWARE_VERSION_REQUIRED+", attempting update...", this); String port = state.get(State.values.powerport); // disconnect() nukes this state value disconnect(); // TODO: do update here, blocking Updater.updateFirmware(FIRMWARE_ID, FIRMWARE_VERSION_REQUIRED, port); connect(); // check if successful firmwareversion = 0; sendCommand(GET_VERSION); start = System.currentTimeMillis(); while(firmwareversion == 0 && System.currentTimeMillis() - start < 10000) { Util.delay(100); } if (firmwareversion != FIRMWARE_VERSION_REQUIRED) { String msg = "unable to update " + FIRMWARE_ID + " firmware to version "+FIRMWARE_VERSION_REQUIRED; Util.log("error, "+msg, this); state.set(State.values.guinotify, msg); } } } public void serialEvent(SerialPortEvent event) { if (!event.isRXCHAR()) return; try { byte[] input = new byte[32]; if(serialPort == null){ Util.log("serial port is null", this); PowerLogger.append("serial port is null", this); return; } input = serialPort.readBytes(); for (int j = 0; j < input.length; j++) { if ((input[j] == '>') || (input[j] == 13) || (input[j] == 10)) { if (buffSize > 0) execute(); buffSize = 0; // reset lastRead = System.currentTimeMillis(); // last command from board }else if (input[j] == '<') { // start of message buffSize = 0; } else { buffer[buffSize++] = input[j]; // buffer until ready to parse } } } catch (SerialPortException e) { PowerLogger.append("serialEvent:" + e.getLocalizedMessage(), this); } } /** respond to feedback from the device */ public void execute() { String response = ""; for (int i = 0; i < buffSize; i++) response += (char) buffer[i]; PowerLogger.append("serial in: " + response, this); String s[] = response.split(" "); if(s[0].equals("version")) { Util.log(FIRMWARE_ID + " firmware version: " + s[1], this); firmwareversion = Double.valueOf(s[1]); return; } else if (s[0].equals("timeout")) { // TODO: never happens? no 'timeout' string in firmware 0.955 state.set(State.values.dockstatus, AutoDock.UNKNOWN); state.set(State.values.batterylife, s[0]); application.message(null, "battery", s[0]); return; } else if (s[0].equals("power_error")) { if (!s[1].contains(",")) { int e = Integer.parseInt(s[1]); if (IGNORE_ERROR.contains(e) && !state.exists(State.values.powererror.toString())) { if (e == ERROR_NO_BATTERY_CONNECTED) batterypresent = false; else { sendCommand(CLEARALLWARNINGERRORS); batterypresent = true; } Util.log("Power warning " + e + ", "+pwrerr.get(e)+", cleared", this); PowerLogger.append("Power warning "+e+", "+pwrerr.get(e)+", cleared", this); return; } } if (!s[1].equals("0")) { state.set(State.values.powererror, s[1]); application.message("from power PCB, code " + s[1], null, null); } else if (state.exists(State.values.powererror.toString())) { state.delete(State.values.powererror); application.message("power PCB code cleared", null, null); } return; } else if (s[0].equals("docked")) { if (!state.get(State.values.dockstatus).equals(AutoDock.DOCKED)) { application.message(null, "multiple", "dock " + AutoDock.DOCKED + " motion disabled"); state.set(State.values.dockstatus, AutoDock.DOCKED); if (state.getBoolean(State.values.autodocking) && !state.getBoolean(State.values.docking)) application.driverCallServer(PlayerCommands.move, ArduinoPrime.direction.stop.name()); // calls autodockcancel } state.set(State.values.redockifweakconnection, true); // TODO: testing } else if (s[0].equals("undocked")) { if (state.getBoolean(State.values.wallpower)) { state.set(State.values.wallpower, false); state.set(State.values.motionenabled, true); } if (!state.get(State.values.dockstatus).equals(AutoDock.UNDOCKED) && !state.getBoolean(State.values.autodocking)) { // redock if unplanned and redock set if (!state.exists(State.values.telnetusers.toString())) state.set(State.values.telnetusers, 0); if (!state.exists(State.values.driver.toString()) && state.getInteger(State.values.telnetusers) == 0 && settings.getBoolean(GUISettings.redock) && state.get(State.values.dockstatus).equals(AutoDock.DOCKED) && !state.getBoolean(State.values.forceundock) ) { Util.log("unplanned undock, trying redock",this); PowerLogger.append("unplanned undock, trying redock",this); application.driverCallServer(PlayerCommands.redock, null); } state.set(State.values.dockstatus, AutoDock.UNDOCKED); application.message(null, "multiple", "dock " + AutoDock.UNDOCKED + " motion enabled"); state.delete(State.values.redockifweakconnection); } } else if (s[0].equals("wallpower")) { if (!state.getBoolean(State.values.wallpower)) { state.set(State.values.wallpower, true); state.set(State.values.motionenabled, false); } } // debugenabled skips graceful shutdown, straight to power cut else if (s[0].equals("shutdown")) { sendCommand(CONFIRMSHUTDOWN); Util.log("POWER BOARD CALLED SYSTEM SHUTDOWN", this); PowerLogger.append("POWER BOARD CALLED SYSTEM SHUTDOWN", this); if (settings.getBoolean(ManualSettings.debugenabled)) { Util.debug("debugenabled, skipping graceful shutodwn"); PowerLogger.append("debugenabled, skipping graceful shutodwn", this); } else application.driverCallServer(PlayerCommands.powershutdown, null); } else if (s[0].equals("redock") && state.getUpTime() > Util.TWO_MINUTES) { if (!state.exists(State.values.redockifweakconnection)) state.set(State.values.redockifweakconnection, false); if(settings.getBoolean(ManualSettings.redockifweakconnection) && state.getBoolean(State.values.redockifweakconnection)) { application.driverCallServer(PlayerCommands.redock, null); PowerLogger.append("redock", this); Util.log("redock", this); } else { PowerLogger.append("error, redock due to weak connection, skipped", this); Util.log("error, redock due to weak connection, skipped", this); } } // else if (s[0].equals("force_undock")) { // moved to watchdog // state.set(State.values.forceundock, true); // Util.log("force undock", this); // PowerLogger.append("force undock", this); // } else if (s[0].equals("high_current")) { application.driverCallServer(PlayerCommands.move, ArduinoPrime.direction.stop.toString()); if (state.getBoolean(State.values.motionenabled)) application.driverCallServer(PlayerCommands.motionenabletoggle, null); application.message("high current detected", null, null); Util.log("error, high current warning, stopping motors, disabling motion", this); } if (s.length>2) { String battinfo = s[1]; if (batterypresent) battinfo = battinfo.replaceFirst("\\.\\d*", ""); else battinfo = "DISCONNECTED"; if (!state.get(State.values.batterylife).equals(battinfo)) { state.set(State.values.batterylife, battinfo); } String extinfo = s[2]; if (!state.exists(State.values.batteryinfo.toString()) || !state.get(State.values.batteryinfo).equals(extinfo)) { state.set(State.values.batteryinfo, extinfo); // extract sysvolts '_sV:' // Pattern pat = Pattern.compile("_sV:\\d+\\.\\d+"); // Matcher mat = pat.matcher(extinfo); // while (mat.find()) { // String sysvolts = mat.group().replaceFirst("_sV:", ""); // if ((!state.exists(State.values.sysvolts.toString()) || // !state.get(State.values.sysvolts).equals(sysvolts))) { //// && !state.getBoolean(State.values.moving) && ! state.getBoolean(State.values.wallpower)) { // // sysvolts only used by voltscomp, so only update when undocked and NOT moving // state.put(State.values.sysvolts, sysvolts); // } // break; // } // extract battvolts '_lV:' Pattern pat = Pattern.compile("_lV:\\d+\\.\\d+"); Matcher mat = pat.matcher(extinfo); while (mat.find()) { String battvolts = mat.group().replaceFirst("_lV:", ""); if (!state.exists(State.values.batteryvolts.toString()) ) { state.set(State.values.batteryvolts, battvolts); application.comport.setInitialOdomPWM(); } else if (!state.get(State.values.batteryvolts).equals(battvolts)) { state.set(State.values.batteryvolts, battvolts); } } } } } private void disconnect() { try { isconnected = false; state.delete(State.values.powerport); serialPort.closePort(); } catch (Exception e) { Util.log("error in disconnect(): " + e.getMessage(), this); } } public boolean isConnected() { return isconnected; } /** check if alive * BLOCKING * return true if connected, if not, wait for up to 10 seconds * @return boolean isconnected */ public boolean checkisConnectedBlocking() { long start = System.currentTimeMillis(); if (!isconnected) { while (!isconnected && System.currentTimeMillis() - start < ALLOW_FOR_RESET) Util.delay(50); } if (!isconnected) { // still not connected after waiting for rest Util.log("power pcb not connected", this); PowerLogger.append("power pcb not connected", this); return false; } // isconnected true, but could be in the midst of timing out... send ping start = System.currentTimeMillis(); lastRead = start; // set to now so not fighting with watchdog's 15 sec timeout sendCommand(GET_PRODUCT); // response expected immediately while (lastRead == start && System.currentTimeMillis() - start < 1000) Util.delay(1); if (lastRead == start) { // no response to ping, try reset once timeoutlogmessages(); isconnected = false; reset(); start = System.currentTimeMillis(); while (!isconnected && System.currentTimeMillis() - start < ALLOW_FOR_RESET) Util.delay(10); if (!isconnected) { // still not connected after waiting for rest Util.log("power pcb not connected after waiting for reset", this); PowerLogger.append("power pcb not connected after waiting for reset", this); return false; } } return true; } /** * Send a multiple byte command to send the device * * @param cmd * is a byte array of messages to send */ private void sendCommand(byte[] cmd) { String text = "sendCommand(): " + (char)cmd[0] + " "; for(int i = 1 ; i < cmd.length ; i++) text += ((byte)cmd[i] & 0xFF) + " "; // & 0xFF converts to unsigned byte PowerLogger.append(text, this); int n = 0; while (commandlock) { Util.delay(1); n++; } commandlock = true; if (n!=0) Util.log("error, commandlock true for "+n+"ms", this); for (byte b : cmd) commandList.add(b); commandList.add((byte) 13); // EOL commandlock = false; } private void sendCommand(final byte cmd){ sendCommand(new byte[]{cmd}); } /** inner class to send commands to port in sequential order */ public class CommandSender extends Thread { public CommandSender() { this.setDaemon(true); } public void run() { while (true) { if (commandList.size() > 0 &! commandlock) { if (commandList.size() > 15) { commandList.clear(); Util.log("error, command stack up, all dropped", this); Util.delay(1); continue; } int EOLindex = commandList.indexOf((byte) 13); if (EOLindex == -1) { Util.delay(1); continue; } // in case of multiple EOL chars, read only up to 1st byte c[] = new byte[EOLindex+1]; try { for (int i = 0; i <= EOLindex; i++) { c[i] = commandList.get(0); commandList.remove(0); } } catch (Exception e) { Util.log("sendCommand() error, dropped, continuing: ", this); Util.printError(e); commandList.clear(); Util.delay(1); continue; } try { serialPort.writeBytes(c); // writing as array ensures goes at baud rate? } catch (Exception e) { Util.log("sendCommand() error", this); // , attempting reset", this); Util.printError(e); } } Util.delay(1); } } } public void shutdown() { if (state.get(State.values.dockstatus).equals(AutoDock.DOCKED)) { application.message("can't power down when docked", null, null); } else sendCommand(INITIATESHUTDOWN); } public void writeStatusToEeprom() { sendCommand(STATUSTOEEPROM); } /** @param str command character followed by nothing or unsigned short (0-65536) */ public void powercommand(String str) { String s[] = str.split(" "); if (s.length == 0) return; else if (s.length == 1) sendCommand(str.getBytes()); else { int val = Integer.parseInt(s[1]); if (val <= 255) sendCommand(new byte[]{s[0].getBytes()[0], (byte) val}); else { byte val1 = (byte) (val & 0xFF); byte val2 = (byte) ((val >>8) & 0xFF); sendCommand(new byte[]{s[0].getBytes()[0], val1, val2}); } } } public void clearWarningErrors() { boolean warningonly = true; boolean resetrequired = false; String code[] = state.get(State.values.powererror).split(","); for (int i=0; i < code.length; i++) { int c = Integer.parseInt(code[i]); if (c > WARNING_ONLY_BELOW ) warningonly = false; if (c > RESET_REQUIRED_ABOVE) resetrequired = true; } if (! warningonly) return; sendCommand(CLEARALLWARNINGERRORS); if (resetrequired) reset(); // state.powererror gets cleared by firmware if command succeeds } }