diff --git a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/RotelBindingConstants.java b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/RotelBindingConstants.java index 80b180a22..5f4a7e3f8 100644 --- a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/RotelBindingConstants.java +++ b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/RotelBindingConstants.java @@ -263,9 +263,30 @@ public class RotelBindingConstants { public static final String KEY_TONE_MAX = "tone_max"; public static final String KEY1_PLAY_STATUS = "play_status"; public static final String KEY2_PLAY_STATUS = "status"; + public static final String KEY_DISC_NAME = "disc_name"; + public static final String KEY_DISC_TYPE = "disc_type"; public static final String KEY_TRACK = "track"; + public static final String KEY_TRACK_NAME = "track_name"; + public static final String KEY_TIME = "time"; public static final String KEY_RANDOM = "rnd"; public static final String KEY_REPEAT = "rpt"; + public static final String KEY_PRESET_FM = "preset_fm"; + public static final String KEY_FM_PRESET = "fm_preset_"; + public static final String KEY_FM_ALL_PRESET = "fm_allpreset_"; + public static final String KEY_FM = "fm"; + public static final String KEY_FM_MONO = "fm_mono"; + public static final String KEY_FM_RDS = "fm_rds"; + public static final String KEY_FM_FREQ = "fm_freq"; + public static final String KEY_PRESET_DAB = "preset_dab"; + public static final String KEY_DAB_PRESET = "dab_preset_"; + public static final String KEY_DAB_ALL_PRESET = "dab_allpreset_"; + public static final String KEY_DAB = "dab"; + public static final String KEY_DAB_STATION = "dab_station"; + public static final String KEY_PRESET_IRADIO = "preset_iradio"; + public static final String KEY_IRADIO_PRESET = "iradio_preset_"; + public static final String KEY_IRADIO_ALL_PRESET = "iradio_allpreset_"; + public static final String KEY_CURRENT_STATION = "current_station"; + public static final String KEY_SIGNAL_STRENGTH = "signal_strength"; public static final String KEY_DIMMER = "dimmer"; public static final String KEY_FREQ = "freq"; public static final String KEY_FREQ_ZONE1 = "freq_zone1"; @@ -291,8 +312,16 @@ public class RotelBindingConstants { public static final String KEY_CEILING_REAR_RIGHT_LEVEL = "ceiling_rear_right"; public static final String KEY_CEILING_REAR_LEFT_LEVEL = "ceiling_rear_left"; public static final String KEY_PCUSB_CLASS = "pcusb_class"; + public static final String KEY_PRODUCT_TYPE = "product_type"; public static final String KEY_MODEL = "model"; + public static final String KEY_PRODUCT_VERSION = "product_version"; public static final String KEY_VERSION = "version"; + public static final String KEY_TC_VERSION = "tc_version"; + public static final String KEY_DISPLAY = "display"; + public static final String KEY_DISPLAY1 = "display1"; + public static final String KEY_DISPLAY2 = "display2"; + public static final String KEY_DISPLAY3 = "display3"; + public static final String KEY_DISPLAY4 = "display4"; // Output keys only used by the HEX protocol public static final String KEY_LINE1 = "line1"; public static final String KEY_LINE2 = "line2"; diff --git a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelCommand.java b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelCommand.java index 7907eb1f3..d3bb3dc67 100644 --- a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelCommand.java +++ b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelCommand.java @@ -496,8 +496,8 @@ public enum RotelCommand { HDMI_TV_MODE("HDMI TV Mode", PRIMARY_CMD, (byte) 0x79), ROOM_EQ_TOGGLE("Temporary Room EQ Toggle", PRIMARY_CMD, (byte) 0x67), SPEAKER_SETTING_TOGGLE("Speaker Level Setting Toggle", PRIMARY_CMD, (byte) 0xA1), - MODEL("Request the model number", null, "model?"), - VERSION("Request the main CPU software version", null, "version?"); + MODEL("Request the model number", "get_product_type", "model?"), + VERSION("Request the main CPU software version", "get_product_version", "version?"); public static final List DSP_CMDS_SET1 = List.of(DSP_TOGGLE, PROLOGIC_TOGGLE, DOLBY_TOGGLE, PLII_PANORAMA_TOGGLE, PLII_DIMENSION_UP, PLII_DIMENSION_DOWN, PLII_CENTER_WIDTH_UP, PLII_CENTER_WIDTH_DOWN, diff --git a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelSimuConnector.java b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelSimuConnector.java index 80172e4c8..b57e8da34 100644 --- a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelSimuConnector.java +++ b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/communication/RotelSimuConnector.java @@ -45,6 +45,7 @@ public class RotelSimuConnector extends RotelConnector { private static final int STEP_TONE_LEVEL = 1; private static final double STEP_DECIBEL = 0.5; + private static final String FIRMWARE = "V1.1.8"; private final Logger logger = LoggerFactory.getLogger(RotelSimuConnector.class); @@ -175,6 +176,7 @@ public class RotelSimuConnector extends RotelConnector { String textLine1Right = buildVolumeLine1RightResponse(); String textLine2 = ""; String textAscii = ""; + boolean variableLength = false; boolean accepted = true; boolean resetZone = true; int numZone = 0; @@ -1062,10 +1064,22 @@ public class RotelSimuConnector extends RotelConnector { textAscii = buildAsciiResponse(KEY_PCUSB_CLASS, pcUsbClass); break; case MODEL: - textAscii = buildAsciiResponse(KEY_MODEL, model.getName()); + if (protocol == RotelProtocol.ASCII_V1) { + variableLength = true; + textAscii = buildAsciiResponse(KEY_PRODUCT_TYPE, + String.format("%d,%s", model.getName().length(), model.getName())); + } else { + textAscii = buildAsciiResponse(KEY_MODEL, model.getName()); + } break; case VERSION: - textAscii = buildAsciiResponse(KEY_VERSION, "1.00"); + if (protocol == RotelProtocol.ASCII_V1) { + variableLength = true; + textAscii = buildAsciiResponse(KEY_PRODUCT_VERSION, + String.format("%d,%s", FIRMWARE.length(), FIRMWARE)); + } else { + textAscii = buildAsciiResponse(KEY_VERSION, FIRMWARE); + } break; default: accepted = false; @@ -1186,7 +1200,14 @@ public class RotelSimuConnector extends RotelConnector { idxInFeedbackMsg = 0; } } else { - String command = textAscii + (protocol == RotelProtocol.ASCII_V1 ? "!" : "$"); + String command = textAscii; + if (protocol == RotelProtocol.ASCII_V1 && !variableLength) { + command += "!"; + } else if (protocol == RotelProtocol.ASCII_V2 && !variableLength) { + command += "$"; + } else if (protocol == RotelProtocol.ASCII_V2 && variableLength) { + command += "$$"; + } synchronized (lock) { feedbackMsg = command.getBytes(StandardCharsets.US_ASCII); idxInFeedbackMsg = 0; diff --git a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/handler/RotelHandler.java b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/handler/RotelHandler.java index d71a6c083..f0bd7b02d 100644 --- a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/handler/RotelHandler.java +++ b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/handler/RotelHandler.java @@ -1696,9 +1696,11 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL case KEY_PCUSB_CLASS: logger.debug("PC-USB Audio Class is set to {}", value); break; + case KEY_PRODUCT_TYPE: case KEY_MODEL: getThing().setProperty(Thing.PROPERTY_MODEL_ID, value); break; + case KEY_PRODUCT_VERSION: case KEY_VERSION: getThing().setProperty(Thing.PROPERTY_FIRMWARE_VERSION, value); break; @@ -1944,6 +1946,13 @@ public class RotelHandler extends BaseThingHandler implements RotelMessageEventL sendCommand(RotelCommand.SPEAKER); Thread.sleep(SLEEP_INTV); } + if (model != RotelModel.RAP1580 && model != RotelModel.RSP1576 + && model != RotelModel.RSP1582) { + sendCommand(RotelCommand.MODEL); + Thread.sleep(SLEEP_INTV); + sendCommand(RotelCommand.VERSION); + Thread.sleep(SLEEP_INTV); + } break; case ASCII_V2: sendCommand(RotelCommand.UPDATE_AUTO); diff --git a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/ascii/RotelAbstractAsciiProtocolHandler.java b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/ascii/RotelAbstractAsciiProtocolHandler.java index 71cd0f8d9..30399f74f 100644 --- a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/ascii/RotelAbstractAsciiProtocolHandler.java +++ b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/ascii/RotelAbstractAsciiProtocolHandler.java @@ -56,10 +56,10 @@ public abstract class RotelAbstractAsciiProtocolHandler extends RotelAbstractPro /** Empty table of special characters */ public static final byte[][] NO_SPECIAL_CHARACTERS = {}; + private static final int MAX_SIZE_RESPONSE = 128; + private final Logger logger = LoggerFactory.getLogger(RotelAbstractAsciiProtocolHandler.class); - private final char terminatingChar; - private final int size; private final byte[] dataBuffer; private int index; @@ -68,31 +68,31 @@ public abstract class RotelAbstractAsciiProtocolHandler extends RotelAbstractPro * Constructor * * @param model the Rotel model in use - * @param protocol the protocol to be used */ - public RotelAbstractAsciiProtocolHandler(RotelModel model, char terminatingChar) { + public RotelAbstractAsciiProtocolHandler(RotelModel model) { super(model); - this.terminatingChar = terminatingChar; - this.size = 64; - this.dataBuffer = new byte[size]; + this.dataBuffer = new byte[MAX_SIZE_RESPONSE]; this.index = 0; } - @Override - public void handleIncomingData(byte[] inDataBuffer, int length) { - for (int i = 0; i < length; i++) { - if (index < size) { - dataBuffer[index++] = inDataBuffer[i]; - } - if (inDataBuffer[i] == terminatingChar) { - if (index >= size) { - dataBuffer[index - 1] = (byte) terminatingChar; - } - byte[] msg = Arrays.copyOf(dataBuffer, index); - handleIncomingMessage(msg); - index = 0; - } + protected boolean fillDataBuffer(byte data) { + if (index < MAX_SIZE_RESPONSE) { + dataBuffer[index++] = data; + return true; } + return false; + } + + protected byte[] getDataBuffer() { + return Arrays.copyOf(dataBuffer, index); + } + + protected void resetDataBuffer() { + index = 0; + } + + protected int getRemainingSizeInDataBuffer() { + return MAX_SIZE_RESPONSE - index; } /** @@ -109,12 +109,6 @@ public abstract class RotelAbstractAsciiProtocolHandler extends RotelAbstractPro logger.debug("Unexpected message length: {}", responseMessage.length); throw new RotelException("Unexpected message length"); } - - if (responseMessage[responseMessage.length - 1] != '!' && responseMessage[responseMessage.length - 1] != '$') { - logger.debug("Unexpected ending character in response: {}", - Integer.toHexString(responseMessage[responseMessage.length - 1] & 0x000000FF)); - throw new RotelException("Unexpected ending character in response"); - } } /** @@ -133,18 +127,19 @@ public abstract class RotelAbstractAsciiProtocolHandler extends RotelAbstractPro } } - String value = new String(message, 0, message.length - 1, StandardCharsets.US_ASCII); + String value = new String(message, 0, message.length, StandardCharsets.US_ASCII); logger.debug("handleValidAsciiMessage: chars *{}*", value); value = value.trim(); if (value.isEmpty()) { return; } try { - String[] splittedValue = value.split("="); - if (splittedValue.length != 2) { + int idxSeparator = value.indexOf("="); + if (idxSeparator < 0) { logger.debug("handleValidAsciiMessage: ignored message {}", value); } else { - dispatchKeyValue(splittedValue[0].trim().toLowerCase(), splittedValue[1]); + dispatchKeyValue(value.substring(0, idxSeparator).trim().toLowerCase(), + value.substring(idxSeparator + 1)); } } catch (PatternSyntaxException e) { logger.debug("handleValidAsciiMessage: ignored message {}", value); diff --git a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/ascii/RotelAsciiV1ProtocolHandler.java b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/ascii/RotelAsciiV1ProtocolHandler.java index e1b2ea274..565e4f827 100644 --- a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/ascii/RotelAsciiV1ProtocolHandler.java +++ b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/ascii/RotelAsciiV1ProtocolHandler.java @@ -12,7 +12,11 @@ */ package org.openhab.binding.rotel.internal.protocol.ascii; +import static org.openhab.binding.rotel.internal.RotelBindingConstants.*; + import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -33,15 +37,26 @@ public class RotelAsciiV1ProtocolHandler extends RotelAbstractAsciiProtocolHandl private static final char CHAR_END_RESPONSE = '!'; + private static final Set KEYSET1 = Set.of(KEY_DISPLAY, KEY_DISPLAY1, KEY_DISPLAY2, KEY_DISPLAY3, + KEY_DISPLAY4, KEY_PRODUCT_TYPE, KEY_PRODUCT_VERSION, KEY_TC_VERSION, KEY_TRACK); + private static final Set KEYSET2 = Set.of(KEY_FM_PRESET, KEY_FM_ALL_PRESET, KEY_DAB_PRESET, + KEY_DAB_ALL_PRESET, KEY_IRADIO_PRESET, KEY_IRADIO_ALL_PRESET); + private final Logger logger = LoggerFactory.getLogger(RotelAsciiV1ProtocolHandler.class); + private final byte[] lengthBuffer = new byte[8]; + private boolean searchKey = true; + private boolean searchLength; + private int valueLength; + private int indexLengthBuffer; + /** * Constructor * * @param model the Rotel model in use */ public RotelAsciiV1ProtocolHandler(RotelModel model) { - super(model, CHAR_END_RESPONSE); + super(model); } @Override @@ -97,4 +112,61 @@ public class RotelAsciiV1ProtocolHandler extends RotelAbstractAsciiProtocolHandl logger.debug("Command \"{}\" => {}", cmd, messageStr); return message; } + + @Override + public void handleIncomingData(byte[] inDataBuffer, int length) { + for (int i = 0; i < length; i++) { + boolean end = false; + if (searchKey && inDataBuffer[i] == '=') { + // End of key reading, check if the value is a fixed or variable length + searchKey = false; + byte[] dataKey = getDataBuffer(); + String key = new String(dataKey, 0, dataKey.length, StandardCharsets.US_ASCII).trim(); + searchLength = isVariableLengthApplicable(key); + indexLengthBuffer = 0; + valueLength = 0; + logger.trace("handleIncomingData: key = *{}* {}", key, searchLength ? "variable" : "fixed"); + fillDataBuffer(inDataBuffer[i]); + } else if (searchKey) { + // Reading key + fillDataBuffer(inDataBuffer[i]); + } else if (searchLength && inDataBuffer[i] == ',') { + // End of value length reading + searchLength = false; + byte[] lengthData = Arrays.copyOf(lengthBuffer, indexLengthBuffer); + String lengthStr = new String(lengthData, 0, lengthData.length, StandardCharsets.US_ASCII); + valueLength = Integer.parseInt(lengthStr); + logger.trace("handleIncomingData: valueLength = {}", valueLength); + if (getRemainingSizeInDataBuffer() < valueLength) { + logger.warn( + "handleIncomingData: the size of the internal buffer is too small, reponse will be truncated"); + } + end = valueLength == 0; + } else if (searchLength) { + // Reading value length + lengthBuffer[indexLengthBuffer++] = inDataBuffer[i]; + } else if (valueLength > 0) { + // Reading value (variable length) + fillDataBuffer(inDataBuffer[i]); + valueLength--; + end = valueLength == 0; + } else if (inDataBuffer[i] == CHAR_END_RESPONSE) { + // End of value reading + end = true; + } else { + // Reading value (fixed length) + fillDataBuffer(inDataBuffer[i]); + } + if (end) { + handleIncomingMessage(getDataBuffer()); + resetDataBuffer(); + searchKey = true; + searchLength = false; + } + } + } + + private boolean isVariableLengthApplicable(String key) { + return KEYSET1.contains(key) || KEYSET2.stream().filter(k -> key.startsWith(k)).count() > 0; + } } diff --git a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/ascii/RotelAsciiV2ProtocolHandler.java b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/ascii/RotelAsciiV2ProtocolHandler.java index 5c2b2cf75..8b9ca22a5 100644 --- a/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/ascii/RotelAsciiV2ProtocolHandler.java +++ b/bundles/org.openhab.binding.rotel/src/main/java/org/openhab/binding/rotel/internal/protocol/ascii/RotelAsciiV2ProtocolHandler.java @@ -15,6 +15,7 @@ package org.openhab.binding.rotel.internal.protocol.ascii; import static org.openhab.binding.rotel.internal.RotelBindingConstants.*; import java.nio.charset.StandardCharsets; +import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -35,15 +36,22 @@ public class RotelAsciiV2ProtocolHandler extends RotelAbstractAsciiProtocolHandl private static final char CHAR_END_RESPONSE = '$'; + private static final Set KEYSET = Set.of(KEY_DISC_NAME, KEY_DISC_TYPE, KEY_TRACK_NAME, KEY_TIME, KEY_FM_RDS, + KEY_DAB_STATION); + private final Logger logger = LoggerFactory.getLogger(RotelAsciiV2ProtocolHandler.class); + private boolean searchKey = true; + private boolean variableLength; + private boolean prevIsEndCharacter; + /** * Constructor * * @param model the Rotel model in use */ public RotelAsciiV2ProtocolHandler(RotelModel model) { - super(model, CHAR_END_RESPONSE); + super(model); } @Override @@ -116,6 +124,44 @@ public class RotelAsciiV2ProtocolHandler extends RotelAbstractAsciiProtocolHandl return message; } + @Override + public void handleIncomingData(byte[] inDataBuffer, int length) { + for (int i = 0; i < length; i++) { + boolean end = false; + if (searchKey && inDataBuffer[i] == '=') { + // End of key reading, check if the value is a fixed or variable length + searchKey = false; + byte[] dataKey = getDataBuffer(); + String key = new String(dataKey, 0, dataKey.length, StandardCharsets.US_ASCII).trim(); + variableLength = KEYSET.contains(key); + logger.trace("handleIncomingData: key = *{}* {}", key, variableLength ? "variable" : "fixed"); + fillDataBuffer(inDataBuffer[i]); + } else if (searchKey) { + // Reading key + fillDataBuffer(inDataBuffer[i]); + } else if (inDataBuffer[i] == CHAR_END_RESPONSE) { + end = !variableLength || prevIsEndCharacter; + } else { + if (prevIsEndCharacter) { + // End character inside a variable length value + fillDataBuffer((byte) CHAR_END_RESPONSE); + } + // Reading value + fillDataBuffer(inDataBuffer[i]); + } + if (end) { + // End of value reading + handleIncomingMessage(getDataBuffer()); + resetDataBuffer(); + searchKey = true; + variableLength = false; + prevIsEndCharacter = false; + } else { + prevIsEndCharacter = inDataBuffer[i] == CHAR_END_RESPONSE; + } + } + } + @Override protected void dispatchKeyValue(String key, String value) { // For distribution amplifiers, we need to split certain values to get the value for each zone