Posts tonen met het label reverse engineering. Alle posts tonen
Posts tonen met het label reverse engineering. Alle posts tonen

donderdag 11 augustus 2016

Reverse engineering the SLAB HT2000 CO2, temperature and relative humidity sensor

TL;DR: I reverse engineered the SLAB HT2000 CO2, temperature and relative humidity (RH) data logger made by Dongguan Xintai Instrument Corporation. Sourcecode and binaries available at http://GitHub.com/tomvanbraeckel/slab_ht2000


I found a great CO2, temperature and relative humidity meter with USB connection. It's made by Dongguan Xintai Instrument Co. in China and branded under the name HT-2000. Available online for less than $100, it is quite cheap for this kind of device.



There's only one downside to a lot of cheap Chinese product and this one is no exception.. it came bundled with:
  • no Windows software or drivers, although I found one online
  • no manual, although I found one online
  • no Linux software or drivers
  • no protocol specification
  • no datasheet
So how does one read out the values or communicate with this thing through the USB port?

With the help of the Linux kernel, of course!

Connecting the device to a modern Linux (kernel version 4.7) and running lsusb yields in:

# lsusb
Bus 003 Device 011: ID 10c4:82cd Cygnal Integrated Products, Inc. # this is the one!

Cygnal makes USB-to-serial devices that are supported by the cp120x driver but this device actually reports itself to be a Human Input Device in its USB device descriptor:

# dmesg | tail
[407925.138165] usb 3-6: new full-speed USB device number 10 using xhci_hcd
[407947.073443] usb 3-6: new full-speed USB device number 11 using xhci_hcd
[407947.203629] usb 3-6: New USB device found, idVendor=10c4, idProduct=82cd
[407947.203632] usb 3-6: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[407947.203634] usb 3-6: Product: HT2000
[407947.203635] usb 3-6: Manufacturer: SLAB
[407947.209156] hid-generic 0003:10C4:82CD.0007: hiddev0,hidraw0: USB HID v1.01 Device [SLAB HT2000] on usb-0000:00:14.0-6/input0

And our Linux system creates a /dev/hidraw0 device as a virtual respresentation of the physical hardware device, the CO2 meter.

Manually reading out the device descriptor results in more confirmation that this is a raw HID device:

# lsusb -v -v -v

Bus 003 Device 003: ID 10c4:82cd Cygnal Integrated Products, Inc. 
Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               1.10
  bDeviceClass            0 (Defined at Interface level)
  bDeviceSubClass         0 
  bDeviceProtocol         0 
  bMaxPacketSize0        64
  idVendor           0x10c4 Cygnal Integrated Products, Inc.
  idProduct          0x82cd 
  bcdDevice            0.00
  iManufacturer           1 SLAB
  iProduct                2 HT2000
  iSerial                 0 
  bNumConfigurations      1
  Configuration Descriptor:
    bLength                 9
    bDescriptorType         2
    wTotalLength           41
    bNumInterfaces          1
    bConfigurationValue     1
    iConfiguration          0 
    bmAttributes         0x80
      (Bus Powered)
    MaxPower               64mA
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        0
      bAlternateSetting       0
      bNumEndpoints           2
      bInterfaceClass         3 Human Interface Device
      bInterfaceSubClass      0 No Subclass
      bInterfaceProtocol      0 None
      iInterface              0 
      Warning: Descriptor too short
        HID Device Descriptor:
          bLength                 9
          bDescriptorType        33
          bcdHID               1.01
          bCountryCode            0 Not supported
          bNumDescriptors         2
          bDescriptorType        34 Report
          wDescriptorLength     128
          bDescriptorType        31 (null)
          wDescriptorLength     157
         Report Descriptors: 
           ** UNAVAILABLE **

      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x81  EP 1 IN
        bmAttributes            3
          Transfer Type            Interrupt
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0040  1x 64 bytes
        bInterval              10

      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x01  EP 1 OUT
        bmAttributes            3
          Transfer Type            Interrupt
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0040  1x 64 bytes
        bInterval              10
Device Status:     0x0000
  (Bus Powered)

So we know it's a Human Input Device but we have no further info on how to get data from it.

No data is coming out when just reading from it while the device is making measurements:

sudo head -c 1   /dev/hidraw0 # Never finishes so not a single byte of data comes out here

Time to check out the Linux kernel samples of how to deal with hidraw devices. In the Linux kernel sources (version 4.7) there is an example samples/hidraw/hid-example.c which shows how to read a report from a hidraw device.

