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.serial.internal.handler;
15 import static org.openhab.binding.serial.internal.SerialBindingConstants.BINARY_CHANNEL;
16 import static org.openhab.binding.serial.internal.SerialBindingConstants.STRING_CHANNEL;
17 import static org.openhab.binding.serial.internal.SerialBindingConstants.TRIGGER_CHANNEL;
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.io.OutputStream;
22 import java.nio.charset.Charset;
23 import java.nio.charset.IllegalCharsetNameException;
24 import java.nio.charset.StandardCharsets;
25 import java.util.Base64;
26 import java.util.TooManyListenersException;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29 import java.util.concurrent.atomic.AtomicBoolean;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.serial.internal.util.Parity;
34 import org.openhab.binding.serial.internal.util.StopBits;
35 import org.openhab.core.io.transport.serial.PortInUseException;
36 import org.openhab.core.io.transport.serial.SerialPort;
37 import org.openhab.core.io.transport.serial.SerialPortEvent;
38 import org.openhab.core.io.transport.serial.SerialPortEventListener;
39 import org.openhab.core.io.transport.serial.SerialPortIdentifier;
40 import org.openhab.core.io.transport.serial.SerialPortManager;
41 import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
42 import org.openhab.core.library.types.RawType;
43 import org.openhab.core.library.types.StringType;
44 import org.openhab.core.thing.Bridge;
45 import org.openhab.core.thing.ChannelUID;
46 import org.openhab.core.thing.CommonTriggerEvents;
47 import org.openhab.core.thing.ThingStatus;
48 import org.openhab.core.thing.ThingStatusDetail;
49 import org.openhab.core.thing.binding.BaseBridgeHandler;
50 import org.openhab.core.types.Command;
51 import org.openhab.core.types.RefreshType;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
56 * The {@link SerialBridgeHandler} is responsible for handling commands, which
57 * are sent to one of the channels.
59 * @author Mike Major - Initial contribution
62 public class SerialBridgeHandler extends BaseBridgeHandler implements SerialPortEventListener {
64 private final Logger logger = LoggerFactory.getLogger(SerialBridgeHandler.class);
66 private SerialBridgeConfiguration config = new SerialBridgeConfiguration();
68 private final SerialPortManager serialPortManager;
69 private @Nullable SerialPort serialPort;
71 private @Nullable InputStream inputStream;
72 private @Nullable OutputStream outputStream;
74 private Charset charset = StandardCharsets.UTF_8;
76 private @Nullable String lastValue;
78 private final AtomicBoolean readerActive = new AtomicBoolean(false);
79 private @Nullable ScheduledFuture<?> reader;
81 public SerialBridgeHandler(final Bridge bridge, final SerialPortManager serialPortManager) {
83 this.serialPortManager = serialPortManager;
87 public void handleCommand(final ChannelUID channelUID, final Command command) {
88 if (command instanceof RefreshType) {
89 final String lastValue = this.lastValue;
91 if (lastValue != null) {
92 refresh(channelUID.getId(), lastValue);
95 switch (channelUID.getId()) {
97 writeString(command.toFullString(), false);
100 writeString(command.toFullString(), true);
110 public void initialize() {
111 config = getConfigAs(SerialBridgeConfiguration.class);
114 if (config.charset != null) {
115 charset = Charset.forName(config.charset);
117 logger.debug("Serial port '{}' charset '{}' set", config.serialPort, charset);
118 } catch (final IllegalCharsetNameException e) {
119 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Invalid charset");
123 final String port = config.serialPort;
125 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port must be set");
129 // parse ports and if the port is found, initialize the reader
130 final SerialPortIdentifier portId = serialPortManager.getIdentifier(port);
131 if (portId == null) {
132 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port is not known");
136 // initialize serial port
138 final SerialPort serialPort = portId.open(getThing().getUID().toString(), 2000);
139 this.serialPort = serialPort;
141 serialPort.setSerialPortParams(config.baudRate, config.dataBits,
142 StopBits.fromConfig(config.stopBits).getSerialPortValue(),
143 Parity.fromConfig(config.parity).getSerialPortValue());
145 serialPort.addEventListener(this);
147 // activate the DATA_AVAILABLE notifier
148 serialPort.notifyOnDataAvailable(true);
149 inputStream = serialPort.getInputStream();
150 outputStream = serialPort.getOutputStream();
152 updateStatus(ThingStatus.ONLINE);
153 } catch (final IOException ex) {
154 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "I/O error");
155 } catch (final PortInUseException e) {
156 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Port is in use");
157 } catch (final TooManyListenersException e) {
158 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
159 "Cannot attach listener to port");
160 } catch (final UnsupportedCommOperationException e) {
161 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
162 "Unsupported port parameters: " + e.getMessage());
167 public void dispose() {
168 final SerialPort serialPort = this.serialPort;
169 if (serialPort != null) {
170 serialPort.removeEventListener();
172 this.serialPort = null;
175 final InputStream inputStream = this.inputStream;
176 if (inputStream != null) {
179 } catch (final IOException e) {
180 logger.debug("Error while closing the input stream: {}", e.getMessage());
182 this.inputStream = null;
185 final OutputStream outputStream = this.outputStream;
186 if (outputStream != null) {
188 outputStream.close();
189 } catch (final IOException e) {
190 logger.debug("Error while closing the output stream: {}", e.getMessage());
192 this.outputStream = null;
195 readerActive.set(false);
196 final ScheduledFuture<?> reader = this.reader;
197 if (reader != null) {
198 reader.cancel(false);
206 public void serialEvent(final SerialPortEvent event) {
207 switch (event.getEventType()) {
208 case SerialPortEvent.DATA_AVAILABLE:
209 if (readerActive.compareAndSet(false, true)) {
210 reader = scheduler.schedule(() -> receiveAndProcess(new StringBuilder(), true), 0,
211 TimeUnit.MILLISECONDS);
220 * Sends a string to the serial port.
222 * @param string the string to send
224 public void writeString(final String string) {
225 writeString(string, false);
229 * Refreshes the channel with the last received data
231 * @param channelId the channel to refresh
232 * @param channelId the data to use
234 private void refresh(final String channelId, final String data) {
235 if (!isLinked(channelId)) {
241 updateState(channelId, new StringType(data));
244 final StringBuilder sb = new StringBuilder("data:");
245 sb.append(RawType.DEFAULT_MIME_TYPE).append(";base64,")
246 .append(Base64.getEncoder().encodeToString(data.getBytes(charset)));
247 updateState(channelId, new StringType(sb.toString()));
255 * Read from the serial port and process the data
257 * @param sb the string builder to receive the data
258 * @param firstAttempt indicates if this is the first read attempt without waiting
260 private void receiveAndProcess(final StringBuilder sb, final boolean firstAttempt) {
261 final InputStream inputStream = this.inputStream;
263 if (inputStream == null) {
264 readerActive.set(false);
269 if (firstAttempt || inputStream.available() > 0) {
270 final byte[] readBuffer = new byte[20];
272 // read data from serial device
273 while (inputStream.available() > 0) {
274 final int bytes = inputStream.read(readBuffer);
275 sb.append(new String(readBuffer, 0, bytes, charset));
278 // Add wait states around reading the stream, so that interrupted transmissions
280 if (readerActive.get()) {
281 reader = scheduler.schedule(() -> receiveAndProcess(sb, false), 100, TimeUnit.MILLISECONDS);
285 final String result = sb.toString();
287 triggerChannel(TRIGGER_CHANNEL, CommonTriggerEvents.PRESSED);
288 refresh(STRING_CHANNEL, result);
289 refresh(BINARY_CHANNEL, result);
291 result.lines().forEach(l -> getThing().getThings().forEach(t -> {
292 final SerialDeviceHandler device = (SerialDeviceHandler) t.getHandler();
293 if (device != null) {
294 device.handleData(l);
300 if (readerActive.compareAndSet(true, false)) {
301 // Check we haven't received more data while processing
302 if (inputStream.available() > 0 && readerActive.compareAndSet(false, true)) {
303 reader = scheduler.schedule(() -> receiveAndProcess(new StringBuilder(), true), 0,
304 TimeUnit.MILLISECONDS);
308 } catch (final IOException e) {
309 logger.debug("Error reading from serial port: {}", e.getMessage(), e);
310 readerActive.set(false);
315 * Sends a string to the serial port.
317 * @param string the string to send
318 * @param isRawType the string should be handled as a RawType
320 private void writeString(final String string, final boolean isRawType) {
321 final OutputStream outputStream = this.outputStream;
323 if (outputStream == null) {
327 logger.debug("Writing '{}' to serial port {}", string, config.serialPort);
330 // write string to serial port
332 final RawType rt = RawType.valueOf(string);
333 outputStream.write(rt.getBytes());
335 outputStream.write(string.getBytes(charset));
338 outputStream.flush();
339 } catch (final IOException | IllegalArgumentException e) {
340 logger.warn("Error writing '{}' to serial port {}: {}", string, config.serialPort, e.getMessage());