package de.uniluebeck.itm.wsn.drivers.telosb; import com.google.inject.Inject; import com.google.inject.Singleton; import de.uniluebeck.itm.util.StringUtils; import de.uniluebeck.itm.util.TimeDiff; import de.uniluebeck.itm.wsn.drivers.core.exception.*; import de.uniluebeck.itm.wsn.drivers.core.serialport.SerialPortConnection; import gnu.io.SerialPort; import gnu.io.UnsupportedCommOperationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import static com.google.common.base.Preconditions.checkArgument; @Singleton public class BSLTelosb { private static final Logger log = LoggerFactory.getLogger(BSLTelosb.class); /** * Possible values for msp430 baud rate */ public static enum BaudRate { /** * 9600 baud (initial rate) */ Baud9600, /** * 19200 baud */ Baud19200, /** * 38000 baud */ Baud38000; public String toString() { if (this == Baud19200) { return "19200"; } else if (this == Baud38000) { return "38400"; } else { return "9600"; } } /** * @return int value of baud rate */ public int toInt() { if (this == Baud19200) { return 19200; } else if (this == Baud38000) { return 38400; } else { return 9600; } } } /** boot loader command ids */ /** * mass erase */ public static final int CMD_MASSERASE = 0x18; /** * transmit password */ public static final int CMD_TXPASSWORD = 0x10; /** * transmit data block */ public static final int CMD_TXDATABLOCK = 0x12; /** * load program counter */ public static final int CMD_LOADPC = 0x1A; /** * receive data block */ public static final int CMD_RXDATABLOCK = 0x14; /** * receive bsl version */ public static final int CMD_RXBSLVERSION = 0x1E; /** * change baud rate */ public static final int CMD_CHANGEBAUD = 0x20; /** * ACK */ public static final int DATA_ACK = 0x90; /** * NO ACK */ public static final int DATA_NACK = 0xA0; /* bsl synchronization byte */ private final int BSL_SYNC = 0x80; /* bsl synchronization acknowledge byte */ private final int SYNC_ACK = 0x90; /* hdr byte (1st byte of any bsl command message) */ private final int BSL_HDR = 0x80; /** * byte value constants for bsl communication */ /* time out for waiting for a message reply of the connected device */ private static final int DEFAULT_REPLY_TIMEOUT_MILLIS = 2000; /* set to true if the patch required by the bsl was loaded into device memory * and is ready to be executed when needed */ private boolean bslPatchLoaded = false; /* current baud rate used for communicating with the bsl */ private BaudRate currentBaudRate = BaudRate.Baud9600; public final Object dataAvailableMonitor = new Object(); int oldBaudRate; boolean bslBaudRateSet = false; private final SerialPortConnection connection; @Inject public BSLTelosb(SerialPortConnection connection) { this.connection = connection; } /** * Initializes bsl communication by resetting the device. * * @return true if BSL was started successfully */ public boolean invokeBSL() { TelosI2CCom i2cCom = new TelosI2CCom(connection.getSerialPort()); log.debug("invokeBSL()"); // send commands via I2C to reset device and invoke boot loader i2cCom.writeCommand(0, 1); i2cCom.writeCommand(0, 3); i2cCom.writeCommand(0, 1); i2cCom.writeCommand(0, 3); i2cCom.writeCommand(0, 2); i2cCom.writeCommand(0, 0); i2cCom.writeCommand(0, 0); waitForMpOscillatorToStabilize(); return true; } private void waitForMpOscillatorToStabilize() { try { Thread.sleep(250); } catch (InterruptedException e) { log.error("InterruptedException while waiting for mp oscillator to stabilize: {}", e); throw new RuntimeException(e); } } /** * Reset the device. * * @return true if reset successfully */ public boolean reset() throws IOException { TelosI2CCom i2cCom = new TelosI2CCom(connection.getSerialPort()); log.debug("reset()"); // reset device by sending commands via I2C i2cCom.writeCommand(0, 3); i2cCom.writeCommand(0, 2); i2cCom.writeCommand(0, 0); waitForMpOscillatorToStabilize(); connection.clear(); // set baud rate back to standard when initializing bsl communication the next time currentBaudRate = BaudRate.Baud9600; return true; } /** * Send BSL command with given address, length and data. * Return the received answer of the bsl. * * @param cmd * command byte (CMD) * @param address * high and low address bytes (AL and AH) * @param length * number of data bytes or other info (LL and LH) * @param data * array of data bytes (D1...Dn), may be null * @param wait * if true, BSL sync will be repeated forever until it succeeds * * @throws UnexpectedResponseException */ public void sendBSLCommand(int cmd, int address, int length, @Nullable byte data[], boolean wait) throws TimeoutException, IOException, UnexpectedResponseException { checkArgument(data == null || data.length <= 250, "Number of bytes in data must not exceed 250 bytes!"); byte[] frame; int frameLength; int l1l2; int checksum; int lllh; // in case of uneven data length, append one byte to make it even if (data != null) { if (data.length % 2 == 0) { // HDR + CMD + L1|L2 + AL|AH + LL|LH + DATA + CHK frameLength = 1 + 1 + 2 + 2 + 2 + data.length + 2; // L1|L2 + AL|AH + DATA l1l2 = 2 + 2 + data.length; lllh = data.length; } else { // HDR + CMD + L1|L2 + AL|AH + LL|LH + DATA + PADDING + CHK frameLength = 1 + 1 + 2 + 2 + 2 + data.length + 1 + 2; // L1|L2 + AL|AH + DATA + PADDING l1l2 = 2 + 2 + data.length + 1; lllh = data.length + 1; } } else { // HDR + CMD + L1|L2 + AL|AH + LL|LH + CHK frameLength = 1 + 1 + 2 + 2 + 2 + 2; // L1|L2 + AL|AH l1l2 = 2 + 2; lllh = length; } // create data frame to send frame = new byte[frameLength]; frame[0] = (byte) BSL_HDR; // HDR frame[1] = (byte) cmd; // CMD frame[2] = (byte) l1l2; // L1 frame[3] = (byte) l1l2; // L2 frame[4] = (byte) ((address) & 0xFF); // AL frame[5] = (byte) ((address >> 8) & 0xFF); // AH frame[6] = (byte) ((lllh) & 0xFF); // LL frame[7] = (byte) ((lllh >> 8) & 0xFF); // LH if (data != null) { // copy data bytes to frame System.arraycopy(data, 0, frame, 8, data.length); if (data.length % 2 != 0) { frame[8 + data.length] = (byte) 0xFF; // fill up with padding for even number of bytes } } // calculate and add checksum checksum = calcChecksum(frame, frameLength - 2); // without CKL|CKH frame[frameLength - 2] = (byte) (checksum & 0xFF); // CKL frame[frameLength - 1] = (byte) ((checksum >> 8) & 0xFF); // CKH // synchronize BSL before sending a command if (!bslSynchronize(wait)) { throw new UnexpectedResponseException("BSL sync failed while sending bsl command.", DATA_ACK, DATA_NACK); } // send frame OutputStream outputStream = connection.getOutputStream(); outputStream.write(frame); outputStream.flush(); if (log.isDebugEnabled()) { String frameString = ""; for (final byte b : frame) { frameString += String.format("%02x ", b); } log.debug("txFrame: " + frameString); } } /** * Receive bsl reply to a previously sent command * * @return bsl reply (ACK, NACK or replied data D0..Dn) * * @throws IOException * @throws TimeoutException * @throws InvalidChecksumException * @throws ReceivedIncorrectDataException * @throws UnexpectedResponseException */ public byte[] receiveBSLReply() throws IOException, TimeoutException, InvalidChecksumException, ReceivedIncorrectDataException, UnexpectedResponseException { byte[] dataNoHeader; int reply; byte[] tempData; byte[] result; int receivedChecksumL; int receivedChecksumH; int lengthFrameData; int numBytesRead; int numTries; int checksum; byte[] dataFrame; String frameString; InputStream inputStream = connection.getInputStream(); waitDataAvailable(inputStream, DEFAULT_REPLY_TIMEOUT_MILLIS); reply = inputStream.read(); if (reply == DATA_ACK) { // acknowledge received // if (log.isDebugEnabled()) { // log.debug("Received bsl ACK"); // } result = (new byte[]{(byte) reply}); } else if (reply == DATA_NACK) { // no acknowledge received if (log.isWarnEnabled()) { log.warn("Received BSL NACK"); } result = (new byte[]{(byte) reply}); } else if (reply == BSL_HDR) { // receiving a data frame dataFrame = new byte[4]; dataFrame[0] = (byte) reply; // read header numBytesRead = 0; numTries = 0; while (true) { waitDataAvailable(inputStream, DEFAULT_REPLY_TIMEOUT_MILLIS); numBytesRead += inputStream.read(dataFrame, 1 + numBytesRead, 3 - numBytesRead); if (numBytesRead != 3) { if (numTries < 3) { // if (log.isDebugEnabled()) { // log.debug("Time out receiving bsl reply, retrying..."); // } } else { frameString = ""; for (int i = 0; i < numBytesRead; i++) { frameString += String.format(" 0x%02x ", dataFrame[i]); } throw new TimeoutException( "Time out receiving BSL reply data.\nData received so far: " + frameString ); } } else { break; } numTries++; } // check if frame header is correct if ((dataFrame[1] != 0x00) || (dataFrame[2] != dataFrame[3])) { if (log.isDebugEnabled()) { log.debug("Header of received bsl reply is corrupt"); } throw new ReceivedIncorrectDataException("Header of received BSL reply is corrupt."); } // extend array length for received data tempData = new byte[dataFrame.length]; System.arraycopy(dataFrame, 0, tempData, 0, dataFrame.length); lengthFrameData = (0xFF & dataFrame[2]); dataFrame = new byte[4 + lengthFrameData]; System.arraycopy(tempData, 0, dataFrame, 0, tempData.length); // read frame data excluding the checksum numBytesRead = 0; numTries = 0; while (true) { waitDataAvailable(inputStream, DEFAULT_REPLY_TIMEOUT_MILLIS); numBytesRead += inputStream.read(dataFrame, 4 + numBytesRead, lengthFrameData - numBytesRead); if (numBytesRead != lengthFrameData) { if (numTries < 5) { // if (log.isDebugEnabled()) { // log.debug("Time out receiving bsl reply. Retrying to receive data..."); // } } else { frameString = ""; for (int i = 0; i < numBytesRead + 4; i++) { frameString += String.format(" 0x%02x ", dataFrame[i]); } throw new TimeoutException("Time out receiving BSL reply data (was expecting " + lengthFrameData + "bytes but received " + numBytesRead + " instead. " + "\nData Received so far: " + frameString ); } } else { break; } numTries++; } // read and validate checksum waitDataAvailable(inputStream, DEFAULT_REPLY_TIMEOUT_MILLIS); receivedChecksumL = inputStream.read(); receivedChecksumH = inputStream.read(); if (receivedChecksumH == -1 || receivedChecksumL == -1) { frameString = ""; for (final byte aDataFrame : dataFrame) { frameString += String.format(" 0x%02x ", aDataFrame); } throw new TimeoutException("Time out receiving BSL reply: missing checksum in data frame. " + "\nData received so far: " + frameString ); } // if (log.isDebugEnabled()) { // frameString = ""; // for (int i=0; i<dataFrame.length; i++) { // frameString += String.format(" 0x%02x ", dataFrame[i]); // } // log.debug("Received bsl reply:"+frameString); // } checksum = calcChecksum(dataFrame, dataFrame.length); if ((receivedChecksumL != (checksum & 0xFF)) || (receivedChecksumH != ((checksum >> 8) & 0xFF))) { throw new InvalidChecksumException(String.format("Wrong checksum receiving BSL reply: " + "was: 0x%02x 0x%02x but should be: 0x%02x 0x%02x", receivedChecksumL, receivedChecksumH, checksum & 0xFF, (checksum >> 8) & 0xFF ) ); } // complete frame received correctly, return data without header dataNoHeader = new byte[dataFrame.length - 4]; System.arraycopy(dataFrame, 4, dataNoHeader, 0, dataNoHeader.length); result = dataNoHeader; } else { throw new UnexpectedResponseException("Received unknown BSL reply.", DATA_ACK, (0xFF & reply)); } return result; } /** * Transmit 32 byte password to the boot loader to unlock all password protected commands * * @param password * the password * @param wait * if true, bsl sync will try forever * * @return true if received ACK, false if received NACK * * @throws IOException * @throws TimeoutException * @throws InvalidChecksumException * @throws ReceivedIncorrectDataException * @throws UnexpectedResponseException */ public boolean transmitPassword(byte[] password, boolean wait) throws IOException, TimeoutException, InvalidChecksumException, ReceivedIncorrectDataException, UnexpectedResponseException { byte[] pwData; byte[] reply; if (log.isDebugEnabled()) { log.debug("transmitPassword()"); } if (password == null) { // transmit default password (all bytes 0xFF) pwData = new byte[32]; for (int i = 0; i < pwData.length; i++) { pwData[i] = (byte) 0xFF; } if (log.isDebugEnabled()) { log.debug("Default password transmitted."); } } else { // password must consist of 32 bytes if (password.length != 32) { log.error( "Error transmitting BSL password: password length of " + password.length + " bytes is not correct" ); return false; } else { pwData = password; if (log.isDebugEnabled()) { log.debug("Password [" + StringUtils.toHexString(password) + "] transmitted."); } } } sendBSLCommand(CMD_TXPASSWORD, 0xFFE0, // start address is always 0xFFE0 0x20, // password length is always 32 (=0x20) bytes pwData, wait ); reply = receiveBSLReply(); if ((reply[0] & 0xFF) == DATA_ACK) { return true; } else if ((reply[0] & 0xFF) == DATA_NACK) { return false; } else { throw new UnexpectedResponseException("Received unknown reply while sending BSL password.", DATA_ACK, (reply[0] & 0xFF) ); } } /** * Prepare BSL patch by directing program counter of the mote to start address of the patch code. * This patch is required by BSL version 1.10 to execute commands for reading/writing blocks correctly. * * @return true if ACK received, false if NACK received * * @throws IOException * @throws TimeoutException * @throws InvalidChecksumException * @throws ReceivedIncorrectDataException * @throws UnexpectedResponseException */ public boolean executeBSLPatch() throws IOException, TimeoutException, InvalidChecksumException, ReceivedIncorrectDataException, UnexpectedResponseException { byte[] reply; if (bslPatchLoaded) { sendBSLCommand(CMD_LOADPC, 0x0220, 0, null, false); reply = receiveBSLReply(); if (reply[0] == (0xFF & DATA_ACK)) { return true; } else if (reply[0] == (0xFF & DATA_NACK)) { return false; } else { throw new UnexpectedResponseException("Received unknown reply while executing BSL patch.", DATA_ACK, (reply[0] & 0xFF) ); } } return true; } /** * Verify a memory block of given length against given data or 0xFF (erased data) * * @param address * address of data block to verify * @param length * number of bytes to verify * @param data * data against which to verify or null if to verify against 0xFF * * @return true if checked data matches supplied data, false otherwise * * @throws TimeoutException * @throws InvalidChecksumException * @throws IOException * @throws ReceivedIncorrectDataException * @throws UnexpectedResponseException */ public boolean verifyBlock(int address, int length, byte[] data) throws TimeoutException, InvalidChecksumException, IOException, ReceivedIncorrectDataException, UnexpectedResponseException { byte[] reply; int checkedLength; String dataString = ""; // if (log.isDebugEnabled()) { // if (data != null) { // log.debug(String.format("Verifying data at 0x%02x, %d bytes", address, length)); // } else { // log.debug(String.format("Verifying at 0x%02x that %d bytes are erased correctly", address, length)); // } // } checkedLength = length; if (data != null) { if (length > data.length) { log.warn( "Warning: supplied length for block verification(" + length + ") does not match length of data." ); checkedLength = data.length; } } // execute bsl patch executeBSLPatch(); // receive data block sendBSLCommand(CMD_RXDATABLOCK, address, checkedLength, null, false); reply = receiveBSLReply(); if (((0xFF & reply[0]) == DATA_NACK) && reply.length == 1) { throw new ReceivedIncorrectDataException("Failed to verify data block: received NACK."); } if (reply.length != length) { for (int i = 0; i < reply.length; i++) { dataString += String.format("0x%02x ", reply[i]); } throw new ReceivedIncorrectDataException("Failed to verify data block: " + "length of bsl reply is unexpected. Reply: " + dataString ); } // verify data for (int i = 0; i < reply.length; i++) { if (data == null) { // check against 0xFF if ((reply[i] & 0xFF) != 0xFF) { if (log.isDebugEnabled()) { log.debug(String.format("Error validating block at 0x%02x, byte %d. " + "Was: 0x%02x, but should be: 0xFF", address, i + 1, reply[i] ) ); } return false; } } else { // check against supplied data if (reply[i] != data[i]) { if (log.isDebugEnabled()) { log.debug(String.format("Error validating block at 0x%02x, byte %d. " + "Was: 0x%02x, but should be: 0x%02x", address, i + 1, reply[i], data[i] ) ); } return false; } } } return true; } /** * Change the baud rate for data transmissions to the boot loader. * Valid baud rates are 9600, 19200, 38000. Baud rate for bsl communication * can only be changed by using this bsl command. * * @param newBaud * new baud rate to set * * @return true if baud rate was changed successfully, false if no ACK was received * * @throws TimeoutException * @throws UnexpectedResponseException * @throws IOException * @throws InvalidChecksumException * @throws ReceivedIncorrectDataException */ public boolean changeBaudRate(BaudRate newBaud) throws TimeoutException, UnexpectedResponseException, IOException, InvalidChecksumException, ReceivedIncorrectDataException { byte[] reply; int a; int l; BaudRate newBaudRate; switch (newBaud) { case Baud19200: a = 0x85E0; l = 0x0001; newBaudRate = BaudRate.Baud19200; break; case Baud38000: a = 0x87E0; l = 0x0002; newBaudRate = BaudRate.Baud38000; break; case Baud9600: default: a = 0x8580; l = 0x0000; newBaudRate = BaudRate.Baud9600; } // send bsl command sendBSLCommand(CMD_CHANGEBAUD, a, l, null, false); reply = receiveBSLReply(); if ((0xFF & reply[0]) == DATA_NACK) { log.error("Failed to change baud rate, received NACK."); return false; } else if ((0xFF & reply[0]) == DATA_ACK) { log.debug("Changing baud rate to " + newBaudRate + "..."); } else if (reply.length != 1) { log.error("Failed to change baud rate, received unexpected reply of length " + reply.length); return false; } // set new baud rate for serial port SerialPort serialPort = connection.getSerialPort(); try { serialPort.setSerialPortParams(newBaudRate.toInt(), serialPort.getDataBits(), serialPort.getStopBits(), serialPort.getParity() ); } catch (UnsupportedCommOperationException e) { throw new IOException("Error changing baud rate: " + e); } currentBaudRate = newBaudRate; return true; } /** * Change the baud rate and parity information for data transmissions. * * @param newBaudRate * new baud rate to set * * @return true if baud rate was changed successfully, false if no ACK was received * * @throws IOException */ public boolean changeComPort(int newBaudRate, int newParity) throws IOException { // set new baud rate for serial port SerialPort serialPort = connection.getSerialPort(); try { serialPort.setSerialPortParams(newBaudRate, serialPort.getDataBits(), serialPort.getStopBits(), newParity ); } catch (UnsupportedCommOperationException e) { throw new IOException("Error changing baud rate: " + e); } return true; } /* * Send synchronization byte to initiate sending of a bsl command * @param wait if true, retry infinitely often to sync * @return true, if sync was successful (ACK received), false otherwise */ private boolean bslSynchronize(boolean wait) throws TimeoutException, IOException { int answer; int maxTries = 10; //TODO: check for right baud rate while (wait || (maxTries > 0)) { // clear input stream flushInputStream(); maxTries--; // send sync byte and read answer connection.getOutputStream().write(BSL_SYNC); answer = connection.getInputStream().read(); if (answer == SYNC_ACK) { // ack received // if (log.isDebugEnabled()) { // log.debug("BSL sync: received ACK"); // } return true; } else if (answer == -1) { // nothing received //TODO: retry to reset? if (maxTries == 0) { // was last try // if (log.isDebugEnabled()) { // log.debug("BSL sync time out"); // } throw new TimeoutException("Time out waiting for BSL sync ACK."); } // retry to sync if (log.isDebugEnabled()) { log.debug("BSL sync time out, retry..."); } } else { // no ack received // if (log.isDebugEnabled()) { // log.debug(String.format("BSL sync: missing ACK (answer was: 0x%02x)",answer)); // } return false; } } return false; } /* * Calculate checksum of a complete given bsl command message */ private int calcChecksum(byte frame[], int length) { int checksum = 0; for (int i = 0; i < (length / 2); i++) { checksum = checksum ^ ((0xFF & frame[2 * i]) + ((0xFF & frame[2 * i + 1]) << 8)); } return 0xFFFF & (checksum ^ 0xFFFF); } /* * Wait for data being available from serial port. Throw TimeoutException after * specified time out. */ private int waitDataAvailable(InputStream inputStream, int timeoutMillis) throws TimeoutException, IOException { TimeDiff timeDiff = new TimeDiff(); int avail = 0; while (inputStream != null && (avail = inputStream.available()) == 0) { if (timeoutMillis > 0 && timeDiff.ms() >= timeoutMillis) { throw new TimeoutException( "Timeout waiting for data (waited: " + timeDiff.ms() + ", timeoutMs:" + timeoutMillis + ")" ); } synchronized (dataAvailableMonitor) { try { dataAvailableMonitor.wait(50); } catch (InterruptedException e) { log.error("Error " + e); } } } return avail; } /* * Flush the input buffer. */ private void flushInputStream() { long i; //log.debug("flushInputStream()"); try { InputStream inputStream = connection.getInputStream(); while ((i = inputStream.available()) > 0) { //noinspection ResultOfMethodCallIgnored inputStream.skip(i); } } catch (IOException e) { log.warn("Error while flushing serial input stream: " + e, e); } } /** * Set the baud rate to the appropriate value necessary for bsl communication, in case another * baud rate was used before for communication with the os. * * @throws IOException */ public void setBslBaudRate() throws IOException { if (bslBaudRateSet) { return; } SerialPort serialPort = connection.getSerialPort(); oldBaudRate = serialPort.getBaudRate(); try { serialPort.setSerialPortParams(currentBaudRate.toInt(), serialPort.getDataBits(), serialPort.getStopBits(), serialPort.getParity() ); log.debug("Baud rate changed for bsl communication from {} to {}", oldBaudRate, currentBaudRate.toInt()); } catch (UnsupportedCommOperationException e) { throw new IOException(e.getMessage()); } bslBaudRateSet = true; } /** * Restore the external baud rate used by the serial port before * the baud rate was changed for bsl communication. * * @throws IOException */ public void restoreNonBslBaudRate() throws IOException { if (!bslBaudRateSet) { return; } SerialPort serialPort = connection.getSerialPort(); try { serialPort.setSerialPortParams(oldBaudRate, serialPort.getDataBits(), serialPort.getStopBits(), serialPort.getParity() ); log.debug("Baud rate changed back after bsl communication to " + oldBaudRate + "."); } catch (UnsupportedCommOperationException e) { throw new IOException(e.getMessage()); } bslBaudRateSet = false; } public void writeFlash(int address, byte[] bytes, int len) throws IOException, FlashProgramFailedException, TimeoutException, InvalidChecksumException, ReceivedIncorrectDataException, UnexpectedResponseException { sendBSLCommand(BSLTelosb.CMD_TXDATABLOCK, address, len, bytes, false); final byte[] reply = receiveBSLReply(); final int responseCode = (reply[0] & 0xFF); if (responseCode == BSLTelosb.DATA_NACK) { throw new FlashProgramFailedException("Failed to program flash: received NACK from BSL!"); } else if (responseCode != BSLTelosb.DATA_ACK) { throw new FlashProgramFailedException("Failed to program flash: received no ACK but: " + responseCode); } } }