Add rudimentary support for DS18B120 Thermometers, MH-Z19B CO2 Sensors, and ZWave.me controllers.

This commit is contained in:
MarkBryanMilligan 2021-10-26 15:45:13 -05:00
parent 883cf7865d
commit 88933a2286
19 changed files with 334 additions and 67 deletions

View File

@ -15,7 +15,7 @@
<dependencies>
<dependency>
<groupId>com.lanternsoftware.util</groupId>
<artifactId>lantern-util-common</artifactId>
<artifactId>lantern-util-servlet</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
@ -29,10 +29,25 @@
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>com.neuronrobotics</groupId>
<artifactId>nrjavaserial</artifactId>
<version>5.2.1</version>
</dependency>
<dependency>
<groupId>com.fazecast</groupId>
<artifactId>jSerialComm</artifactId>
<version>2.5.1</version>
</dependency>
<dependency>
<groupId>org.hid4java</groupId>
<artifactId>hid4java</artifactId>
<version>0.5.0</version>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>com.pi4j</groupId>
<artifactId>pi4j-device</artifactId>
<version>1.3</version>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,41 @@
package com.lanternsoftware.thermometer;
import com.lanternsoftware.util.CollectionUtils;
import com.pi4j.component.temperature.TemperatureSensor;
import com.pi4j.component.temperature.impl.TmpDS18B20DeviceType;
import com.pi4j.io.w1.W1Device;
import com.pi4j.io.w1.W1Master;
import java.util.List;
public class DS18B20Thermometer implements IThermometer {
W1Device device;
public static List<DS18B20Thermometer> devices() {
W1Master master = new W1Master();
return CollectionUtils.transform(master.getDevices(TmpDS18B20DeviceType.FAMILY_CODE), DS18B20Thermometer::new);
}
public DS18B20Thermometer() {
W1Master master = new W1Master();
device = CollectionUtils.getFirst(master.getDevices(TmpDS18B20DeviceType.FAMILY_CODE));
}
public DS18B20Thermometer(W1Device _device) {
device = _device;
}
@Override
public double getTemperatureCelsius() {
return device == null?-273:((TemperatureSensor) device).getTemperature();
}
@Override
public boolean isConnected() {
return device != null;
}
@Override
public void shutdown() {
}
}

View File

