20 April 2014

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.
 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

 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                          })
 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.


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
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:
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.

# 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,
                         'temperature_c': dev.get_temperature(),

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

# 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)]
TIMEOUT = 2000

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

    def get_temperature(self, format='celsius'):
            if not self._handle:
                self._handle = self._device.open()
                except usb.USBError:
                except usb.USBError:
                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._control_transfer(self._handle, "\x01\x82\x77\x01\x00\x00\x00\x00") # uIni1
            self._control_transfer(self._handle, "\x01\x86\xff\x01\x00\x00\x00\x00") # uIni2
            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)
     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)
     raise ValueError("Unknown format")
        except usb.USBError, e:
            if "not permitted" in str(e):
                raise Exception("Permission problem accessing USB. Maybe I need to run as root?")
    def getid(self):
        #print self._device.idProduct
        return self._device.idProduct

    def close(self):
        if self._handle:
            except ValueError:
            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

No comments:

Post a Comment