package net.solarnetwork.node.hw.sma.sunnynet; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.EnumMap; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * SMA communication packet. * * <p> * This is the basic unit of communication for requests to and responses from * the SMA controller. The data is encoded into/decoded from a byte array (the * {@code packet} field) from/to other fields on this class. * </p> * * <p> * The packet structure consists of a fixed-length header, a variable-length * {@code userData} section, and a fixed-length footer. The header contains * information like the length of the user data section, the source and * destination addresses, and the command type. The user data section contains * any data required to pass with the command, or sent back from the controller. * The footer contains a checksum value calculated from parts of the header and * the complete user data section. * </p> * * <p> * The special {@code userDataFields} Map will contain data decoded from the * user data of response packets, such as the value returned by GetData * commands. * </p> * * <p> * The SMA controller uses little endian encoding in all communication. * </p> * * <pre> * # 2 optional sync bytes * # 1 telegram start byte (0x68) * # 1 user length byte * # 1 user length byte repeated * # 1 telegram start byte (0x68) * ### the checksum data block starts here * # 2 source address bytes * # 2 destination address bytes * # 1 control byte (0x40) * # 1 packet counter byte * # 1 command type byte * # X user data bytes * ### the checksum data block ends here * # 2 checksum bytes * # 1 end character byte (0x16) * </pre> */ public final class SmaPacket { /** A "wakeup" packet byte. */ public static final byte WAKEUP = (byte) 0xAA; /** A "telegram" packet byte. */ public static final byte TELEGRAM = (byte) 0x68; /** An "end" packet byte. */ public static final byte END = (byte) 0x16; /** An empty byte array, useful for specifying empty user data. */ public static final byte[] EMPTY_DATA = new byte[0]; private static final Logger LOG = LoggerFactory.getLogger(SmaPacket.class); private byte[] packet; private byte[] userData = null; private int srcAddress = 0; private int destAddress = 0; private SmaControl control = null; private int packetCounter = 0; private SmaCommand command = null; private int crc = 0; private int computedCrc = 0; private Map<SmaUserDataField, Object> userDataFields = null; /** * Construct a request packet. * * <p> * This constructor can be used for any type of packet, but generally will * be used to construct a request packet to send to the controller. * </p> * * @param src * the source address (usually 0) * @param dest * the destination address (use 0 for a RequestGroup control type) * @param packetCounter * the packet counter (usually 0) * @param control * the request type (usually RequestSingle or RequestGroup) * @param command * the command to issue * @param userData * the data to send with the command */ public SmaPacket(int src, int dest, int packetCounter, SmaControl control, SmaCommand command, byte[] userData) { this.srcAddress = src; this.destAddress = dest; this.control = control; this.command = command; this.userData = userData; this.packetCounter = packetCounter; encodePacket(); } /** * Construct a response packet. * * <p> * This constructor will decode a raw packet byte array into the individual * fields on this class. * </p> * * @param packet * the raw packet data to decode */ public SmaPacket(byte[] packet) { this.packet = packet; decodePacket(); } /** * Get a specific user data field value. * * @param key * the key of the field to get * @return the associated value, or <em>null</em> if not available */ public Object getUserDataField(SmaUserDataField key) { if ( userDataFields == null ) { return null; } return userDataFields.get(key); } /** * Return <em>true</em> if this packet is valid according to the packet's * CRC check. * * @return boolean */ public boolean isValid() { return crc != 0 && computedCrc == crc && command != null && command != SmaCommand.Unknown; } @Override public String toString() { return "SmaPacket{command=" + command + ",packetCounter=" + packetCounter + ",crc=" + crc + ",control=" + control + ",valid=" + isValid() + '}'; } private void encodePacket() { byte[] req = new byte[13 + this.userData.length + 3]; req[0] = WAKEUP; // wakeup req[1] = WAKEUP; req[2] = TELEGRAM; // telegram req[3] = (byte) this.userData.length; // length of data, twice req[4] = req[3]; req[5] = TELEGRAM; // telegram req[6] = (byte) 0x00; // source address req[7] = req[6]; req[8] = (byte) (this.destAddress & 0xFF); // destination (low byte) req[9] = (byte) ((this.destAddress >> 8) & 0xFF);// destination (high byte) req[10] = (byte) this.control.getCode(); req[11] = (byte) this.packetCounter; req[12] = (byte) this.command.getCode(); System.arraycopy(this.userData, 0, req, 13, this.userData.length); this.crc = 0; for ( int i = 6; i < 13 + this.userData.length; i++ ) { this.crc += (0xFF & req[i]); } req[req.length - 3] = (byte) (this.crc & 0xFF); // computedCrc low byte req[req.length - 2] = (byte) ((this.crc >> 8) & 0xFF); // computedCrc high byte req[req.length - 1] = END; this.computedCrc = this.crc; this.packet = req; } private void decodePacket() { if ( packet == null || packet.length < 1 ) { return; } int idx = 0; // skip (optional) WAKEUP bytes while ( packet[idx] == WAKEUP ) { idx++; } // next is TELEGRAM, user data length, user data length, TELEGRAM idx++; int dataLength = 0xFF & packet[idx]; userData = new byte[dataLength]; idx += 3; // CRC data starts now for ( int i = 0; i < (7 + dataLength); i++, idx++ ) { int b = 0xFF & packet[idx]; switch (i) { case 0: srcAddress = b; break; case 1: srcAddress |= (b << 8); break; case 2: destAddress = b; break; case 3: destAddress |= (b << 8); break; case 4: control = SmaControl.forCode(b); break; case 5: packetCounter = b; break; case 6: command = SmaCommand.forCode(b); break; default: userData[i - 7] = packet[idx]; } computedCrc += b; } // CRC two bytes crc = (0xFF & packet[idx++]) | ((0xFF & packet[idx++]) << 8); // final byte should be 0x16 idx++; // we may have more data collected that a single packet, so if more // adjust our packet to the correct length, so calling code knows how // many bytes we consumed constructing this packet if ( idx < packet.length ) { if ( LOG.isTraceEnabled() ) { LOG.trace("Adjusting packet length from " + packet.length + " to " + idx); } byte[] adjustedPacket = new byte[idx]; System.arraycopy(packet, 0, adjustedPacket, 0, idx); this.packet = adjustedPacket; } } /** * Decode the user data into a Map of {@link SmaUserDataField} keys and * associated values. * * <p> * After calling this method, use * {@link #getUserDataField(SmaUserDataField)} to obtain values for specific * fields. * </p> */ public void decodeUserDataFields() { Map<SmaUserDataField, Object> results = new EnumMap<SmaUserDataField, Object>( SmaUserDataField.class); if ( command != null ) { switch (command) { case NetStart: decodeNetUserDataFields(results); break; case GetChannelInfo: decodeChannelInfoUserDataFields(results); break; case GetData: decodeGetDataUserDataFields(results); break; case SetData: decodeSetDataUserDataFields(results); break; default: // nothing to do break; } } this.userDataFields = results; } private void decodeGetDataUserDataFields(Map<SmaUserDataField, Object> results) { // # 1 request type1 // # 1 request type2 // # 1 channel number (index) // # 2 number of data sets // # 4 seconds since // # 4 time basis // # other stuff if ( userData.length < 13 ) { return; } SmaChannelType type1 = decodeGetSetUserDataFieldsHeader(results); results.put(SmaUserDataField.SecondsSince, (0xFF & userData[5]) | ((0xFF & userData[6]) << 8) | ((0xFF & userData[7]) << 16) | ((0xFF & userData[8]) << 24)); results.put(SmaUserDataField.TimeBasis, (0xFF & userData[9]) | ((0xFF & userData[10]) << 8) | ((0xFF & userData[11]) << 16) | ((0xFF & userData[12]) << 24)); switch (type1) { case Analog: // # 2 byte integer results.put(SmaUserDataField.Value, (0xFF & userData[13]) | ((0xFF & userData[14]) << 8)); break; case Digital: // # 16 byte char // # 16 byte char results.put(SmaUserDataField.TextLow, SmaUtils.parseString(userData, 13, 16)); results.put(SmaUserDataField.TextHigh, SmaUtils.parseString(userData, 29, 16)); break; case Counter: // # 4 byte integer results.put(SmaUserDataField.Value, (0xFF & userData[13]) | ((0xFF & userData[14]) << 8) | ((0xFF & userData[15]) << 16) | ((0xFF & userData[16]) << 24)); break; case Status: // # 4-byte char results.put(SmaUserDataField.Value, SmaUtils.parseString(userData, 13, 4)); break; default: results.put(SmaUserDataField.Error, "Unknown user data type"); break; } if ( LOG.isTraceEnabled() ) { LOG.trace("Decoded GetData userDataFields: " + results); } } private void decodeSetDataUserDataFields(Map<SmaUserDataField, Object> results) { // # 1 request type1 // # 1 request type2 // # 1 channel number (index) // # 2 number of data sets if ( userData.length < 5 ) { return; } decodeGetSetUserDataFieldsHeader(results); if ( LOG.isTraceEnabled() ) { LOG.trace("Decoded SetData userDataFields: " + results); } } private SmaChannelType decodeGetSetUserDataFieldsHeader(Map<SmaUserDataField, Object> results) { // # 1 request type1 // # 1 request type2 // # 1 channel number (index) // # 2 number of data sets SmaChannelType type1 = SmaChannelType.forCode(0xFF & userData[0]); results.put(SmaUserDataField.ChannelType1, type1); results.put(SmaUserDataField.ChannelType2, 0xFF & userData[1]); results.put(SmaUserDataField.ChannelIndex, 0xFF & userData[2]); results.put(SmaUserDataField.DataSets, (0xFF & userData[3]) | ((0xFF & userData[4]) << 8)); results.put(SmaUserDataField.SecondsSince, (0xFF & userData[5]) | ((0xFF & userData[6]) << 8) | ((0xFF & userData[7]) << 16) | ((0xFF & userData[8]) << 24)); results.put(SmaUserDataField.TimeBasis, (0xFF & userData[9]) | ((0xFF & userData[10]) << 8) | ((0xFF & userData[11]) << 16) | ((0xFF & userData[12]) << 24)); return type1; } private void decodeNetUserDataFields(Map<SmaUserDataField, Object> results) { if ( userData.length < 12 ) { return; } // # 4 bytes of serial (number) // # 8 bytes of device type (characters) long serial = (0xFF & userData[0]) | ((0xFF & userData[1]) << 8) | ((0xFF & userData[2]) << 16) | ((0xFF & userData[3]) << 24); results.put(SmaUserDataField.DeviceSerialNumber, Long.valueOf(serial)); try { String type = new String(userData, 4, 8, "ASCII"); results.put(SmaUserDataField.DeviceType, type); } catch ( UnsupportedEncodingException e ) { throw new RuntimeException(e); } } private void decodeChannelInfoUserDataFields(Map<SmaUserDataField, Object> results) { List<SmaChannel> channels = new ArrayList<SmaChannel>(); int offset = 0; do { SmaChannel channel = new SmaChannel(this.userData, offset); if ( LOG.isTraceEnabled() ) { LOG.trace("Found channel " + channel); } channels.add(channel); offset += channel.getDataLength(); } while ( offset < userData.length ); results.put(SmaUserDataField.Channels, channels); } /** * @return the userData */ public byte[] getUserData() { return userData; } /** * @param userData * the userData to set */ public void setUserData(byte[] userData) { this.userData = userData; } /** * @return the srcAddress */ public int getSrcAddress() { return srcAddress; } /** * @return the destAddress */ public int getDestAddress() { return destAddress; } /** * @return the control */ public SmaControl getControl() { return control; } /** * @return the packetCounter */ public int getPacketCounter() { return packetCounter; } /** * @return the command */ public SmaCommand getCommand() { return command; } /** * @return the crc */ public int getCrc() { return crc; } /** * @return the computedCrc */ public int getComputedCrc() { return computedCrc; } /** * @return the packet */ public byte[] getPacket() { return packet; } }