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();
}
}