]> git.basschouten.com Git - openhab-addons.git/blob
84001426f17e1dd2929f0cadf256cab0d3311029
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.serial.internal.handler;
14
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;
18
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;
30
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;
54
55 /**
56  * The {@link SerialBridgeHandler} is responsible for handling commands, which
57  * are sent to one of the channels.
58  *
59  * @author Mike Major - Initial contribution
60  */
61 @NonNullByDefault
62 public class SerialBridgeHandler extends BaseBridgeHandler implements SerialPortEventListener {
63
64     private final Logger logger = LoggerFactory.getLogger(SerialBridgeHandler.class);
65
66     private SerialBridgeConfiguration config = new SerialBridgeConfiguration();
67
68     private final SerialPortManager serialPortManager;
69     private @Nullable SerialPort serialPort;
70
71     private @Nullable InputStream inputStream;
72     private @Nullable OutputStream outputStream;
73
74     private Charset charset = StandardCharsets.UTF_8;
75
76     private @Nullable String lastValue;
77
78     private final AtomicBoolean readerActive = new AtomicBoolean(false);
79     private @Nullable ScheduledFuture<?> reader;
80
81     public SerialBridgeHandler(final Bridge bridge, final SerialPortManager serialPortManager) {
82         super(bridge);
83         this.serialPortManager = serialPortManager;
84     }
85
86     @Override
87     public void handleCommand(final ChannelUID channelUID, final Command command) {
88         if (command instanceof RefreshType) {
89             final String lastValue = this.lastValue;
90
91             if (lastValue != null) {
92                 refresh(channelUID.getId(), lastValue);
93             }
94         } else {
95             switch (channelUID.getId()) {
96                 case STRING_CHANNEL:
97                     writeString(command.toFullString(), false);
98                     break;
99                 case BINARY_CHANNEL:
100                     writeString(command.toFullString(), true);
101                     break;
102                 default:
103                     break;
104             }
105
106         }
107     }
108
109     @Override
110     public void initialize() {
111         config = getConfigAs(SerialBridgeConfiguration.class);
112
113         try {
114             if (config.charset != null) {
115                 charset = Charset.forName(config.charset);
116             }
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");
120             return;
121         }
122
123         final String port = config.serialPort;
124         if (port == null) {
125             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port must be set");
126             return;
127         }
128
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");
133             return;
134         }
135
136         // initialize serial port
137         try {
138             final SerialPort serialPort = portId.open(getThing().getUID().toString(), 2000);
139             this.serialPort = serialPort;
140
141             serialPort.setSerialPortParams(config.baudRate, config.dataBits,
142                     StopBits.fromConfig(config.stopBits).getSerialPortValue(),
143                     Parity.fromConfig(config.parity).getSerialPortValue());
144
145             serialPort.addEventListener(this);
146
147             // activate the DATA_AVAILABLE notifier
148             serialPort.notifyOnDataAvailable(true);
149             inputStream = serialPort.getInputStream();
150             outputStream = serialPort.getOutputStream();
151
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());
163         }
164     }
165
166     @Override
167     public void dispose() {
168         final SerialPort serialPort = this.serialPort;
169         if (serialPort != null) {
170             serialPort.removeEventListener();
171             serialPort.close();
172             this.serialPort = null;
173         }
174
175         final InputStream inputStream = this.inputStream;
176         if (inputStream != null) {
177             try {
178                 inputStream.close();
179             } catch (final IOException e) {
180                 logger.debug("Error while closing the input stream: {}", e.getMessage());
181             }
182             this.inputStream = null;
183         }
184
185         final OutputStream outputStream = this.outputStream;
186         if (outputStream != null) {
187             try {
188                 outputStream.close();
189             } catch (final IOException e) {
190                 logger.debug("Error while closing the output stream: {}", e.getMessage());
191             }
192             this.outputStream = null;
193         }
194
195         readerActive.set(false);
196         final ScheduledFuture<?> reader = this.reader;
197         if (reader != null) {
198             reader.cancel(false);
199             this.reader = null;
200         }
201
202         lastValue = null;
203     }
204
205     @Override
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);
212                 }
213                 break;
214             default:
215                 break;
216         }
217     }
218
219     /**
220      * Sends a string to the serial port.
221      *
222      * @param string the string to send
223      */
224     public void writeString(final String string) {
225         writeString(string, false);
226     }
227
228     /**
229      * Refreshes the channel with the last received data
230      *
231      * @param channelId the channel to refresh
232      * @param channelId the data to use
233      */
234     private void refresh(final String channelId, final String data) {
235         if (!isLinked(channelId)) {
236             return;
237         }
238
239         switch (channelId) {
240             case STRING_CHANNEL:
241                 updateState(channelId, new StringType(data));
242                 break;
243             case BINARY_CHANNEL:
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()));
248                 break;
249             default:
250                 break;
251         }
252     }
253
254     /**
255      * Read from the serial port and process the data
256      * 
257      * @param sb the string builder to receive the data
258      * @param firstAttempt indicates if this is the first read attempt without waiting
259      */
260     private void receiveAndProcess(final StringBuilder sb, final boolean firstAttempt) {
261         final InputStream inputStream = this.inputStream;
262
263         if (inputStream == null) {
264             readerActive.set(false);
265             return;
266         }
267
268         try {
269             if (firstAttempt || inputStream.available() > 0) {
270                 final byte[] readBuffer = new byte[20];
271
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));
276                 }
277
278                 // Add wait states around reading the stream, so that interrupted transmissions
279                 // are merged
280                 if (readerActive.get()) {
281                     reader = scheduler.schedule(() -> receiveAndProcess(sb, false), 100, TimeUnit.MILLISECONDS);
282                 }
283
284             } else {
285                 final String result = sb.toString();
286
287                 triggerChannel(TRIGGER_CHANNEL, CommonTriggerEvents.PRESSED);
288                 refresh(STRING_CHANNEL, result);
289                 refresh(BINARY_CHANNEL, result);
290
291                 result.lines().forEach(l -> getThing().getThings().forEach(t -> {
292                     final SerialDeviceHandler device = (SerialDeviceHandler) t.getHandler();
293                     if (device != null) {
294                         device.handleData(l);
295                     }
296                 }));
297
298                 lastValue = result;
299
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);
305                     }
306                 }
307             }
308         } catch (final IOException e) {
309             logger.debug("Error reading from serial port: {}", e.getMessage(), e);
310             readerActive.set(false);
311         }
312     }
313
314     /**
315      * Sends a string to the serial port.
316      *
317      * @param string the string to send
318      * @param isRawType the string should be handled as a RawType
319      */
320     private void writeString(final String string, final boolean isRawType) {
321         final OutputStream outputStream = this.outputStream;
322
323         if (outputStream == null) {
324             return;
325         }
326
327         logger.debug("Writing '{}' to serial port {}", string, config.serialPort);
328
329         try {
330             // write string to serial port
331             if (isRawType) {
332                 final RawType rt = RawType.valueOf(string);
333                 outputStream.write(rt.getBytes());
334             } else {
335                 outputStream.write(string.getBytes(charset));
336             }
337
338             outputStream.flush();
339         } catch (final IOException | IllegalArgumentException e) {
340             logger.warn("Error writing '{}' to serial port {}: {}", string, config.serialPort, e.getMessage());
341         }
342     }
343 }