/* $OpenBSD: ugold.c,v 1.29 2024/07/27 17:31:49 miod Exp $ */
/*
* Copyright (c) 2013 Takayoshi SASANO <uaa@openbsd.org>
* Copyright (c) 2013 Martin Pieuchot <mpi@openbsd.org>
* Copyright (c) 2015 Joerg Jung <jung@openbsd.org>
* Copyright (c) 2023 Miodrag Vallat.
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
* Driver for Microdia's HID based TEMPer and TEMPerHUM temperature and
* humidity sensors
*/
#include <sys/param.h>
#include <sys/systm.h>
#include <sys/device.h>
#include <sys/sensors.h>
#include <dev/usb/usb.h>
#include <dev/usb/usbhid.h>
#include <dev/usb/usbdi.h>
#include <dev/usb/usbdevs.h>
#include <dev/usb/uhidev.h>
#define UGOLD_INNER 0
#define UGOLD_OUTER 1
#define UGOLD_HUM 1
#define UGOLD_MAX_SENSORS 2
#define UGOLD_CMD_DATA 0x80
#define UGOLD_CMD_INIT 0x82
#define UGOLD_TYPE_INVALID -1
#define UGOLD_TYPE_SI7005 1
#define UGOLD_TYPE_SI7006 2
#define UGOLD_TYPE_SHT1X 3
#define UGOLD_TYPE_GOLD 4
#define UGOLD_TYPE_TEMPERX 5
#define UGOLD_TYPE_DS75 6
/*
* This driver uses three known commands for the TEMPer and TEMPerHUM
* devices.
*
* The first byte of the answer corresponds to the command and the
* second one seems to be the size (in bytes) of the answer.
*
* The device always sends 8 bytes and if the length of the answer
* is less than that, it just leaves the last bytes untouched. That
* is why most of the time the last n bytes of the answers are the
* same.
*
* The type command below seems to generate two answers with a
* string corresponding to the device, for example:
* 'TEMPer1F' and '1.1Per1F' (here Per1F is repeated).
*/
static uint8_t cmd_data[8] = { 0x01, 0x80, 0x33, 0x01, 0x00, 0x00, 0x00, 0x00 };
static uint8_t cmd_init[8] = { 0x01, 0x82, 0x77, 0x01, 0x00, 0x00, 0x00, 0x00 };
static uint8_t cmd_type[8] = { 0x01, 0x86, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00 };
/*
* The following command is also recognized and reports some kind of status
* byte (i.e. 87 xx 00 00 00 00 00 00).
{ 0x01, 0x87, 0xee, 0x01, 0x00, 0x00, 0x00, 0x00 };
*/
struct ugold_softc;
struct ugold_softc {
struct uhidev sc_hdev;
struct usbd_device *sc_udev;
int sc_num_sensors;
int sc_type;
char sc_model[16 + 1];
unsigned int sc_model_len;
struct ksensor sc_sensor[UGOLD_MAX_SENSORS];
struct ksensordev sc_sensordev;
struct sensor_task *sc_sensortask;
void (*sc_intr)(struct ugold_softc *, uint8_t *, u_int);
};
const struct usb_devno ugold_devs[] = {
{ USB_VENDOR_MICRODIA, USB_PRODUCT_MICRODIA_TEMPER },
{ USB_VENDOR_MICRODIA, USB_PRODUCT_MICRODIA_TEMPERHUM },
{ USB_VENDOR_PCSENSORS, USB_PRODUCT_PCSENSORS_TEMPER },
{ USB_VENDOR_RDING, USB_PRODUCT_RDING_TEMPER },
{ USB_VENDOR_WCH2, USB_PRODUCT_WCH2_TEMPER },
};
int ugold_match(struct device *, void *, void *);
void ugold_attach(struct device *, struct device *, void *);
int ugold_detach(struct device *, int);
void ugold_setup_sensors(struct ugold_softc *);
void ugold_intr(struct uhidev *, void *, u_int);
void ugold_ds75_intr(struct ugold_softc *, uint8_t *, u_int);
void ugold_si700x_intr(struct ugold_softc *, uint8_t *, u_int);
void ugold_refresh(void *);
int ugold_issue_cmd(struct ugold_softc *, uint8_t *, int);
struct cfdriver ugold_cd = {
NULL, "ugold", DV_DULL
};
const struct cfattach ugold_ca = {
sizeof(struct ugold_softc), ugold_match, ugold_attach, ugold_detach,
};
int
ugold_match(struct device *parent, void *match, void *aux)
{
struct uhidev_attach_arg *uha = aux;
int size;
void *desc;
if (UHIDEV_CLAIM_MULTIPLE_REPORTID(uha))
return (UMATCH_NONE);
if (usb_lookup(ugold_devs, uha->uaa->vendor, uha->uaa->product) == NULL)
return (UMATCH_NONE);
/*
* XXX Only match the sensor interface.
*
* Does it make sense to attach various uhidev(4) to these
* non-standard HID devices?
*/
uhidev_get_report_desc(uha->parent, &desc, &size);
if (hid_is_collection(desc, size, uha->reportid,
HID_USAGE2(HUP_GENERIC_DESKTOP, HUG_KEYBOARD)))
return (UMATCH_NONE);
return (UMATCH_VENDOR_PRODUCT);
}
void
ugold_attach(struct device *parent, struct device *self, void *aux)
{
struct ugold_softc *sc = (struct ugold_softc *)self;
struct uhidev_attach_arg *uha = aux;
int size, repid;
void *desc;
sc->sc_udev = uha->parent->sc_udev;
sc->sc_hdev.sc_parent = uha->parent;
sc->sc_hdev.sc_report_id = uha->reportid;
sc->sc_hdev.sc_intr = ugold_intr;
switch (uha->uaa->product) {
case USB_PRODUCT_MICRODIA_TEMPER:
sc->sc_intr = ugold_ds75_intr;
break;
case USB_PRODUCT_MICRODIA_TEMPERHUM:
case USB_PRODUCT_PCSENSORS_TEMPER:
case USB_PRODUCT_RDING_TEMPER:
case USB_PRODUCT_WCH2_TEMPER:
sc->sc_intr = ugold_si700x_intr;
break;
default:
printf(", unknown product\n");
return;
}
uhidev_get_report_desc(uha->parent, &desc, &size);
repid = uha->reportid;
sc->sc_hdev.sc_isize = hid_report_size(desc, size, hid_input, repid);
sc->sc_hdev.sc_osize = hid_report_size(desc, size, hid_output, repid);
sc->sc_hdev.sc_fsize = hid_report_size(desc, size, hid_feature, repid);
if (uhidev_open(&sc->sc_hdev)) {
printf(", unable to open interrupt pipe\n");
return;
}
strlcpy(sc->sc_sensordev.xname, sc->sc_hdev.sc_dev.dv_xname,
sizeof(sc->sc_sensordev.xname));
/* 0.166Hz */
sc->sc_sensortask = sensor_task_register(sc, ugold_refresh, 6);
if (sc->sc_sensortask == NULL) {
printf(", unable to register update task\n");
return;
}
printf("\n");
/* speed up sensor identification */
ugold_refresh(sc);
sensordev_install(&sc->sc_sensordev);
}
int
ugold_detach(struct device *self, int flags)
{
struct ugold_softc *sc = (struct ugold_softc *)self;
int i;
if (sc->sc_sensortask != NULL) {
sensor_task_unregister(sc->sc_sensortask);
sensordev_deinstall(&sc->sc_sensordev);
}
if (sc->sc_type != UGOLD_TYPE_INVALID) {
for (i = 0; i < sc->sc_num_sensors; i++)
sensor_detach(&sc->sc_sensordev, &sc->sc_sensor[i]);
}
if (sc->sc_hdev.sc_state & UHIDEV_OPEN)
uhidev_close(&sc->sc_hdev);
return (0);
}
void
ugold_setup_sensors(struct ugold_softc *sc)
{
int i;
switch (sc->sc_type) {
default:
return;
case UGOLD_TYPE_SI7005:
case UGOLD_TYPE_SI7006:
case UGOLD_TYPE_SHT1X:
case UGOLD_TYPE_TEMPERX:
/* 1 temperature and 1 humidity sensor */
sc->sc_sensor[UGOLD_INNER].type = SENSOR_TEMP;
strlcpy(sc->sc_sensor[UGOLD_INNER].desc, "inner",
sizeof(sc->sc_sensor[UGOLD_INNER].desc));
sc->sc_sensor[UGOLD_HUM].type = SENSOR_HUMIDITY;
strlcpy(sc->sc_sensor[UGOLD_HUM].desc, "RH",
sizeof(sc->sc_sensor[UGOLD_HUM].desc));
break;
case UGOLD_TYPE_GOLD:
case UGOLD_TYPE_DS75:
/* up to 2 temperature sensors */
sc->sc_sensor[UGOLD_INNER].type = SENSOR_TEMP;
strlcpy(sc->sc_sensor[UGOLD_INNER].desc, "inner",
sizeof(sc->sc_sensor[UGOLD_INNER].desc));
sc->sc_sensor[UGOLD_OUTER].type = SENSOR_TEMP;
strlcpy(sc->sc_sensor[UGOLD_OUTER].desc, "outer",
sizeof(sc->sc_sensor[UGOLD_OUTER].desc));
break;
}
for (i = 0; i < sc->sc_num_sensors; i++) {
sc->sc_sensor[i].flags |= SENSOR_FINVALID;
sensor_attach(&sc->sc_sensordev, &sc->sc_sensor[i]);
}
}
static void
strnvis(char *dst, const char *src, size_t siz)
{
char *start, *end;
int c;
for (start = dst, end = start + siz - 1; (c = *src) && dst < end; ) {
if (c >= 0x20 && c <= 0x7f) {
if (c == '\\') {
/* need space for the extra '\\' */
if (dst + 2 > end)
break;
*dst++ = '\\';
}
*dst++ = c;
} else {
if (dst + 4 > end)
break;
*dst++ = '\\';
*dst++ = ((u_char)c >> 6 & 07) + '0';
*dst++ = ((u_char)c >> 3 & 07) + '0';
*dst++ = ((u_char)c & 07) + '0';
}
src++;
}
if (siz > 0)
*dst = '\0';
}
static int
ugold_ds75_temp(uint8_t msb, uint8_t lsb)
{
/* DS75 12bit precision mode: 0.0625 degrees Celsius ticks */
return (((msb * 100) + ((lsb >> 4) * 25 / 4)) * 10000) + 273150000;
}
static void
ugold_ds75_type(struct ugold_softc *sc)
{
char model[4 * sizeof(sc->sc_model) + 1];
strnvis(model, sc->sc_model, sizeof model);
if (memcmp(sc->sc_model, "TEMPer1F", 8) == 0 ||
memcmp(sc->sc_model, "TEMPer2F", 8) == 0 ||
memcmp(sc->sc_model, "TEMPerF1", 8) == 0) {
sc->sc_type = UGOLD_TYPE_DS75;
ugold_setup_sensors(sc);
printf("%s: \"%s\", %d sensor%s"
" type ds75/12bit (temperature)\n",
sc->sc_hdev.sc_dev.dv_xname, model, sc->sc_num_sensors,
(sc->sc_num_sensors == 1) ? "" : "s");
ugold_refresh(sc);
return;
}
printf("%s: unknown model \"%s\"\n",
sc->sc_hdev.sc_dev.dv_xname, model);
sc->sc_num_sensors = 0;
sc->sc_type = UGOLD_TYPE_INVALID;
}
void
ugold_ds75_intr(struct ugold_softc *sc, uint8_t *buf, u_int len)
{
int temp;
switch (buf[0]) {
case UGOLD_CMD_INIT:
if (sc->sc_num_sensors != 0)
break;
sc->sc_num_sensors = imin(buf[1], UGOLD_MAX_SENSORS) /* XXX */;
ugold_refresh(sc);
break;
case UGOLD_CMD_DATA:
switch (buf[1]) {
case 4:
temp = ugold_ds75_temp(buf[4], buf[5]);
sc->sc_sensor[UGOLD_OUTER].value = temp;
sc->sc_sensor[UGOLD_OUTER].flags &= ~SENSOR_FINVALID;
/* FALLTHROUGH */
case 2:
temp = ugold_ds75_temp(buf[2], buf[3]);
sc->sc_sensor[UGOLD_INNER].value = temp;
sc->sc_sensor[UGOLD_INNER].flags &= ~SENSOR_FINVALID;
break;
default:
#ifdef UGOLD_DEBUG
printf("%s: invalid data length (%d bytes)\n",
sc->sc_hdev.sc_dev.dv_xname, buf[1]);
#endif
break;
}
break;
default:
ugold_ds75_type(sc);
break;
}
}
static int
ugold_si700x_temp(int type, uint8_t msb, uint8_t lsb)
{
int temp = msb * 256 + lsb;
switch (type) { /* convert to mdegC */
case UGOLD_TYPE_SI7005: /* 14bit 32 codes per degC 0x0000 = -50 degC */
temp = (((temp & 0x3fff) * 1000) / 32) - 50000;
break;
case UGOLD_TYPE_SI7006: /* 14bit and status bit */
temp = (((temp & ~3) * 21965) / 8192) - 46850;
break;
case UGOLD_TYPE_SHT1X:
temp = (temp * 1000) / 256;
break;
case UGOLD_TYPE_GOLD:
case UGOLD_TYPE_TEMPERX:
/* temp = temp / 100 to get degC, then * 1000 to get mdegC */
temp = temp * 10;
break;
default:
temp = 0;
}
return temp;
}
static int
ugold_si700x_rhum(int type, uint8_t msb, uint8_t lsb, int temp)
{
int rhum = msb * 256 + lsb;
switch (type) { /* convert to m%RH */
case UGOLD_TYPE_SI7005: /* 12bit 16 codes per %RH 0x0000 = -24 %RH */
rhum = (((rhum & 0x0fff) * 1000) / 16) - 24000;
#if 0 /* todo: linearization and temperature compensation */
rhum -= -0.00393 * rhum * rhum + 0.4008 * rhum - 4.7844;
rhum += (temp - 30) * (0.00237 * rhum + 0.1973);
#endif
break;
case UGOLD_TYPE_SI7006: /* 14bit and status bit */
rhum = (((rhum & ~3) * 15625) / 8192) - 6000;
break;
case UGOLD_TYPE_SHT1X: /* 16 bit */
rhum = rhum * 32;
break;
case UGOLD_TYPE_TEMPERX:
rhum = rhum * 10;
break;
default:
rhum = 0;
}
/* limit the humidity to valid values */
if (rhum < 0)
rhum = 0;
else if (rhum > 100000)
rhum = 100000;
return rhum;
}
static void
ugold_si700x_type(struct ugold_softc *sc)
{
char model[4 * sizeof(sc->sc_model) + 1];
const char *descr;
int nsensors = 0;
strnvis(model, sc->sc_model, sizeof model);
/* TEMPerHUM prefix */
if (sc->sc_model_len >= 9 &&
memcmp(sc->sc_model, "TEMPerHum", 9) == 0) {
if (memcmp(sc->sc_model + 9, "M12V1.0", 16 - 9) == 0) {
sc->sc_type = UGOLD_TYPE_SI7005;
descr = "si7005 (temperature and humidity)";
goto identified;
}
if (memcmp(sc->sc_model + 9, "M12V1.2", 16 - 9) == 0) {
sc->sc_type = UGOLD_TYPE_SI7006;
descr = "si7006 (temperature and humidity)";
goto identified;
}
}
if (sc->sc_model_len >= 9 &&
memcmp(sc->sc_model, "TEMPerHUM", 9) == 0) {
if (memcmp(sc->sc_model + 9, "_V3.9 ", 16 - 9) == 0 ||
memcmp(sc->sc_model + 9, "_V4.0 ", 16 - 9) == 0 ||
memcmp(sc->sc_model + 9, "_V4.1\0\0", 16 - 9) == 0) {
sc->sc_type = UGOLD_TYPE_TEMPERX;
descr = "temperx (temperature and humidity)";
goto identified;
}
}
/* TEMPerX prefix */
if (sc->sc_model_len >= 8 &&
memcmp(sc->sc_model, "TEMPerX_", 8) == 0) {
if (memcmp(sc->sc_model + 8, "V3.1 ", 16 - 8) == 0 ||
memcmp(sc->sc_model + 8, "V3.3 ", 16 - 8) == 0) {
sc->sc_type = UGOLD_TYPE_TEMPERX;
descr = "temperx (temperature and humidity)";
goto identified;
}
}
/* TEMPer1F or TEMPer2_ prefixes */
if (sc->sc_model_len >= 16 &&
memcmp(sc->sc_model, "TEMPer1F_H1V1.5F", 16) == 0) {
sc->sc_type = UGOLD_TYPE_SHT1X;
descr = "sht1x (temperature and humidity)";
goto identified;
}
if (sc->sc_model_len >= 16 &&
(memcmp(sc->sc_model, "TEMPer1F_V4.1\0\0\0", 16) == 0 ||
memcmp(sc->sc_model, "TEMPer2_V4.1\0\0\0\0", 16) == 0)) {
sc->sc_type = UGOLD_TYPE_GOLD;
/*
* TEMPer1F devices lack the internal sensor, but will never
* report data for it, so it will never get marked as valid.
* We thus keep the value of sc_num_sensors unchanged at 2,
* and make sure we will only report one single sensor below.
*/
if (sc->sc_model[6] == '1')
nsensors = 1;
descr = "gold (temperature only)";
goto identified;
}
/* TEMPerGold prefix */
if (sc->sc_model_len >= 11 &&
memcmp(sc->sc_model, "TEMPerGold_", 11) == 0) {
/*
* All V3.something models ought to work, but better be
* safe than sorry, and TEMPerHum models have been known
* to use slightly different sensors between models.
*/
if (memcmp(sc->sc_model + 11, "V3.1 ", 16 - 11) == 0 ||
memcmp(sc->sc_model + 11, "V3.4 ", 16 - 11) == 0 ||
memcmp(sc->sc_model + 11, "V3.5 ", 16 - 11) == 0) {
sc->sc_type = UGOLD_TYPE_GOLD;
sc->sc_num_sensors = 1;
descr = "gold (temperature only)";
goto identified;
}
}
printf("%s: unknown model \"%s\"\n",
sc->sc_hdev.sc_dev.dv_xname, model);
sc->sc_num_sensors = 0;
sc->sc_type = UGOLD_TYPE_INVALID;
return;
identified:
ugold_setup_sensors(sc);
if (nsensors == 0)
nsensors = sc->sc_num_sensors;
printf("%s: \"%s\", %d sensor%s type %s\n", sc->sc_hdev.sc_dev.dv_xname,
model, nsensors, (nsensors == 1) ? "" : "s", descr);
ugold_refresh(sc);
}
void
ugold_si700x_intr(struct ugold_softc *sc, uint8_t *buf, u_int len)
{
int temp, sensor, rhum;
switch (buf[0]) {
case UGOLD_CMD_INIT:
if (sc->sc_num_sensors != 0)
break;
/* XXX some devices report 0x04 here */
sc->sc_num_sensors = imin(buf[1], UGOLD_MAX_SENSORS);
ugold_refresh(sc);
break;
case UGOLD_CMD_DATA:
if (sc->sc_type == UGOLD_TYPE_GOLD) {
if (buf[1] == 0x80)
sensor = UGOLD_INNER;
else if (buf[1] == 0x01)
sensor = UGOLD_OUTER;
else
sensor = -1;
} else {
if (buf[1] == 0x04 || buf[1] == 0x20 ||
buf[1] == 0x40 || buf[1] == 0x80)
sensor = UGOLD_INNER;
else
sensor = -1;
}
if (sensor < 0) {
/* unexpected data, ignore */
#ifdef UGOLD_DEBUG
printf("%s: unexpected sensor id %02x\n",
sc->sc_hdev.sc_dev.dv_xname, buf[1]);
#endif
break;
}
temp = ugold_si700x_temp(sc->sc_type, buf[2], buf[3]);
sc->sc_sensor[sensor].value = (temp * 1000) + 273150000;
/*
* TEMPer1F and TEMPer2 report 200C when the sensor probe is
* missing or not plugged correctly.
*/
if (sc->sc_type == UGOLD_TYPE_GOLD && temp == 200000)
sc->sc_sensor[sensor].flags |= SENSOR_FINVALID;
else
sc->sc_sensor[sensor].flags &= ~SENSOR_FINVALID;
if (sc->sc_type != UGOLD_TYPE_GOLD) {
rhum = ugold_si700x_rhum(sc->sc_type, buf[4], buf[5], temp);
sc->sc_sensor[UGOLD_HUM].value = rhum;
sc->sc_sensor[UGOLD_HUM].flags &= ~SENSOR_FINVALID;
}
break;
default:
ugold_si700x_type(sc);
break;
}
}
void
ugold_intr(struct uhidev *addr, void *ibuf, u_int len)
{
struct ugold_softc *sc = (struct ugold_softc *)addr;
uint8_t *buf = ibuf;
unsigned long chunk;
#ifdef UGOLD_DEBUG
{
printf("%s: %u bytes\n", sc->sc_hdev.sc_dev.dv_xname, len);
u_int i;
for (i = 0; i < len; i++) {
if (i != 0 && (i % 8) == 0)
printf("\n");
printf("%02x ", buf[i]);
}
printf("\n");
}
#endif
switch (buf[0]) {
case UGOLD_CMD_INIT:
case UGOLD_CMD_DATA:
(*sc->sc_intr)(sc, buf, len);
break;
default:
if (!sc->sc_type) {
/*
* During initialization, some devices need a bit
* more time to submit their identification string.
*/
if (len == sc->sc_model_len &&
!memcmp(sc->sc_model, buf, len)) {
#ifdef UGOLD_DEBUG
printf("%s: duplicate string component\n",
sc->sc_hdev.sc_dev.dv_xname);
#endif
break;
}
/*
* Exact sensor type is not known yet, type command
* returns arbitrary string.
*/
chunk = ulmin(len,
sizeof(sc->sc_model) - 1 - sc->sc_model_len);
if (chunk != 0) {
memcpy(sc->sc_model + sc->sc_model_len, buf,
chunk);
sc->sc_model_len += chunk;
}
if (sc->sc_model_len > 8) {
/* should have enough data now */
(*sc->sc_intr)(sc, buf, len);
}
break;
}
printf("%s: unknown command 0x%02x\n",
sc->sc_hdev.sc_dev.dv_xname, buf[0]);
break;
}
}
void
ugold_refresh(void *arg)
{
struct ugold_softc *sc = arg;
int i;
/*
* Don't waste time talking to the device if we don't understand
* its language.
*/
if (sc->sc_type == UGOLD_TYPE_INVALID)
return;
if (!sc->sc_num_sensors) {
ugold_issue_cmd(sc, cmd_init, sizeof(cmd_init));
return;
}
if (!sc->sc_type) {
ugold_issue_cmd(sc, cmd_type, sizeof(cmd_type));
return;
}
if (ugold_issue_cmd(sc, cmd_data, sizeof(cmd_data))) {
for (i = 0; i < sc->sc_num_sensors; i++)
sc->sc_sensor[i].flags |= SENSOR_FINVALID;
}
}
int
ugold_issue_cmd(struct ugold_softc *sc, uint8_t *cmd, int len)
{
int actlen;
actlen = uhidev_set_report_async(sc->sc_hdev.sc_parent,
UHID_OUTPUT_REPORT, sc->sc_hdev.sc_report_id, cmd, len);
return (actlen != len);
}