This example tool yields the following output:

# sudo ./hid-example /dev/hidraw0 

Report Descriptor Size: 128
Report Descriptor:
6 0 ff 9 1 a1 1 85 1 95 6 75 8 26 ff 0 15 0 9 1 91 2 85 2 95 3c 75 8 26 ff 0 15 0 9 1 91 2 85 3 95 1 75 8 26 ff 0 15 0 9 1 91 2 85 4 95 2 75 8 26 ff 0 15 0 9 1 91 2 85 5 95 1f 75 8 26 ff 0 15 0 9 1 81 2 85 6 95 3c 75 8 26 ff 0 15 0 9 1 81 2 85 7 95 3c 75 8 26 ff 0 15 0 9 1 81 2 85 8 95 3c 75 8 26 ff 0 15 0 9 1 81 2 c0 

Raw Name: SLAB HT2000
Raw Phys: usb-0000:00:14.0-6/input0
Raw Info:
bustype: 3 (USB)
vendor: 0x10c4
product: 0x82cd
ioctl HIDIOCGFEATURE returned: 4
ioctl HIDIOCGFEATURE returned: 61
Report data (not containing the report number):
8 9b a5 22 5 5 9b a3 22 5 5 9b a1 22 5 5 9b a1 22 5 5 9b a1 22 54 5 9b a2 22 54 5 9b a4 22 54 5 9b a5 22 54 5 9b a4 22 84 5 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 

write() wrote 2 bytes
read: Resource temporarily unavailable

The "Report data" looks interesting but we are looking for live measurements and these values don't seem to change when running the program multiple times.

So we need to dig deeper.

Now, looking at the source code of the hidraw example, I noticed that it is trying to read out report number 9. So I set out to read the other report numbers and seeing whether anything useful comes out.

Report numbers 1 to 4 all have the same content as the 9, except that the first byte is different and seems to contain the report number itself.

Report number 5 is interesting though. The report data seems to be live because it is changing slightly after every run:

5 77 0 c4 e1 0 36 2 8c 1 f7 1 90 3 20 0 64 3 b6 b0 3 1 a1 22 2 73 0 0 7 d0 5 9b
5 77 0 c4 c6 0 64 2 8c 1 f9 1 90 3 20 0 64 3 b6 b0 3 0 a1 22 2 75 0 0 7 d0 5 9b
5 77 0 c4 e6 0 64 2 8c 1 fa 1 90 3 20 0 64 3 b6 b0 3 0 a1 22 2 7c 0 0 7 d0 5 9b
5 77 0 c4 c7 0 64 2 8c 1 f9 1 90 3 20 0 64 3 b6 b0 3 0 a1 22 2 7f 0 0 7 d0 5 9b

AHA! That is live data we are seeing there!

Comparing these values to the actual measurements on the display allowed me to spit out the following partial reverse engineered specification for this report #5:

Output:

5 77 0 c5 f2 0 64 2 97 1 ee 1 90 3 20 0 64 3 b6 b0 3 0 ff ff 2 e8 0 0 7 d0 ff ff 
  DD D DD DD      T TT H HH                                  C CC

Where:

DD D DD DD = a timestamp, seconds since epoch + 2004450700 (magic number)
T TT = the temperature, multiplied by 10 and plus 400 (26.3 degrees Celcius in the example above)
H HH = the humidity, multiplied by 10 (49.4 % R.H. in the example above)
C CC = the CO2 concentration, 744 ppm in the example above

Writing a C program that reads out these values was trivial and published on GitHub.

Future work

Report #6

Report #6 toggles between 2 different outputs:

6 1 3 20 0 64 3 b6 0 57 22 b5 b6 1 57 22 b5 b6 0 0 0 0 7 d0 0 0 0 0 0 0 0 6 ff ff ff 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

and

6 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 fe 20 0 64 1 0 1 b0 1 90

Report #7

There seems to be a pattern here, similar to what report #6 outputs:

7 1 3 20 0 64 3 b6 0 57 22 b5 b6 1 57 22 b5 b6 0 64 0 0 7 d0 0 0 0 0 0 0 0 7 ff ff ff 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

More features

According to the specifications, this device also contains memory for at least 10000 measurements and can log in different modes (Immediately, Schedule, Real-time & Roll-over) but I could not figure out how to change the default mode without having the Windows PC software. This means the "REC" button does not do anything because the mode is set to "Immediately" instead of "Manual".

Also, recalling minimal and maximal values should be possible on the display but I have no idea how.