@ -1,4 +1,4 @@
package com.lanternsoftware.thermometer.context;
package com.lanternsoftware.thermometer;
import com.lanternsoftware.util.NullUtils;
import com.lanternsoftware.util.concurrency.ConcurrencyUtils;
@ -11,14 +11,15 @@ import org.slf4j.LoggerFactory;
import java.util.Timer;
import java.util.TimerTask;
public class ThermometerApp {
private static final Logger LOG = LoggerFactory.getLogger(ThermometerApp.class);
public class HidThermometer implements IThermometer{
private static final Logger LOG = LoggerFactory.getLogger(HidThermometer.class);
private HidDevice device;
private final Timer timer = new Timer();
private double lastTemp;
private final byte[] READ = hexToByte("0180330100000000");
public void start() {
public HidThermometer() {
HidServices hs = HidManager.getHidServices();
for (HidDevice d : hs.getAttachedHidDevices()) {
if (NullUtils.isEqual(d.getVendorId(), (short) 0x413d) && NullUtils.isEqual(d.getProductId(), (short) 0x2107)) {
@ -27,11 +28,13 @@ public class ThermometerApp {
}
}
if ((device != null) && device.open()) {
synchronized (device) {
read(hexToByte("0182770100000000"));
read(hexToByte("0186ff0100000000"));
read(hexToByte("0182770100000000"));
read(hexToByte("0182770100000000"));
final byte[] INIT1 = hexToByte("0182770100000000");
final byte[] INIT2 = hexToByte("0186ff0100000000");
synchronized (this) {
read(INIT1);
read(INIT2);
read(INIT1);
read(INIT1);
}
} else {
LOG.error("Failed to open HID Device");
@ -45,7 +48,11 @@ public class ThermometerApp {
}, 0L, 10000L);
}
public void stop() {
public boolean isConnected() {
return device != null;
}
public void shutdown() {
timer.cancel();
ConcurrencyUtils.sleep(10000);
if (device != null) {
@ -100,14 +107,14 @@ public class ThermometerApp {
}
return response;
}
public double getTemperature() {
public double getTemperatureCelsius() {
return lastTemp;
}
public double readTemperature() {
private double readTemperature() {
if (device != null) {
synchronized (device) {
byte[] response = read(hexToByte("0180330100000000"));
synchronized (this) {
byte[] response = read(READ);
if (response == null)
return 5.0;
int rawReading = ((response[3] & 0xFF) + (response[2] << 8));

View File

@ -0,0 +1,6 @@
package com.lanternsoftware.thermometer;
public interface ICO2Sensor {
int getPPM();
void shutdown();
}

View File

@ -0,0 +1,7 @@
package com.lanternsoftware.thermometer;
public interface IThermometer {
double getTemperatureCelsius();
boolean isConnected();
void shutdown();
}

View File

@ -0,0 +1,165 @@
package com.lanternsoftware.thermometer;
import com.lanternsoftware.util.concurrency.ConcurrencyUtils;
import gnu.io.CommPortIdentifier;
import gnu.io.SerialPort;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicInteger;
public class MHZ19BCO2Sensor implements ICO2Sensor {
private static final Logger LOG = LoggerFactory.getLogger(MHZ19BCO2Sensor.class);
private static final int DEFAULT_TIMEOUT = 1000;
private static final byte[] CMD_GAS_CONCENTRATION = {(byte)0xff, 0x01, (byte)0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};
private static final byte[] CMD_CALIBRATE_ZERO_POINT = {(byte)0xff, 0x01, (byte)0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78};
private static final byte[] CMD_AUTO_CALIBRATION_ON_WITHOUT_CHECKSUM = {(byte)0xff, 0x01, (byte)0x79, (byte)0xa0, 0x00, 0x00, 0x00, 0x00};
private static final byte[] CMD_AUTO_CALIBRATION_OFF_WITHOUT_CHECKSUM = {(byte)0xff, 0x01, (byte)0x79, 0x00, 0x00, 0x00, 0x00, 0x00};
private static final int CALIBRATE_SPAN_POINT_MIN = 1000;
private SerialPort serialPort;
private InputStream is;
private OutputStream os;
private MHZ19BCO2Sensor(String _port) {
this(_port, DEFAULT_TIMEOUT);
}
private MHZ19BCO2Sensor(String _port, int _timeout) {
try {
CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier(_port);
serialPort = portIdentifier.open("co2port", 2000);
serialPort.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
serialPort.enableReceiveTimeout(_timeout);
serialPort.enableReceiveThreshold(9);
is = serialPort.getInputStream();
os = serialPort.getOutputStream();
} catch (Exception _e) {
if (serialPort != null) {
serialPort.close();
serialPort = null;
}
LOG.error("Exception while starting MHZ19BCO2Sensor", _e);
}
}
public void shutdown() {
IOUtils.closeQuietly(is);
IOUtils.closeQuietly(os);
if (serialPort != null)
serialPort.close();
}
private void write(byte[] out) {
try {
int length = is.available();
if (length > 0) {
byte[] unread = new byte[length];
int read = is.read(unread, 0, length);
LOG.debug("deleted unread buffer length:{}", read);
}
os.write(out, 0, out.length);
}
catch (Exception _e) {
LOG.error("Exception while writing to MHZ19B", _e);
}
}
private byte getCheckSum(byte[] data) {
int ret = 0;
for (int i = 1; i <= 7; i++) {
ret += data[i];
}
return (byte)(~(byte)(ret & 0x000000ff) + 1);
}
private byte[] getCommandWithCheckSum(byte[] baseCommand) {
byte[] checkSum = {getCheckSum(baseCommand)};
byte[] data = new byte[baseCommand.length + 1];
System.arraycopy(baseCommand, 0, data, 0, baseCommand.length);
System.arraycopy(checkSum, 0, data, baseCommand.length, 1);
return data;
}
@Override
public int getPPM() {
write(CMD_GAS_CONCENTRATION);
try {
ByteBuffer buf = ByteBuffer.allocate(2);
byte[] data = new byte[9];
if (is.read(data, 0, 9) < 9)
return 0;
buf.put(data[2]);
buf.put(data[3]);
return buf.getShort(0);
}
catch (Exception _e) {
LOG.error("Could not read value from MHZ19B", _e);
return 0;
}
}
public void setCalibrateZeroPoint() {
write(CMD_CALIBRATE_ZERO_POINT);
}
public void setCalibrateSpanPoint(int point) {
if (point < CALIBRATE_SPAN_POINT_MIN) {
LOG.info("since span needs at least {} ppm, set it to {} ppm.", CALIBRATE_SPAN_POINT_MIN, CALIBRATE_SPAN_POINT_MIN);
point = CALIBRATE_SPAN_POINT_MIN;
}
byte high = (byte)((point / 256) & 0x000000ff);
byte low = (byte)((point % 256) & 0x000000ff);
byte[] CMD_CALIBRATE_SPAN_POINT = {(byte)0xff, 0x01, (byte)0x88, high, low, 0x00, 0x00, 0x00};
write(getCommandWithCheckSum(CMD_CALIBRATE_SPAN_POINT));
LOG.info("set the calibration span point to {} ppm.", point);
}
public void setAutoCalibration(boolean set) {
if (set) {
write(getCommandWithCheckSum(CMD_AUTO_CALIBRATION_ON_WITHOUT_CHECKSUM));
LOG.info("set auto calibration to ON.");
} else {
write(getCommandWithCheckSum(CMD_AUTO_CALIBRATION_OFF_WITHOUT_CHECKSUM));
LOG.info("set auto calibration to OFF.");
}
}
private void setDetectionRange(int range) {
byte high = (byte)((range / 256) & 0x000000ff);
byte low = (byte)((range % 256) & 0x000000ff);
byte[] CMD_DETECTION_RANGE = {(byte)0xff, 0x01, (byte)0x99, high, low, 0x00, 0x00, 0x00};
write(getCommandWithCheckSum(CMD_DETECTION_RANGE));
LOG.info("set the detection range to {} ppm.", range);
}
public void setDetectionRange2000() {
setDetectionRange(2000);
}
public void setDetectionRange5000() {
setDetectionRange(5000);
}
public static void main(String[] args) {
MHZ19BCO2Sensor mhz19b = new MHZ19BCO2Sensor("/dev/ttyAMA0");
mhz19b.setDetectionRange5000();
mhz19b.setAutoCalibration(false);
AtomicInteger i = new AtomicInteger(0);
while (i.incrementAndGet() < 2000) {
LOG.debug("co2: {}PPM", mhz19b.getPPM());
ConcurrencyUtils.sleep(5000);
}
Runtime.getRuntime().addShutdownHook(new Thread(mhz19b::shutdown, "Shutdown"));
}
}

View File

@ -1,16 +0,0 @@
package com.lanternsoftware.thermometer;
import com.lanternsoftware.thermometer.context.ThermometerApp;
public class TestThermo {
public static void main(String[] args) {
ThermometerApp app = new ThermometerApp();
app.start();
try {
Thread.sleep(20000);
} catch (InterruptedException _e) {
_e.printStackTrace();
}
app.stop();
}
}

View File

@ -1,22 +1,27 @@
package com.lanternsoftware.thermometer.context;
import com.lanternsoftware.thermometer.DS18B20Thermometer;
import com.lanternsoftware.thermometer.HidThermometer;
import com.lanternsoftware.thermometer.IThermometer;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.util.ArrayList;
import java.util.List;
public class Globals implements ServletContextListener {
public static ThermometerApp app;
public static List<IThermometer> thermometers = new ArrayList<>();
@Override
public void contextInitialized(ServletContextEvent sce) {
app = new ThermometerApp();
app.start();
IThermometer t = new HidThermometer();
if (t.isConnected())
thermometers.add(t);
thermometers.addAll(DS18B20Thermometer.devices());
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
if (app != null) {
app.stop();
app = null;
}
thermometers.forEach(IThermometer::shutdown);
}
}

View File

@ -1,18 +1,23 @@
package com.lanternsoftware.thermometer.servlet;
import com.lanternsoftware.thermometer.IThermometer;
import com.lanternsoftware.thermometer.context.Globals;
import com.lanternsoftware.util.CollectionUtils;
import com.lanternsoftware.util.dao.DaoSerializer;
import com.lanternsoftware.util.servlet.LanternServlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/temp")
public class TempServlet extends ThermoServlet {
@WebServlet("/temp/*")
public class TempServlet extends LanternServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
setResponseEntity(resp, "application/json", "{\"temp\": "+ Globals.app.getTemperature() + "}");
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
int idx = DaoSerializer.toInteger(CollectionUtils.get(path(req), 0));
IThermometer therm = CollectionUtils.get(Globals.thermometers, idx);
double temp = therm == null ? -273 : therm.getTemperatureCelsius();
setResponseEntity(resp, "application/json", "{\"temp\": "+ temp + "}");
}
}

View File

@ -9,9 +9,9 @@
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/opt/currentmonitor/log/log.txt</file>
<file>/opt/tomcat/log/thermo.txt</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>/opt/currentmonitor/log/log.%d{yyyy-MM-dd}.%i.txt</fileNamePattern>
<fileNamePattern>/opt/currentmonitor/log/thermo.%d{yyyy-MM-dd}.%i.txt</fileNamePattern>
<maxFileSize>20MB</maxFileSize>
<maxHistory>20</maxHistory>
</rollingPolicy>
@ -20,9 +20,10 @@
</encoder>
</appender>
<logger name="com.lanternsoftware" level="INFO"/>
<logger name="com.lanternsoftware" level="DEBUG"/>
<root level="OFF">
<appender-ref ref="FILE"/>
<appender-ref ref="STDOUT"/>
</root>
</configuration>

View File

@ -1,16 +1,14 @@
package com.lanternsoftware.thermometer;
import com.lanternsoftware.thermometer.context.ThermometerApp;
import com.lanternsoftware.util.concurrency.ConcurrencyUtils;
public class TestStartup {
public static void main(String[] args) {
ThermometerApp app = new ThermometerApp();
app.start();
try {
Thread.sleep(20000);
} catch (InterruptedException _e) {
_e.printStackTrace();
IThermometer thermometer = new DS18B20Thermometer();
for (int i=0; i<200; i++) {
System.out.println(String.format("%.2f", thermometer.getTemperatureCelsius()));
ConcurrencyUtils.sleep(1000);
}
app.stop();
thermometer.shutdown();
}
}

View File

@ -2,6 +2,7 @@ package com.lanternsoftware.zwave;
import com.lanternsoftware.datamodel.zwave.Switch;
import com.lanternsoftware.datamodel.zwave.SwitchType;
import com.lanternsoftware.util.concurrency.ConcurrencyUtils;
import com.lanternsoftware.zwave.security.SecurityController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -14,6 +15,8 @@ public class TestSecurity {
Switch sw = new Switch("Garage", "Door 1", 1000, true, false, null, 0);
sw.setGpioPin(7);
sw.setType(SwitchType.SECURITY);
c.listen(sw, (nodeId, _open) -> LOG.info("Door is " + (_open ? "OPEN" : "CLOSED")));
c.listen(sw, (nodeId, _open) -> LOG.error("Door event, now " + (_open ? "OPEN" : "CLOSED")));
ConcurrencyUtils.sleep(60000);
c.shutdown();
}
}

View File

@ -22,6 +22,7 @@ import com.lanternsoftware.zwave.controller.Controller;
import com.lanternsoftware.zwave.dao.MongoZWaveDao;
import com.lanternsoftware.zwave.message.IMessageSubscriber;
import com.lanternsoftware.zwave.message.MessageEngine;
import com.lanternsoftware.zwave.message.impl.AddNodeToNetworkRequest;
import com.lanternsoftware.zwave.message.impl.BinarySwitchReportRequest;
import com.lanternsoftware.zwave.message.impl.BinarySwitchSetRequest;
import com.lanternsoftware.zwave.message.impl.CRC16EncapRequest;
@ -226,6 +227,8 @@ public class ZWaveApp {
}
});
controller.send(new MultilevelSwitchSetRequest((byte)2, 0xFF));
// controller.send(new MultilevelSensorGetRequest((byte)11));
// controller.send(new ThermostatSetPointGetRequest((byte)11, ThermostatSetPointIndex.HEATING));
// controller.send(new ThermostatSetPointGetRequest((byte)11, ThermostatSetPointIndex.COOLING));

View File

@ -9,7 +9,7 @@
</appender>
<logger name="com.lanternsoftware" level="INFO"/>
<logger name="com.lanternsoftware" level="DEBUG"/>
<root level="OFF">
<appender-ref ref="STDOUT"/>

View File

@ -7,10 +7,10 @@ public class TestStartup {
ZWaveApp app = new ZWaveApp();
app.start();
try {
Thread.sleep(20000);
Thread.sleep(20000000);
} catch (InterruptedException _e) {
_e.printStackTrace();
}
app.stop();
Runtime.getRuntime().addShutdownHook(new Thread(app::stop, "Shutdown"));
}
}

View File

@ -39,13 +39,13 @@ public class Controller {
private SerialPort serialPort;
private OutputStream os;
private boolean running = false;
private AtomicInteger callbackId = new AtomicInteger(0);
private final AtomicInteger callbackId = new AtomicInteger(0);
private final Object ackMutex = new Object();
private final Object responseMutex = new Object();
private final Object callbackMutex = new Object();
private boolean responseReceived;
private final Map<Byte, Byte> callbacks = new HashMap<>();
private ExecutorService executor = Executors.newFixedThreadPool(2);
private final ExecutorService executor = Executors.newFixedThreadPool(2);
private NodeManager nodeManager;
public boolean start(String _port) {
@ -172,9 +172,10 @@ public class Controller {
logger.debug("Finished outbound of: {}", message.describe());
}
if (message instanceof RequestMessage) {
logger.debug("Waiting for response from: {}", message.describe());
synchronized (responseMutex) {
responseMutex.wait(1000);
logger.debug("Waiting for response from: {}", message.describe());
if (!responseReceived)
responseMutex.wait(1000);
logger.debug("Response received: {}", responseReceived);
responseReceived = false;
}

View File

@ -33,9 +33,17 @@ public abstract class MessageEngine {
}
MessageType messageType = _data[2] == 0x00 ? MessageType.REQUEST : MessageType.RESPONSE;
ControllerMessageType controllerMessageType = ControllerMessageType.fromByte((byte)(_data[3] & 0xFF));
int offset = ((messageType == MessageType.REQUEST) && NullUtils.isOneOf(controllerMessageType, ControllerMessageType.SendData, ControllerMessageType.ApplicationCommandHandler))?7:5;
CommandClass commandClass = _data.length > offset + 1 ? CommandClass.fromByte((byte)(_data[offset] & 0xFF)):CommandClass.NO_OPERATION;
byte command = ((commandClass == CommandClass.NO_OPERATION) || (_data.length <= offset+2))?0:(byte)(_data[offset+1] & 0xFF);
CommandClass commandClass = CommandClass.NO_OPERATION;
byte command = 0;
int offset = 5;
if (NullUtils.isOneOf(controllerMessageType, ControllerMessageType.SendData, ControllerMessageType.ApplicationCommandHandler)) {
if (messageType == MessageType.REQUEST)
offset = 7;
if (_data.length > offset + 1)
commandClass = CommandClass.fromByte((byte)(_data[offset] & 0xFF));
if (_data.length > offset + 2)
command = (byte)(_data[offset+1] & 0xFF);
}
Message message = messages.get(Message.toKey(controllerMessageType.data, messageType.data, commandClass.data, command));
if (message == null) {
logger.debug("Could not find message class for message: {} {} {} {}", controllerMessageType.label, messageType.name(), commandClass.label, command);

View File

@ -0,0 +1,17 @@
package com.lanternsoftware.zwave.message.impl;
import com.lanternsoftware.zwave.message.ControllerMessageType;
import com.lanternsoftware.zwave.message.NoCommandRequestMessage;
public class AddNodeToNetworkRequest extends NoCommandRequestMessage {
public AddNodeToNetworkRequest() {
super(ControllerMessageType.AddNodeToNetwork);
}
@Override
public byte[] getPayload() {
byte[] payload = new byte[1];
payload[0] = (byte)0x05;
return payload;
}
}

View File

@ -1,3 +1,4 @@
com.lanternsoftware.zwave.message.impl.AddNodeToNetworkRequest
com.lanternsoftware.zwave.message.impl.ApplicationUpdateRequest
com.lanternsoftware.zwave.message.impl.AssociationGetRequest
com.lanternsoftware.zwave.message.impl.AssociationReportRequest