2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.phc.internal.handler;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.io.OutputStream;
18 import java.util.ArrayList;
19 import java.util.HashMap;
20 import java.util.List;
22 import java.util.TooManyListenersException;
23 import java.util.concurrent.BlockingQueue;
24 import java.util.concurrent.LinkedBlockingQueue;
25 import java.util.concurrent.ScheduledThreadPoolExecutor;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.phc.internal.PHCBindingConstants;
30 import org.openhab.binding.phc.internal.PHCHelper;
31 import org.openhab.binding.phc.internal.util.StringUtils;
32 import org.openhab.core.io.transport.serial.PortInUseException;
33 import org.openhab.core.io.transport.serial.SerialPort;
34 import org.openhab.core.io.transport.serial.SerialPortEvent;
35 import org.openhab.core.io.transport.serial.SerialPortEventListener;
36 import org.openhab.core.io.transport.serial.SerialPortIdentifier;
37 import org.openhab.core.io.transport.serial.SerialPortManager;
38 import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
39 import org.openhab.core.library.types.OnOffType;
40 import org.openhab.core.library.types.PercentType;
41 import org.openhab.core.library.types.StopMoveType;
42 import org.openhab.core.thing.Bridge;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.Thing;
45 import org.openhab.core.thing.ThingStatus;
46 import org.openhab.core.thing.ThingStatusDetail;
47 import org.openhab.core.thing.ThingUID;
48 import org.openhab.core.thing.binding.BaseBridgeHandler;
49 import org.openhab.core.types.Command;
50 import org.openhab.core.util.HexUtils;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
55 * The {@link PHCBridgeHandler} is responsible for handling the serial Communication to and from the PHC Modules.
57 * @author Jonas Hohaus - Initial contribution
60 public class PHCBridgeHandler extends BaseBridgeHandler implements SerialPortEventListener {
62 private final Logger logger = LoggerFactory.getLogger(PHCBridgeHandler.class);
64 private static final int BAUD = 19200;
65 private static final int SEND_RETRY_COUNT = 20; // max count to send the same message
66 private static final int SEND_RETRY_TIME_MILLIS = 60; // time to wait for an acknowledge before send the message
67 // again in milliseconds
69 private @Nullable InputStream serialIn;
70 private @Nullable OutputStream serialOut;
71 private @Nullable SerialPort commPort;
72 private final SerialPortManager serialPortManager;
74 private final Map<Byte, Boolean> toggleMap = new HashMap<>();
75 private final InternalBuffer buffer = new InternalBuffer();
76 private final BlockingQueue<QueueObject> receiveQueue = new LinkedBlockingQueue<>();
77 private final BlockingQueue<QueueObject> sendQueue = new LinkedBlockingQueue<>();
78 private final ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(3);
80 private final byte emLedOutputState[] = new byte[32];
81 private final byte amOutputState[] = new byte[32];
82 private final byte dmOutputState[] = new byte[32];
84 private final List<Byte> modules = new ArrayList<>();
86 public PHCBridgeHandler(Bridge phcBridge, SerialPortManager serialPortManager) {
88 this.serialPortManager = serialPortManager;
92 public void initialize() {
93 String port = ((String) getConfig().get(PHCBindingConstants.PORT));
95 // find the given port
96 SerialPortIdentifier portId = serialPortManager.getIdentifier(port);
99 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
100 "Serial port '" + port + "' could not be found.");
105 // initialize serial port
106 SerialPort serialPort = portId.open(this.getClass().getName(), 2000); // owner, timeout
107 serialIn = serialPort.getInputStream();
108 // set port parameters
109 serialPort.setSerialPortParams(BAUD, SerialPort.DATABITS_8, SerialPort.STOPBITS_2, SerialPort.PARITY_NONE);
110 serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
112 serialPort.addEventListener(this);
113 // activate the DATA_AVAILABLE notifier
114 serialPort.notifyOnDataAvailable(true);
116 // get the output stream
117 serialOut = serialPort.getOutputStream();
119 commPort = serialPort;
124 for (int j = 0; j <= 0x1F; j++) {
125 serialWrite(buildMessage((byte) j, 0, b, false));
127 updateStatus(ThingStatus.ONLINE);
130 threadPoolExecutor.execute(new Runnable() {
134 processReceivedBytes();
138 // process received messages
139 threadPoolExecutor.execute(new Runnable() {
143 processReceiveQueue();
147 // sendig commands to the modules
148 threadPoolExecutor.execute(new Runnable() {
155 } catch (PortInUseException | TooManyListenersException e) {
156 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
157 "Could not open serial port " + port + ": " + e.getMessage());
158 } catch (UnsupportedCommOperationException e) {
159 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
160 "Could not configure serial port " + port + ": " + e.getMessage());
161 } catch (IOException e) {
162 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
163 "Failed to get input or output stream for serialPort: " + e.getMessage());
164 logger.debug("Failed to get inputstream for serialPort", e);
169 * Reads the data on serial port and puts it into the internal buffer.
172 public void serialEvent(SerialPortEvent event) {
173 if (event.getEventType() == SerialPortEvent.DATA_AVAILABLE && serialIn != null) {
175 byte[] bytes = new byte[serialIn.available()];
176 serialIn.read(bytes);
180 if (logger.isTraceEnabled()) {
181 logger.trace("buffer offered {}", HexUtils.bytesToHex(bytes, " "));
183 } catch (IOException e) {
184 logger.warn("Error on reading input stream to internal buffer", e);
190 * process internal incoming buffer (recognize on read messages)
192 private void processReceivedBytes() {
193 int faultCounter = 0;
196 byte module = buffer.get();
199 // Recognition of messages from byte buffer.
200 // not a known module address
201 if (!modules.contains(module)) {
202 module = buffer.get();
206 if (logger.isDebugEnabled()) {
207 logger.debug("get module: {}", new String(HexUtils.byteToHex(module)));
210 byte sizeToggle = buffer.get();
212 // read length of command and check if makes sense
213 int size = (sizeToggle & 0x7F);
215 if (!isSizeToggleValid(sizeToggle, module)) {
216 if (logger.isDebugEnabled()) {
217 logger.debug("get invalid sizeToggle: {}", new String(HexUtils.byteToHex(sizeToggle)));
224 // read toggle, size and command
225 boolean toggle = (sizeToggle & 0x80) == 0x80;
227 logger.debug("get toggle: {}", toggle);
229 byte[] command = new byte[size];
231 for (int i = 0; i < size; i++) {
232 command[i] = buffer.get();
236 if (logger.isTraceEnabled()) {
237 logger.trace("command read: {}", PHCHelper.bytesToBinaryString(command));
241 byte crcByte1 = buffer.get();
242 byte crcByte2 = buffer.get();
244 short crc = (short) (crcByte1 & 0xFF);
245 crc |= (crcByte2 << 8);
247 // calculate checkCrc
248 short checkCrc = calcCrc(module, sizeToggle, command);
251 if (crc != checkCrc) {
252 logger.debug("CRC not correct (crc from message, calculated crc): {}, {}", crc, checkCrc);
254 faultCounter = handleCrcFault(faultCounter);
256 module = buffer.get();
260 if (logger.isTraceEnabled()) {
261 logger.trace("get crc: {}", HexUtils.bytesToHex(new byte[] { crcByte1, crcByte2 }, " "));
266 processReceivedMsg(module, toggle, command);
267 module = buffer.get();
269 } catch (InterruptedException e) {
270 Thread.currentThread().interrupt();
274 private boolean isSizeToggleValid(byte sizeToggle, byte module) {
275 int unsigned = sizeToggle & 0xFF;
277 if (unsigned > 0 && unsigned < 4) {
279 } else if (unsigned > 0x80 && unsigned < 0x84) {
281 } else if ((module & 0xE0) == 0x00) {
282 if (unsigned > 0 && unsigned < 16) {
284 } else if (unsigned > 0x80 && unsigned < 0x90) {
292 private int handleCrcFault(int faultCounter) throws InterruptedException {
293 if (faultCounter > 0) {
294 // Normally in this case we read the message repeatedly offset to the real -> skip one to 6 bytes
295 for (int i = 0; i < faultCounter; i++) {
296 if (buffer.hasNext()) {
302 int resCounter = faultCounter + 1;
303 if (resCounter > 6) {
309 private void processReceivedMsg(byte module, boolean toggle, byte[] command) {
310 // Acknowledgement received (command first byte 0)
311 if (command[0] == 0) {
313 byte channel = 0; // only needed for dim
314 if ((module & 0xE0) == 0x40) {
315 moduleType = PHCBindingConstants.CHANNELS_AM;
316 } else if ((module & 0xE0) == 0xA0) {
317 moduleType = PHCBindingConstants.CHANNELS_DIM;
318 channel = (byte) ((command[0] >>> 5) & 0x0F);
320 moduleType = PHCBindingConstants.CHANNELS_EM_LED;
323 setModuleOutputState(moduleType, (byte) (module & 0x1F), command[1], channel);
324 toggleMap.put(module, !toggle);
326 // initialization (first byte FF)
327 } else if (command[0] == (byte) 0xFF) {
328 if ((module & 0xE0) == 0x00) { // EM
329 sendEmConfig(module);
330 } else if ((module & 0xE0) == 0x40 || (module & 0xE0) == 0xA0) { // AM, JRM and DIM
331 sendAmConfig(module);
334 logger.debug("initialization: {}", module);
336 // ignored - ping (first byte 01)
337 } else if (command[0] == 0x01) {
338 logger.debug("first byte 0x01 -> ignored");
340 // EM command / update
342 if ((module & 0xE0) == 0x00) {
343 sendEmAcknowledge(module, toggle);
344 logger.debug("send acknowledge (modul, toggle) {} {}", module, toggle);
346 for (byte cmdByte : command) {
347 byte channel = (byte) ((cmdByte >>> 4) & 0x0F);
349 OnOffType onOff = OnOffType.OFF;
351 byte cmd = (byte) (cmdByte & 0x0F);
354 onOff = OnOffType.ON;
356 logger.debug("Command {} isn't implemented for EM", cmd);
361 QueueObject qo = new QueueObject(PHCBindingConstants.CHANNELS_EM, module, channel, onOff);
363 // put recognized message into queue
364 if (!receiveQueue.contains(qo)) {
365 receiveQueue.offer(qo);
369 // ignore if message not from EM module
370 } else if (logger.isDebugEnabled()) {
371 logger.debug("Incoming message (module, toggle, command) not from EM module: {} {} {}",
372 new String(HexUtils.byteToHex(module)), toggle, PHCHelper.bytesToBinaryString(command));
378 * process receive queue
380 private void processReceiveQueue() {
383 QueueObject qo = receiveQueue.take();
385 logger.debug("Consume Receive QueueObject: {}", qo);
386 handleIncomingCommand(qo.getModuleAddress(), qo.getChannel(), (OnOffType) qo.getCommand());
387 } catch (InterruptedException e) {
388 Thread.currentThread().interrupt();
396 private void processSendQueue() {
399 QueueObject qo = sendQueue.take();
402 } catch (InterruptedException e1) {
403 Thread.currentThread().interrupt();
408 private void sendQueueObject(QueueObject qo) {
410 // Send the command to the module until a response is received. Max. SEND_RETRY_COUNT repeats.
412 switch (qo.getModuleType()) {
413 case PHCBindingConstants.CHANNELS_AM:
414 sendAm(qo.getModuleAddress(), qo.getChannel(), qo.getCommand());
416 case PHCBindingConstants.CHANNELS_EM_LED:
417 sendEm(qo.getModuleAddress(), qo.getChannel(), qo.getCommand());
419 case PHCBindingConstants.CHANNELS_JRM:
420 sendJrm(qo.getModuleAddress(), qo.getChannel(), qo.getCommand(), qo.getTime());
422 case PHCBindingConstants.CHANNELS_DIM:
423 sendDim(qo.getModuleAddress(), qo.getChannel(), qo.getCommand(), qo.getTime());
429 Thread.sleep(SEND_RETRY_TIME_MILLIS);
430 } catch (InterruptedException e) {
431 Thread.currentThread().interrupt();
433 } while (!isChannelOutputState(qo.getModuleType(), qo.getModuleAddress(), qo.getChannel(), qo.getCommand())
434 && sendCount < SEND_RETRY_COUNT);
436 if (PHCBindingConstants.CHANNELS_JRM.equals(qo.getModuleType())) {
437 // there aren't state per channel for JRM modules
438 amOutputState[qo.getModuleAddress() & 0x1F] = -1;
439 } else if (PHCBindingConstants.CHANNELS_DIM.equals(qo.getModuleType())) {
440 // state ist the same for every dim level except zero/off -> inizialize state
441 // with 0x0F after sending a command.
442 dmOutputState[qo.getModuleAddress() & 0x1F] |= (0x0F << (qo.getChannel() * 4));
445 if (sendCount >= SEND_RETRY_COUNT) {
446 // change the toggle: if no acknowledge received it may be wrong.
447 byte module = qo.getModuleAddress();
448 if (PHCBindingConstants.CHANNELS_AM.equals(qo.getModuleType())
449 || PHCBindingConstants.CHANNELS_JRM.equals(qo.getModuleType())) {
451 } else if (PHCBindingConstants.CHANNELS_DIM.equals(qo.getModuleType())) {
454 toggleMap.put(module, !getToggle(module));
456 if (logger.isDebugEnabled()) {
457 logger.debug("No acknowledge from the module {} received.", qo.getModuleAddress());
462 private void setModuleOutputState(String moduleType, byte moduleAddress, byte state, byte channel) {
463 if (PHCBindingConstants.CHANNELS_EM_LED.equals(moduleType)) {
464 emLedOutputState[moduleAddress] = state;
465 } else if (PHCBindingConstants.CHANNELS_AM.equals(moduleType)) {
466 amOutputState[moduleAddress & 0x1F] = state;
467 } else if (PHCBindingConstants.CHANNELS_DIM.equals(moduleType)) {
468 dmOutputState[moduleAddress & 0x1F] = (byte) (state << channel * 4);
472 private boolean isChannelOutputState(String moduleType, byte moduleAddress, byte channel, Command cmd) {
473 int state = OnOffType.OFF.equals(cmd) ? 0 : 1;
475 if (PHCBindingConstants.CHANNELS_EM_LED.equals(moduleType)) {
476 return ((emLedOutputState[moduleAddress & 0x1F] >>> channel) & 0x01) == state;
477 } else if (PHCBindingConstants.CHANNELS_AM.equals(moduleType)) {
478 return ((amOutputState[moduleAddress & 0x1F] >>> channel) & 0x01) == state;
479 } else if (PHCBindingConstants.CHANNELS_JRM.equals(moduleType)) {
480 return (amOutputState[moduleAddress & 0x1F] != -1);
481 } else if (PHCBindingConstants.CHANNELS_DIM.equals(moduleType)) {
482 return ((dmOutputState[moduleAddress & 0x1F] >>> channel * 4) & 0x0F) != 0x0F;
488 private boolean getToggle(byte moduleAddress) {
489 if (!toggleMap.containsKey(moduleAddress)) {
490 toggleMap.put(moduleAddress, false);
493 return toggleMap.get(moduleAddress);
497 * Put the given command into the queue to send.
500 * @param moduleAddress
505 public void send(@Nullable String moduleType, int moduleAddress, String channel, Command command,
507 if (PHCBindingConstants.CHANNELS_JRM.equals(moduleType)
508 || PHCBindingConstants.CHANNELS_DIM.equals(moduleType)) {
509 sendQueue.offer(new QueueObject(moduleType, moduleAddress, channel, command, upDownTime));
511 sendQueue.offer(new QueueObject(moduleType, moduleAddress, channel, command));
515 private void sendAm(byte moduleAddress, byte channel, Command command) {
516 byte module = (byte) (moduleAddress | 0x40);
518 byte[] cmd = { (byte) (channel << 5) };
520 if (OnOffType.ON.equals(command)) {
525 serialWrite(buildMessage(module, channel, cmd, getToggle(module)));
528 private void sendEm(byte moduleAddress, byte channel, Command command) {
529 byte[] cmd = { (byte) (channel << 4) };
531 if (OnOffType.ON.equals(command)) {
536 serialWrite(buildMessage(moduleAddress, channel, cmd, getToggle(moduleAddress)));
539 private void sendJrm(byte moduleAddress, byte channel, Command command, short upDownTime) {
540 // The up and the down message needs two additional bytes for the time.
541 int size = (command == StopMoveType.STOP) ? 2 : 4;
542 byte[] cmd = new byte[size];
547 byte module = (byte) (moduleAddress | 0x40);
549 cmd[0] = (byte) (channel << 5);
552 switch (command.toString()) {
555 cmd[2] = (byte) (upDownTime & 0xFF);// Time 1/10 sec. LSB
556 cmd[3] = (byte) ((upDownTime >> 8) & 0xFF); // 1/10 sec. MSB
560 cmd[2] = (byte) (upDownTime & 0xFF);// Time 1/10 sec. LSB
561 cmd[3] = (byte) ((upDownTime >> 8) & 0xFF); // 1/10 sec. MSB
568 serialWrite(buildMessage(module, channel, cmd, getToggle(module)));
571 private void sendDim(byte moduleAddress, byte channel, Command command, short dimTime) {
572 byte module = (byte) (moduleAddress | 0xA0);
573 byte[] cmd = new byte[(command instanceof PercentType && !(((PercentType) command).byteValue() == 0)) ? 3 : 1];
575 cmd[0] = (byte) (channel << 5);
577 if (command instanceof OnOffType) {
578 if (OnOffType.ON.equals(command)) {
580 } else if (OnOffType.OFF.equals(command)) {
584 if (((PercentType) command).byteValue() == 0) {
588 cmd[1] = (byte) (((PercentType) command).byteValue() * 2.55);
589 cmd[2] = (byte) dimTime;
592 serialWrite(buildMessage(module, channel, cmd, getToggle(module)));
595 private void sendPorBroadcast() {
596 byte[] msg = buildMessage((byte) 0xFF, 0, new byte[] { 0 }, false);
597 for (int i = 0; i < 20; i++) {
603 private void sendAmConfig(byte moduleAddress) {
604 byte[] cmd = new byte[3];
606 cmd[0] = (byte) 0xFE;
608 cmd[2] = (byte) 0xFF;
610 serialWrite(buildMessage(moduleAddress, 0, cmd, false));
613 private void sendEmConfig(byte moduleAddress) {
614 byte[] cmd = new byte[52];
617 cmd[pos++] = (byte) 0xFE;
618 cmd[pos++] = (byte) 0x00; // POR
623 for (int i = 0; i < 16; i++) { // 16 inputs
624 cmd[pos++] = (byte) ((i << 4) | 0x02);
625 cmd[pos++] = (byte) ((i << 4) | 0x03);
626 cmd[pos++] = (byte) ((i << 4) | 0x05);
629 serialWrite(buildMessage(moduleAddress, 0, cmd, false));
632 private void sendEmAcknowledge(byte module, boolean toggle) {
633 byte[] msg = buildMessage(module, 0, new byte[] { 0 }, toggle);
634 for (int i = 0; i < 3; i++) { // send three times stops the module faster from sending messages if the first
635 // response is not recognized.
641 * Build a serial message from the given parameters.
649 private byte[] buildMessage(byte modulAddr, int channel, byte[] cmd, boolean toggle) {
650 int len = cmd.length;
651 byte[] buffer = new byte[len + 4];
653 buffer[0] = modulAddr;
654 buffer[1] = (byte) (toggle ? (len | 0x80) : len); // 0x80: 1000 0000
656 System.arraycopy(cmd, 0, buffer, 2, len);
658 short crc = calcCrc(modulAddr, buffer[1], cmd);
660 buffer[2 + len] = (byte) (crc & 0xFF);
661 buffer[3 + len] = (byte) ((crc >> 8) & 0xFF);
667 * Calculate the 16 bit crc of the message.
674 private short calcCrc(byte module, byte sizeToggle, byte[] cmd) {
675 short crc = (short) 0xFFFF;
677 crc = crc16Update(crc, module);
678 crc = crc16Update(crc, sizeToggle);
681 crc = crc16Update(crc, b);
689 * Update the 16 bit crc of the message.
695 private short crc16Update(short crc, byte messagePart) {
696 byte data = (byte) (messagePart ^ (crc & 0xFF));
700 return (short) (((data16 << 8) | (((crc >> 8) & 0xFF) & 0xFF)) ^ ((data >> 4) & 0xF)
701 ^ ((data16 << 3) & 0b11111111111));
705 * Send the incoming command to the appropriate handler and channel.
707 * @param moduleAddress
712 private void handleIncomingCommand(byte moduleAddress, int channel, OnOffType onOff) {
713 ThingUID uid = PHCHelper.getThingUIDreverse(PHCBindingConstants.THING_TYPE_EM, moduleAddress);
714 Thing thing = getThing().getThing(uid);
715 String channelId = "em#" + StringUtils.padLeft(Integer.toString(channel), 2, "0");
717 if (thing != null && thing.getHandler() != null) {
718 logger.debug("Input: {}, {}, {}", thing.getUID(), channelId, onOff);
720 PHCHandler handler = (PHCHandler) thing.getHandler();
721 if (handler != null) {
722 handler.handleIncoming(channelId, onOff);
724 logger.debug("No Handler for Thing {} available.", thing.getUID());
728 logger.debug("No Thing with UID {} available.", uid.getAsString());
732 private void serialWrite(byte[] msg) {
733 if (serialOut != null) {
735 // write to serial port
736 serialOut.write(msg);
738 } catch (IOException e) {
739 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
740 "Error writing '" + msg + "' to serial port : " + e.getMessage());
743 if (logger.isTraceEnabled()) {
744 logger.trace("send: {}", PHCHelper.bytesToBinaryString(msg));
750 * Adds the given address to the module list.
754 public void addModule(byte module) {
759 public void handleCommand(ChannelUID channelUID, Command command) {
764 public void dispose() {
765 threadPoolExecutor.shutdownNow();
766 if (commPort != null) {