If you have any idea on how to use those extra features, leave a comment please. Thanks!

dinsdag 19 juli 2016

Linux shell script to stream audio to a Zavio IP Camera

While I was showing an apprentice how to reverse engineer closed HTTP interfaces, we figured out how the proprietary Zavio webbrowser plugin streams audio data to the Zavio F3510 IP Cameras and other models from around 2016.

Then I wrote a rudimentary bash shell script that enables you to play MP3, WAV, FLAC, MIDI or any other file formats that avconv can handle through the camera's poor little speaker.

Interestingly, you can also use this script to continuously stream live audio from a HTTP URL to the camera and that way you can enjoy music anywhere you've installed your camera's.

Currently, this script is compatible with bash on Linux but I don't see why it wouldn't work on bash for Windows or Mac OS if you're into that kind of thing. Just make sure you have the necessary dependencies (avconv, curl,...) in your search PATH.

Enjoy!

#!/bin/bash
# Simple shell script to stream audio to a Zavio F3510 or other Zavio camera's from around the year 2014, 2015 and 2016
# No configuration is required, just supply the parameters on the command line
# Supported audio formats: mp3, mp2, wav, ogg and many more, thanks to avconv and all related projects
# Uses curl, the Swiss Army Knife of networking
# Copyleft by Tom Van Braeckel, 19-07-2016 🔥

# How it works:
# =============
# The Zavio needs to receive pcm_mulaw encoded audio samples
# at a sample rate of 8000 samples/second (8kHz) mono at 16 bits per sample through a HTTP web interface.
# The data is sent through periodic HTTP requests in chunks of 1000 bytes audio data + HTTP protocol overhead.

# Dependencies:
# -------------
# - avconv
# - curl
# - base64
# - sleep (a modern one that accepts non-integer arguments, as in: sleep 0.06)
# - echo (a modern one that supports the -e "\r" construct)


# Theoretical note:
# -----------------
# The HTTP API that we call has no active two-way synchronisation protocol so there will always be timer drift between the rate at which we send samples and the rate at which they are consumed. When there is drift, the Zavio seems to insert a few dummy samples or drop samples to correct the drift. This could theoretically be heard as clicks or cracks when playing long audio clips but it seems to work fine for me.

# Calculation of the rate at which we need to send chunks of audio to the HTTP API:
# ---------------------------------------------------------------------------------
# 1000 bytes/chunk / 2 bytes/sample = 500 samples/chunk
# 8k samples/second / 500 samples/chunk = 16 chunks/second = 1/16 second/chunk = 0.0625 second/chunk

sleep_time_per_audio_chunk=0.06 # target sleep time (0.0625 seconds) minus around 4% = 0.0025s overhead (forking new sleep process)

ip="$1"
auth="$2"
file="$3"
boost="$4"
if [ -z "$ip" -o -z "$auth" -o -z "$file" ]; then
echo "Usage: $0 ip_address username:password filename/url [volumeboost in dB]"
echo
echo "Example: $0 192.168.4.20 tom:supercool world_domination.wav"
echo "Example: $0 192.168.1.203 admin:admin audiofile.mp3 10"
echo "Example: $0 192.168.1.203 admin:admin http://streamingserver.com/stream -5"
exit 1
fi

# Default to 0 boost
if [ -z "$boost" ]; then
boost=0
fi
echo "boost = $boost"


# Encode the username:password pair in base64 encoding
auth64=$(echo -n "$auth" | base64)

# Arbitrary string of text
string="--As from earth's Bos om, sprung to sight"

echo -n "Sending audio sample..."
# Send the encoded audio to stdout with avconv and amplify it a bit, read it in chunks of 1000 bytes, add the HTTP prefix and let curl do the HTTP 1.0 request
# We do this while curl is doing requests of size > 1000 bytes, until the grep no longer exits successfully, which ends the loop
avconv -i "$file" -f wav -acodec pcm_mulaw -ar 8000 -ac 1 -af "volume=$boost"dB - | while (echo -en "$string\r\nContent-Type: audio/wav\r\n" ; head -c 1000 ) | curl -v --http1.0 -H "Content-Type: multipart/form-data; boundary=$string" -H "Authorization: Basic $auth64" --data-binary @- http://$ip/cgi-bin/operator/transmit 2>&1 | grep -q "Content-Length: 1[0-9][0-9][0-9]"; do
sleep "$sleep_time_per_audio_chunk"
echo -n .
done
echo "done."