Skip to content

Instantly share code, notes, and snippets.

@classilla
Created December 20, 2021 05:49
Show Gist options
  • Save classilla/a096420fbe717ddea2ee9884cfbdfa88 to your computer and use it in GitHub Desktop.
Save classilla/a096420fbe717ddea2ee9884cfbdfa88 to your computer and use it in GitHub Desktop.
OS X (10.4+) and Linux code for reading a THUM USB-based temperature/humidity monitor. See https://oldvcr.blogspot.com/2021/12/monitoring-vintage-server-room-and.html
/*
(C)2020-1 Cameron Kaiser, ckaiser@floodgap.com
All rights reserved.
Distributed under the Floodgap Free Software License.
Linux:
gcc -o thum thum.c -lhid -lusb
Mac OS X:
gcc -o thum thum.c -framework CoreFoundation -framework IOKit
*/
#define DEBUG 0 /* Set non-zero for copious debugging output */
#define THUM_VENDID 0x0C70
#define THUM_DEVID 0x0750
#define SEND_PACKET_LENGTH 2 /* size of an instruction packet */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#if defined(__linux__)
#include <hid.h>
bool matcher_skip_fn(struct usb_dev_handle const* dev_h, void* custom, unsigned int len) {
struct usb_device const* dev = usb_device((usb_dev_handle*)dev_h);
unsigned int* skip = custom;
if (*skip == 0) {
return true;
}
if (dev->descriptor.idVendor == THUM_VENDID && dev->descriptor.idProduct == THUM_DEVID) {
*skip = *skip - 1;
}
return false;
}
#elif defined(__APPLE__)
#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/IOCFPlugIn.h>
#include <IOKit/usb/IOUSBLib.h>
#include <IOKit/usb/USBSpec.h>
#include <IOKit/hid/IOHIDLib.h>
#include <IOKit/hid/IOHIDKeys.h>
unsigned char ready = 0;
CFMutableDictionaryRef _getMatchingDictionary(UInt16 version) {
CFMutableDictionaryRef matchingDict = NULL;
int val;
CFNumberRef valRef;
matchingDict = IOServiceMatching(kIOHIDDeviceKey);
if(matchingDict) {
val = THUM_VENDID;
valRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &val);
CFDictionarySetValue(matchingDict, CFSTR(kIOHIDVendorIDKey), valRef);
CFRelease(valRef);
val = THUM_DEVID;
valRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &val);
CFDictionarySetValue(matchingDict, CFSTR(kIOHIDProductIDKey), valRef);
CFRelease(valRef);
}
return matchingDict;
}
io_service_t _getIOService(CFMutableDictionaryRef matchingDict) {
if(!matchingDict) return (io_service_t)NULL;
return IOServiceGetMatchingService(kIOMasterPortDefault, matchingDict);
}
IOHIDDeviceInterface122** _getHIDInterface(io_service_t service) {
IOCFPlugInInterface** iodev = NULL;
IOHIDDeviceInterface122** hidInterface = NULL;
kern_return_t result;
SInt32 score = 0;
if(!service) return NULL;
result = IOCreatePlugInInterfaceForService(service, kIOHIDDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &iodev, &score);
if (result == KERN_SUCCESS && iodev) {
result = (*iodev)->QueryInterface(iodev, CFUUIDGetUUIDBytes(kIOHIDDeviceInterfaceID), (LPVOID) &hidInterface);
if (result != KERN_SUCCESS)
hidInterface = NULL;
(*iodev)->Release(iodev);
}
return hidInterface;
}
static void reportcallback(void *target, IOReturn result, void *refcon, void *sender, UInt32 size)
{
if (DEBUG)
fprintf(stderr, "received %d bytes\n", size);
ready=1;
}
/* Laziness */
#define DAMMIT(x, y, z) \
x = y; if (!x) { fprintf(stderr, z); return -1; }
#define KDAMMIT(y, z, ...) \
result = y; if (result != KERN_SUCCESS) { fprintf(stderr, z, __VA_ARGS__); return -1; }
#else
#error unsupported OS
#endif
/* The THUM requires you fetch both values, so we do that. */
int main(int argc, const char** argv) {
/* temperature: 0x0000, humidity: 0x0100. read temp first. */
unsigned char PACKET[SEND_PACKET_LENGTH] = { 0x00, 0x00 };
double tempc;
int invalid = 1;
#if defined(__linux__)
hid_return ret;
HIDInterface* hid;
unsigned int skip = 0;
HIDInterfaceMatcher matcher = { THUM_VENDID, THUM_DEVID, *matcher_skip_fn, &skip, 0 };
if (DEBUG) {
hid_set_debug(HID_DEBUG_ALL);
hid_set_debug_stream(stderr);
hid_set_usb_debug(0);
}
ret = hid_init();
if (ret != HID_RET_SUCCESS) {
fprintf(stderr, "hid_init failed, return code %d\n", ret);
return 1;
}
hid = hid_new_HIDInterface();
if (hid == 0) {
perror("hid_new_HIDInterface");
return 1;
}
ret = hid_force_open(hid, 0, &matcher, 3);
if (ret != HID_RET_SUCCESS) {
fprintf(stderr, "no THUM: return code %d\n", ret);
return 1;
}
/* usb is really poorly documented in Linux */
/*
int usb_control_msg(usb_dev_handle *dev, int requesttype, int request,
int value, int index, char *bytes, int size, int timeout);
*/
ret = usb_control_msg( // Endpoint transfer type: Control (0)
hid->dev_handle,
0x21, // bmRequestType
0x09, // bRequest
0x0301, // wValue
0x0000, // wIndex
(char *)PACKET,
SEND_PACKET_LENGTH, // wLength
10000);
if (DEBUG) {
fprintf(stderr, "usb_control_transfer: %d\n", ret);
if (ret != 2)
return 1;
} else if (ret != 2) {
fprintf(stderr, "usb_control_transfer: %d\n", ret);
return 1;
}
/*
int usb_interrupt_read(usb_dev_handle *dev, int ep, char *bytes, int size,
int timeout);
*/
ret = usb_interrupt_read( // Endpoint transfer type: Interrupt (3)
hid->dev_handle,
0x81, // Endpoint address
(char *)PACKET,
SEND_PACKET_LENGTH,
10000);
if (DEBUG) {
fprintf(stderr, "usb_interrupt_read: %d\n", ret);
if (ret != 2)
return 1;
} else if (ret != 2) {
fprintf(stderr, "usb_interrupt_read: %d\n", ret);
return 1;
}
#elif defined(__APPLE__)
CFRunLoopSourceRef rl;
CFMutableDictionaryRef matchingDict = NULL;
io_service_t service = (io_service_t)NULL;
IOHIDDeviceInterface122** hidInterface = NULL;
kern_return_t result;
mach_port_t port;
uint32_t bytes;
ready = 0;
DAMMIT(matchingDict, _getMatchingDictionary(0x0100),
"InternalError: Could not create io service matching dictionary v1\n");
DAMMIT(service, _getIOService(matchingDict),
"IOError: Could not find THUM\n");
DAMMIT(hidInterface, _getHIDInterface(service),
"IOError: Could find the HID interface of the THUM\n");
KDAMMIT((*hidInterface)->open(hidInterface, 0),
"IOError: Could open the HID interface of the THUM (%08x)\n", result);
KDAMMIT((*hidInterface)->createAsyncPort(hidInterface, &port),
"IOError: could not create async port (%08x)\n", result);
KDAMMIT((*hidInterface)->createAsyncEventSource(hidInterface, &rl),
"IOError: could not create run loop (%08x)\n", result);
KDAMMIT((*hidInterface)->setInterruptReportHandlerCallback(
hidInterface,
PACKET,
2,
reportcallback,
NULL, NULL),
"IOError: could not set report handler (%08x)\n", result);
KDAMMIT((*hidInterface)->startAllQueues(hidInterface),
"IOError: could not start queue (%08x)\n", result);
CFRunLoopAddSource(CFRunLoopGetCurrent(), rl, kCFRunLoopDefaultMode);
KDAMMIT((*hidInterface)->setReport(
hidInterface,
kIOHIDReportTypeOutput,
0x00,
(char *)PACKET,
SEND_PACKET_LENGTH,
10000,
NULL, NULL, NULL),
"IOError: Could not setReport on device (%08x)\n", result);
while (!ready) {
(void)CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, false);
}
#endif
/* temp tables
3.32 C = 0x115b = 4443
17.27 C = 0x16ce = 5838
25.40 C = 0x19fb = 6651
29.33 C = 0x1b84 = 7044
29.51 C = 0x1b96 = 7062
30.07 C = 0x1bce = 7118
this is a linear relationship.
linear regression fit: temp in C = (0.01*value) - 41.11
the data sheet suggests -40.1 for 5V, but this doesn't quite work.
*/
if (DEBUG)
fprintf(stderr, "\n\n%02x %02x\n\n", PACKET[0], PACKET[1]);
tempc = (0.01*((PACKET[0]*256)+PACKET[1]))-41.11;
/* now switch to humidity */
PACKET[0] = 0x01;
PACKET[1] = 0x00;
#if defined(__linux__)
ret = usb_control_msg( // Endpoint transfer type: Control (0)
hid->dev_handle,
0x21, // bmRequestType
0x09, // bRequest
0x0301, // wValue
0x0000, // wIndex
(char *)PACKET,
SEND_PACKET_LENGTH, // wLength
10000);
if (DEBUG) {
fprintf(stderr, "usb_control_transfer: %d\n", ret);
if (ret != 2)
return 1;
} else if (ret != 2) {
fprintf(stderr, "usb_control_transfer: %d\n", ret);
return 1;
}
ret = usb_interrupt_read( // Endpoint transfer type: Interrupt (3)
hid->dev_handle,
0x81, // Endpoint address
(char *)PACKET,
SEND_PACKET_LENGTH,
10000);
if (DEBUG) {
fprintf(stderr, "usb_interrupt_read: %d\n", ret);
if (ret != 2)
return 1;
} else if (ret != 2) {
fprintf(stderr, "usb_interrupt_read: %d\n", ret);
return 1;
}
#elif defined(__APPLE__)
ready = 0;
KDAMMIT((*hidInterface)->setReport(
hidInterface,
kIOHIDReportTypeOutput,
0x00,
(char *)PACKET,
SEND_PACKET_LENGTH,
10000,
NULL, NULL, NULL),
"IOError: Could not setReport on device (%d)\n", result);
while (!ready) {
(void)CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, false);
}
#endif
/* relative humidity% is based on temperature and sensor reading.
from the SHT7x datasheet (we are fetching two bytes, so must be 12-bit):
RHl = -2.0468 + (0.0367*sensor) + (-1.5955e-6 * (sensor*sensor))
temperature corrected,
RH = (T - 25) * (0.01 + (0.00008*sensor)) + RHl
>100% should be treated like 100% */
if (DEBUG)
fprintf(stderr, "\n\n%02x %02x\n\n", PACKET[0], PACKET[1]);
if (tempc > -40.0 && tempc < 124) { // datasheet
double sensor = (double)(PACKET[0]*256)+PACKET[1];
double rhl = -2.0468 + (0.0367*sensor) + (-1.5955e-6 * (sensor*sensor));
double rh = (tempc - 25) * (0.01 + (0.00008*sensor)) + rhl;
if (rh >= 0.0) {
if (rh > 100.0) rh = 100.0;
fprintf(stdout, "%0.3f C %0.3f%%\n",tempc, rh);
invalid = 0;
}
}
#if defined(__linux__)
ret = hid_close(hid);
if (ret != HID_RET_SUCCESS) {
fprintf(stderr, "hid_close failed, return code %d\n", ret);
return 1;
}
hid_delete_HIDInterface(&hid);
ret = hid_cleanup();
if (ret != HID_RET_SUCCESS) {
fprintf(stderr, "hid_cleanup failed, return code %d\n", ret);
return 1;
}
#elif defined(__APPLE__)
(*hidInterface)->close(hidInterface);
(*hidInterface)->Release(hidInterface);
IOObjectRelease(service);
#endif
if (invalid)
fprintf(stdout, "invalid reading, please retry\n");
return invalid;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment