573. PCSensors K-type USB thermocouple adapter TEMPer1K4 (0c45:7403) on debian

UPDATE 1 May 2014:
I've rewritten the code now to properly deal with the presence of both a 0c45:7401 and a 0c45:7403 simultaneously. Note that I'm not convince about the accuracy of the temperature readings (accuracy as in whether there's any systematic bias to the output. The reproducibility seems to be excellent though).

Original post:
I just received this usb thermocouple reader (TEMPer1k4): http://www.pcsensor.com/index.php?_a=product&product_id=104

About the product:
lsusb shows 0c45:7403 (Microdia Foot Switch)

dmesg shows

[13448.536120] usb 6-1: new low-speed USB device number 3 using uhci_hcd
[13448.709126] usb 6-1: New USB device found, idVendor=0c45, idProduct=7403
[13448.709139] usb 6-1: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[13448.709146] usb 6-1: Product: TH1000isoV1.5
[13448.709152] usb 6-1: Manufacturer: RDing


Getting it to work:
You can use the same program as in this post: http://verahill.blogspot.com.au/2013/12/532-temper-temperature-monitoring-usb.html with some minor modifications. Before running
sudo python setup.py install

make some changes in temperusb/temper.py.

Firstly, you need to allow for the detection of devices with the 0c45:7403 ID
 
15 VIDPIDs = [(0x0c45L,0x7401L),(0x0c45L,0x7403L)]
Secondly, you need to make sure that the program knows which device is which and treats them differently. Finally, you need to collect the right data-- the TEMPer1k4 returns a data_s with a length of 8 (caveat -- I haven't checked what the output from 0c45:7401 looks like. Maybe it too yields 8 fields), and the [2:4] data block contains the internal temperature of the USB device, while the [4:6] data block holds the thermocouple junction temperature. Of a sort -- you get a number which you need to translate into a temperature. See the bottom of this post for how I worked it all out. Please note that you should calibrate the thermocouple -- the uncorrected output seems to be at least 2-3 degrees too high.
temper.py
 
15 VIDPIDs = [(0x0c45L,0x7401L),(0x0c45L,0x7403L)]

63 def getid(self):
64 return self._device.idProduct


125 if self._device.idProduct==29697:
126 temp_c = 125.0/32000.0*(struct.unpack('>h', data_s[2:4])[0])+0.0025
127 if format == 'celsius':
128 temp_c = temp_c * self._scale + self._offset
129 return temp_c
130 elif format == 'fahrenheit':
131 return temp_c*1.8+32.0
132 elif format == 'millicelsius':
133 return int(temp_c*1000)
134 else:
135 raise ValueError("Unknown format")
136 elif self._device.idProduct==29699:
137 temp_c = (struct.unpack('>h', data_s[4:6])[0])*0.25-2.00
138 temp_internal=(125.0/32000.0*(struct.unpack('>h', data_s[2:4])[0]))+0.0025
139 if format == 'celsius':
140 temp_c = temp_c * self._scale + self._offset
141 return temp_c
142 elif format == 'fahrenheit':
143 if self._device.idProduct==29697: #0x7401
144 return temp_c*1.8+32.0
145 elif self._device.idProduct==29699: #0x7403
146 return temp_internal

147

cli.py
 
31 for i, dev in enumerate(devs):
32 readings.append({'device': i,
33 'id':dev.getid(),
34 'temperature_c': dev.get_temperature(),
35 'temperature_f':
36 dev.get_temperature(format="fahrenheit"),
37 'ports': dev.get_ports(),
38 'bus': dev.get_bus()
39 })
40
41 for reading in readings:
42 if disp_ports:
43 portinfo = " (bus %s - port %s)" % (reading['bus'],
44 reading['ports'])
45 else:
46 portinfo = ""
47 if reading['id']==29697:
48 print('Device #%i%s: %0.1f°C %0.1f°F'
49 % (reading['device'],
50 portinfo,
51 reading['temperature_c'],
52 reading['temperature_f']))
53 elif reading['id']==29699:
54 print('Device #%i%s: %0.1f°C %0.1f°C'
55 % (reading['device'],
56 portinfo,
57 reading['temperature_c'],
58 reading['temperature_f']))


Beyond this, follow the instructions in http://verahill.blogspot.com.au/2013/12/532-temper-temperature-monitoring-usb.html and make sure to set up a rules file with the correct USB ID for this device.

Done!

If you're curious about the details, have a look below.

Analysing USB traffic:
Since I wasn't quite sure where to start I figured the easiest approach would be to sniff the traffic between the usb device and the offically supported programme from PC Sensors. So I installed it Win XP in virtualbox, read a couple of temperatures and compared them with the captured data.

To capture data I did:
sudo apt-get install tshark
sudo modprobe usbmon
lsusb


Bus 008 Device 003: ID 17ef:4815 Lenovo Integrated Webcam [R5U877]
Bus 008 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 006 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
Bus 005 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
Bus 004 Device 043: ID 0c45:7403 Microdia Foot Switch
Bus 004 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
Bus 004 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
Bus 007 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 003 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
Bus 002 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
Bus 001 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub

sudo tshark -D

1. eth0
2. wlan0
3. nflog
4. nfqueue
5. usbmon1
6. usbmon2
7. usbmon3
8. usbmon4
9. usbmon5
10. usbmon6
11. usbmon7
12. usbmon8
13. any
14. lo (Loopback)

touch 1.pcap
chmod ugo+w 1.pcap
sudo tshark -i usbmon4 -w $HOME/1.pcap


I opened the 1.pcap file in wireshark and looked for fields called leftover data.

Here are two examples of such 'leftover data' fields:
Host to USB: 01 80 00 00 00 00 00 00
USB to Host: 80 06 17 f0 00 5c 0f ff

Subjecting the system to different temperatures helped me figure out what fields where changing -- I've marked them in bold above (blue for the thermocouple, red for the internal temperature). Interestingly, I got overflow when freezing the thermocouple between two ice cubes, which indicated that there was a fairly high lower limit (1.5 degrees)
The following are some of the data I harvested. The temperature is in the first column, the hex code is in the second one and the decimal value is in the third one:
Internal:
23.63 17a 378
23.69 17b 379
23.94 17f 383
23.75 17c 380
21.88 15e 350
22.31 165 357
22.88 16e 366


Thermocouple:
23.75 05f 95
23.50 05e 9
38.25 099 153
24.00 066 102
87.50 15e 350
79.50 13e 318
In other words, T(thermo)=output(thermo)*0.25+1.50 and T(int.)=(output(int.)-0.0025)/0.0625. Because the internal data is in fields 2:4 it's seen as e.g. 17b0 instead of 17b, so the decimal version needs to be divided by 16 (line 126 above).

I also set up an /etc/temper.conf file and did chown $USER /etc/temper.conf in order to be able to read it (must be a better way). I tried to calibrate the thermocouple in my kitchen with iced water and boiling water. The boiling water gave a reading of 102 degrees, while the iced water gave me 7.5 degrees Celsius even after what should've been long enough to achieve equilibrium. I'll calibrate it in the lab later. So for now a simple offset might be enough to give a working temperature.
temper-poll -p

Device #0 (bus 2 - port 2)

so temper.conf became

2-2: scale = 1.00, offset = -4.0



The temper-usb code is changing too quickly!. You'll thus need to read and understand the code rather than just pasting it in. Here are the full, edited cli.py an temper.py files.

cli.py
# encoding: utf-8
from __future__ import print_function
from temper import TemperHandler


def main():
th = TemperHandler()
devs = th.get_devices()
readings = []
print("Found %i devices" % len(devs))

for i, dev in enumerate(devs):
readings.append({'device': i,
'id':dev.getid(),
'temperature_c': dev.get_temperature(),
'temperature_f':
dev.get_temperature(format="fahrenheit")
})

for reading in readings:
if reading['id']==29697:
print('Device #%i: %0.1f°C %0.1f°F' % (reading['device'],
reading['temperature_c'],
reading['temperature_f']))
elif reading['id']==29699:
print('Device #%i: %0.1f°C %0.1f°C' % (reading['device'],
reading['temperature_c'],
reading['temperature_f']))

temper.py
# encoding: utf-8
#
# Handles devices reporting themselves as USB VID/PID 0C45:7401 (mine also says RDing TEMPerV1.2).
#
# Copyright 2012, 2013 Philipp Adelt
#
# This code is licensed under the GNU public license (GPL). See LICENSE.md for details.

import usb
import sys
import struct

VIDPIDs = [(0x0c45L,0x7401L),(0x0c45L,0x7402L),(0x0c45L,0x7403L)]
REQ_INT_LEN = 8
REQ_BULK_LEN = 8
TIMEOUT = 2000

class TemperDevice():
def __init__(self, device):
self._device = device
self._handle = None

def get_temperature(self, format='celsius'):
try:
if not self._handle:
self._handle = self._device.open()
try:
self._handle.detachKernelDriver(0)
except usb.USBError:
pass
try:
self._handle.detachKernelDriver(1)
except usb.USBError:
pass
self._handle.setConfiguration(1)
self._handle.claimInterface(0)
self._handle.claimInterface(1)
self._handle.controlMsg(requestType=0x21, request=0x09, value=0x0201, index=0x00, buffer="\x01\x01", timeout=TIMEOUT) # ini_control_transfer

self._control_transfer(self._handle, "\x01\x80\x33\x01\x00\x00\x00\x00") # uTemperatura
self._interrupt_read(self._handle)
self._control_transfer(self._handle, "\x01\x82\x77\x01\x00\x00\x00\x00") # uIni1
self._interrupt_read(self._handle)
self._control_transfer(self._handle, "\x01\x86\xff\x01\x00\x00\x00\x00") # uIni2
self._interrupt_read(self._handle)
self._interrupt_read(self._handle)
self._control_transfer(self._handle, "\x01\x80\x33\x01\x00\x00\x00\x00") # uTemperatura
data = self._interrupt_read(self._handle)
data_s = "".join([chr(byte) for byte in data])

if self._device.idProduct==29697:
temp_c = 125.0/32000.0*(struct.unpack('>h', data_s[2:4])[0])+0.0025
if format == 'celsius':
return temp_c
elif format == 'fahrenheit':
return temp_c*1.8+32.0
elif format == 'millicelsius':
return int(temp_c*1000)
else:
raise ValueError("Unknown format")
elif self._device.idProduct==29699:
temp_c = (struct.unpack('>h', data_s[4:6])[0])*0.25-4.00
temp_internal=(125.0/32000.0*(struct.unpack('>h', data_s[2:4])[0]))+0.0025
if format == 'celsius':
return temp_c
elif format == 'fahrenheit':
if self._device.idProduct==29697: #0x7401
return temp_c*1.8+32.0
elif self._device.idProduct==29699: #0x7403
return temp_internal
elif format == 'millicelsius':
return int(temp_c*1000)
else:
raise ValueError("Unknown format")
except usb.USBError, e:
self.close()
if "not permitted" in str(e):
raise Exception("Permission problem accessing USB. Maybe I need to run as root?")
else:
raise

def getid(self):
#print self._device.idProduct
return self._device.idProduct

def close(self):
if self._handle:
try:
self._handle.releaseInterface()
except ValueError:
pass
self._handle = None

def _control_transfer(self, handle, data):
handle.controlMsg(requestType=0x21, request=0x09, value=0x0200, index=0x01, buffer=data, timeout=TIMEOUT)

def _interrupt_read(self, handle):
return handle.interruptRead(0x82, REQ_INT_LEN)


class TemperHandler():
def __init__(self):
busses = usb.busses()
self._devices = []
for bus in busses:
self._devices.extend([TemperDevice(x) for x in bus.devices if (x.idVendor,x.idProduct) in VIDPIDs])

def get_devices(self):
return self._devices

Previous
Next Post »