For all models providing these information and relying on ASCII protocol.
The binding now supports reading of status message containing variable length content.
Signed-off-by: Laurent Garnier <lg.hc@free.fr>
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";
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";
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<RotelCommand> 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,
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);
String textLine1Right = buildVolumeLine1RightResponse();
String textLine2 = "";
String textAscii = "";
+ boolean variableLength = false;
boolean accepted = true;
boolean resetZone = true;
int numZone = 0;
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;
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;
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;
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);
/** 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;
* 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;
}
/**
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");
- }
}
/**
}
}
- 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);
*/
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;
private static final char CHAR_END_RESPONSE = '!';
+ private static final Set<String> 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<String> 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
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;
+ }
}
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;
private static final char CHAR_END_RESPONSE = '$';
+ private static final Set<String> 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
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