package net.solarnetwork.node.hw.sma.protocol; 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 and a variable-length * {@code userData} section. The header contains the source and destination * addresses, a control byte, a counter byte, and the command type. The user * data section contains any data required to pass with the command, or sent * back from the controller. * </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 source address bytes * # 2 destination address bytes * # 1 control byte * # 1 packet counter byte * # 1 command type byte * # X user data bytes * </pre> */ public final class SmaPacket { 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 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(); } /** * Utility to create a NetStart packet. * * @param sourceAddress * the source address * @return the packet */ public static SmaPacket netStartPacket(int sourceAddress) { return new SmaPacket(sourceAddress, 0, 0, SmaControl.RequestGroup, SmaCommand.NetStart, SmaUtils.EMPTY_BYTE_ARRAY); } /** * Utility to create a NetGet packet. * * @param sourceAddress * the source address * @return the packet */ public static SmaPacket netGetPacket(int sourceAddress) { return new SmaPacket(sourceAddress, 0, 0, SmaControl.RequestGroup, SmaCommand.NetGet, SmaUtils.EMPTY_BYTE_ARRAY); } /** * Utility to create a GetChannelInfo packet. * * @param sourceAddress * the source address * @param destinationAddress * the destination address * @return the packet */ public static SmaPacket getChannelInfoPacket(int sourceAddress, int destinationAddress) { return new SmaPacket(sourceAddress, destinationAddress, 0, SmaControl.RequestSingle, SmaCommand.GetChannelInfo, SmaUtils.EMPTY_BYTE_ARRAY); } /** * 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); } @Override public String toString() { return "SmaPacket{command=" + command + ",packetCounter=" + packetCounter + ",control=" + control + '}'; } private void encodePacket() { byte[] req = new byte[7 + this.userData.length]; req[0] = (byte) (this.srcAddress & 0xFF); // source address (low byte) req[1] = (byte) ((this.srcAddress >> 8) & 0xFF); // destination (high byte) req[2] = (byte) (this.destAddress & 0xFF); // destination (low byte) req[3] = (byte) ((this.destAddress >> 8) & 0xFF);// destination (high byte) req[4] = (byte) this.control.getCode(); req[5] = (byte) this.packetCounter; req[6] = (byte) this.command.getCode(); System.arraycopy(this.userData, 0, req, 7, this.userData.length); this.packet = req; } private void decodePacket() { if ( packet == null || packet.length < 7 ) { return; } userData = new byte[packet.length - 7]; // CRC data starts now for ( int i = 0; i < 7; i++ ) { int b = 0xFF & packet[i]; 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; } } System.arraycopy(packet, 7, userData, 0, userData.length); } /** * 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(0xF & userData[0]); results.put(SmaUserDataField.ChannelType, type1); results.put(SmaUserDataField.ChannelTypeGroup, 0x3F & 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); } public byte[] getUserData() { return userData; } public void setUserData(byte[] userData) { this.userData = userData; } public int getSrcAddress() { return srcAddress; } public int getDestAddress() { return destAddress; } public SmaControl getControl() { return control; } public int getPacketCounter() { return packetCounter; } public SmaCommand getCommand() { return command; } public byte[] getPacket() { return packet; } }