Created
December 20, 2021 05:49
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
(C)2020-1 Cameron Kaiser, [email protected] | |
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