diff mbox

[V3,2/3] usb-gotemp: new module emulating a USB thermometer

Message ID 1257899043-28415-3-git-send-email-scottt.tw@gmail.com
State New
Headers show

Commit Message

Scott Tsai Nov. 11, 2009, 12:24 a.m. UTC
Emulate the Vernier Go!Temp USB thermometer
(see: http://www.vernier.com/go/gotemp.html)
used in Greg Kroah-Hartman's "Write a Real, Working, Linux Driver" talk.

The emulation is complete enough for gregkh's sample driver and
using the vendor supplied SDK through the in-kernel 'ldusb' module under Linux.
No testing have yet been done with the vendor's fancier Windows software.

If the qdev property 'controlled_by_monitor' is _NOT_ set on the thermometer,
such as through the command line option '-usbdevice thermometer:controlled_by_monitor',
the temperature would increment on each report but is bound between 25C
~ 40C.

Added new monitor commands:
	info thermometers
	therm_set INDEX
	therm_temp TEMPERATURE
modeled after 'info mice', 'mouse_set' and 'mouse_move'.

Signed-off-by: Scott Tsai <scottt.tw@gmail.com>
---
 Makefile        |    2 +-
 hw/usb-gotemp.c |  762 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 monitor.c       |   98 +++++++
 qemu-monitor.hx |   35 +++
 sensor.h        |   20 ++
 5 files changed, 916 insertions(+), 1 deletions(-)
 create mode 100644 hw/usb-gotemp.c
 create mode 100644 sensor.h
diff mbox

Patch

diff --git a/Makefile b/Makefile
index 30f1c9d..54b8968 100644
--- a/Makefile
+++ b/Makefile
@@ -124,7 +124,7 @@  obj-y += i2c.o smbus.o smbus_eeprom.o
 obj-y += eeprom93xx.o
 obj-y += scsi-disk.o cdrom.o
 obj-y += scsi-generic.o scsi-bus.o
-obj-y += usb.o usb-hub.o usb-$(HOST_USB).o usb-hid.o usb-msd.o usb-wacom.o
+obj-y += usb.o usb-hub.o usb-$(HOST_USB).o usb-hid.o usb-msd.o usb-wacom.o usb-gotemp.o
 obj-y += usb-serial.o usb-net.o usb-bus.o
 obj-$(CONFIG_SSI) += ssi.o
 obj-$(CONFIG_SSI_SD) += ssi-sd.o
diff --git a/hw/usb-gotemp.c b/hw/usb-gotemp.c
new file mode 100644
index 0000000..f52a529
--- /dev/null
+++ b/hw/usb-gotemp.c
@@ -0,0 +1,762 @@ 
+/*
+ * Vernier Go!Temp USB thermometer emulation
+ * see: http://www.vernier.com/go/gotemp.html
+ *
+ * Copyright (c) 2009 Scott Tsai <scottt.tw@gmail.com>
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "hw.h"
+#include "usb.h"
+#include "sensor.h"
+
+//#define DEBUG_GOTEMP
+
+#ifdef DEBUG_GOTEMP
+#define DPRINTF(fmt, ...) \
+    do { fprintf(stderr, "usb-gotemp: " fmt , ## __VA_ARGS__); } while (0)
+static void DHEXDUMP(uint8_t *buf, int len)
+{
+    int i;
+    if (!buf || !len) {
+        fprintf(stderr, "(null)\n");
+        return;
+    }
+    for (i = 0; i < len - 1; i++)
+        fprintf(stderr, "0x%02x ", buf[i]);
+    fprintf(stderr, "0x%02x\n", buf[i]);
+}
+#else
+#define DPRINTF(fmt, ...) do {} while(0)
+static void DHEXDUMP(uint8_t *buf, int len) { }
+#endif
+
+/*
+ * This device has three logical packet streams:
+ * 1. Commands in HID SET_REPORT requests to endpoint 0
+ * 2. Command responses in USB interrupt transfers from endpoint 1
+ * 3. Measurements in USB interrupt transfers also from endpoint 1
+ *
+ * All command, response and measurement packets are 8 bytes long.
+ */
+
+#define PACKET_SIZE 8
+#define QUEUE_SIZE 4  /* arbitrary */
+
+typedef struct {
+    uint8_t buf[QUEUE_SIZE][PACKET_SIZE];
+    int wp, rp;
+} Queue;
+
+static int queue_empty(Queue *q)
+{
+    return q->wp == q->rp;
+}
+
+static void queue_put(Queue *q, uint8_t *pkt)
+{
+    int next = (q->wp + 1) % QUEUE_SIZE;
+    if (next == q->rp)
+        return;
+    q->wp = next;
+    memcpy(q->buf[next], pkt, PACKET_SIZE);
+}
+
+static void queue_get(Queue *q, uint8_t *pkt)
+{
+    q->rp = (q->rp + 1) % QUEUE_SIZE;
+    memcpy(pkt, q->buf[q->rp], PACKET_SIZE);
+}
+
+#define LED_COLOR_RED          0x40
+#define LED_COLOR_GREEN        0x80
+#define LED_COLOR_RED_GREEN    0x00
+#define LED_BRIGHTNESS_MIN     0x00
+#define LED_BRIGHTNESS_MAX     0x10
+#define LED_BRIGHTNESS_DEFAULT 0x04
+
+/* Vernier product code names:
+ * Go!Link is also known as Skip.
+ * Go!Temp is also known as Jonah and is the device emulated here.
+ * Go!Motion is also known as Cyclops
+ */
+
+#define MEASUREMENT_TICK_IN_SECONDS 0.000128
+#define MEASUREMENT_PERIOD_DEFAULT_JONAH 0x0f82 /* unit: 0.000128 seconds, about 0.5 seconds */
+
+#define TEMPERATURE_AUTO_INCREMENT_MIN (celsius_to_internal_temperature_unit(25))
+#define TEMPERATURE_AUTO_INCREMENT_MAX (celsius_to_internal_temperature_unit(40))
+
+typedef struct {
+    USBDevice dev;
+    ThermTempEntry *list_entry;     /* node in qemu_therm_temp_head list */
+    int status;                     /* as reported by CMD_ID_GET_STATUS */
+    int measuring;                  /* whether measurement packets should be sent */
+    uint32_t measurement_period;    /* unit: 0.000128 seconds */
+    int64_t last_measure_time;      /* unit: milliseconds in qemu_get_clock(rt_clock)  */
+    Queue response_queue;           /* queue of response packets */
+    uint8_t rolling_counter;
+    int16_t temperature;            /* unit: 1/128 Celsius */
+    uint8_t temperature_controlled_by_monitor; /* if true, 'temperature' is set by monitor command
+                                                  if false, 'temperature' is incremented on every report */
+    uint8_t red_led_brightness;
+    uint8_t green_led_brightness;
+} GoTempState;
+
+#define MANUFACTURER_STRING       "Vernier Software & Technology"
+#define MANUFACTURER_STRING_INDEX 1
+#define PRODUCT_STRING            "Go! Temp ver 1.53"
+#define PRODUCT_STRING_INDEX      2
+
+/* MASTER_CPU_VERSION: reported in USB device descriptor and the CMD_ID_GET_STATUS command */
+#define MASTER_CPU_VERSION_MAJOR  0x01
+#define MASTER_CPU_VERSION_MINOR  0x53
+
+static const uint8_t gotemp_dev_descriptor[] = {
+    0x12,       /*  u8 bLength; */
+    0x01,       /*  u8 bDescriptorType; Device */
+    0x10, 0x01, /*  u16 bcdUSB; v1.1 */
+
+    0x00,       /*  u8  bDeviceClass; */
+    0x00,       /*  u8  bDeviceSubClass; */
+    0x00,       /*  u8  bDeviceProtocol; [ low/full speeds only ] */
+    0x08,       /*  u8  bMaxPacketSize0; 8 Bytes */
+
+    0xf7, 0x08, /*  u16 idVendor; */
+    0x02, 0x00, /*  u16 idProduct; */
+    MASTER_CPU_VERSION_MINOR, MASTER_CPU_VERSION_MAJOR, /*  u16 bcdDevice, "ver 1.53", also included in product string */
+
+    MANUFACTURER_STRING_INDEX, /*  u8  iManufacturer; */
+    PRODUCT_STRING_INDEX,      /*  u8  iProduct; */
+    0x00,       /*  u8  iSerialNumber; */
+    0x01        /*  u8  bNumConfigurations; */
+};
+
+static const uint8_t gotemp_config_descriptor[] = {
+    /* one configuration */
+    0x09,       /*  u8  bLength; */
+    0x02,       /*  u8  bDescriptorType; Configuration */
+    0x22, 0x00, /*  u16 wTotalLength; */
+    0x01,       /*  u8  bNumInterfaces; (1) */
+    0x01,       /*  u8  bConfigurationValue; */
+    0x00,       /*  u8  iConfiguration; */
+    0x80,       /*  u8  bmAttributes;
+                    Bit 7: must be set,
+6: Self-powered,
+5: Remote wakeup,
+4..0: resvd */
+    100/2,      /*  u8  MaxPower; 100mA */
+
+    /* one interface */
+    0x09,       /*  u8  if_bLength; */
+    0x04,       /*  u8  if_bDescriptorType; Interface */
+    0x00,       /*  u8  if_bInterfaceNumber; */
+    0x00,       /*  u8  if_bAlternateSetting; */
+    0x01,       /*  u8  if_bNumEndpoints; */
+    0x03,       /*  u8  if_bInterfaceClass; HID */
+    0x00,       /*  u8  if_bInterfaceSubClass; */
+    0x00,       /*  u8  if_bInterfaceProtocol; */
+    0x00,       /*  u8  if_iInterface; */
+
+    /* HID descriptor */
+    0x09,       /*  u8  bLength; */
+    0x21,       /*  u8  bDescriptorType; HID */
+    0x10, 0x01, /*  u16 bcdHID; HCD specification release number */
+    0x00,       /*  u8  bCountryCode; */
+    0x01,       /*  u8  bNumDescriptors; */
+    0x22,       /*  u8  bDescriptorType; report descriptor */
+    0x32, 0x00, /*  u16 wDescriptorLength; length of report descriptor above */
+
+    /* one endpoint (status change endpoint) */
+    0x07,       /*  u8  ep_bLength; */
+    0x05,       /*  u8  ep_bDescriptorType; Endpoint */
+    0x81,       /*  u8  ep_bEndpointAddress; IN Endpoint 1 */
+    0x03,       /*  u8  ep_bmAttributes; Interrupt */
+    0x08, 0x00, /*  u16 ep_wMaxPacketSize; */
+    0x0a        /*  u8  ep_bInterval; 10 milliseconds (low-speed) */
+};
+
+static const uint8_t gotemp_hid_report_descriptor[] = {
+    0x06, 0x00, 0xff, /* Usage Page (Vendor Defined) */
+    0x09, 0x01,       /* Usage (Vendor Defined) */
+    0xa1, 0x01,       /* Collection (Application) */
+    0x05, 0x01,       /*   Usage Page (Generic Desktop) */
+    0x09, 0x46,       /*   Usage (Vector) */
+    0x15, 0x80,       /*   Logical Minimum */
+    0x25, 0x7f,       /*   Logical Maximum */
+    0x95, 0x08,       /*   Report Count */
+    0x75, 0x08,       /*   Report Size */
+    0x81, 0x06,       /*   Input (Data, Variable, Relative) */
+    0x05, 0x01,       /*   Usage Page (Generic Desktop) */
+    0x09, 0x46,       /*   Usage (Vector) */
+    0x15, 0x80,       /*   Logical Minimum */
+    0x25, 0x7f,       /*   Logical Maximum */
+    0x95, 0x08,       /*   Report Count */
+    0x75, 0x08,       /*   Report Size */
+    0xb1, 0x06,       /*   Feature (Data, Variable, Relative) */
+    0x05, 0x08,       /*   Usage Page (LEDs) */
+    0x09, 0x2d,       /*   Usage (Ready) */
+    0x15, 0x80,       /*   Logical Minimum */
+    0x25, 0x7f,       /*   Logical Maximum */
+    0x95, 0x08,       /*   Report Count */
+    0x75, 0x08,       /*   Report Size */
+    0x91, 0x06,       /*   Output (Data, Variable, Relative) */
+    0xc0              /* End Collection */
+};
+
+/* gotemp_nv_mem: writable on real hardware */
+static const uint8_t gotemp_nv_mem[] = {
+    0x01, 0x3c, 0x21, 0x00, 0x00, 0x07,
+    0x51, 0x05, 0x54, 0x65, 0x6d, 0x70, 0x65,
+    0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x54, 0x65, 0x6d, 0x70, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x34, 0x0f, 0x01, 0x00, 0x00, 0x00, 0x3f,
+    0x00, 0x00, 0x80, 0x3f, 0xb4, 0x00, 0x00,
+    0x00, 0x01, 0x0e, 0x01, 0x00, 0x00, 0xc8,
+    0xc1, 0x00, 0x00, 0xfa, 0x42, 0x04, 0x02,
+    0x00, 0x20, 0xd0, 0x7f, 0xc3, 0xcd, 0xcc,
+    0xcc, 0x42, 0x00, 0x00, 0x00, 0x00, 0x28,
+    0x43, 0x29, 0x00, 0x00, 0x00, 0x00, 0x50,
+    0x3b, 0xd6, 0xc3, 0xec, 0x51, 0x38, 0x43,
+    0x00, 0x00, 0x00, 0x00, 0x28, 0x46, 0x29,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x89,
+    0x41, 0xcd, 0xcc, 0xcc, 0x42, 0x00, 0x00,
+    0x00, 0x00, 0x28, 0x4b, 0x29, 0x00, 0x00,
+    0x00, 0x00, 0x94,
+};
+
+#define MANUFACTURE_DATE_WEEK_IN_YEAR_IN_BCD        0xff;
+#define MANUFACTURE_DATE_YEAR_LAST_TWO_DIGITS_IN_BCD 0x00;
+#define SERIAL_NUMBER                                 0x0dca1000
+
+#define CMD_ID_GET_STATUS               0x10
+#define CMD_ID_READ_LOCAL_NV_MEM        0x17
+#define CMD_ID_START_MEASUREMENTS       0x18
+#define CMD_ID_STOP_MEASUREMENTS        0x19
+#define CMD_ID_INIT                     0x1a
+#define CMD_ID_SET_MEASUREMENT_PERIOD   0x1b
+#define CMD_ID_GET_MEASUREMENT_PERIOD   0x1c
+#define CMD_ID_SET_LED_STATE            0x1d
+#define CMD_ID_GET_LED_STATE            0x1e
+#define CMD_ID_GET_SERIAL_NUMBER        0x20
+#define CMD_ID_READ_REMOTE_NV_MEM       0x27
+
+#define RESPONSE_HEADER_NV_MEM_READ             0x49
+#define RESPONSE_HEADER_CMD_SUCCESS             0x5a
+#define RESPONSE_HEADER_GET_LED_STATUS          0x5b
+#define RESPONSE_HEADER_GET_STATUS_JONAH        0x5c
+#define RESPONSE_HEADER_GET_MREASUREMENT_PERIOD 0x5d
+#define RESPONSE_HEADER_GET_SERIAL_NUMBER       0x5f
+#define RESPONSE_HEADER_CMD_ERROR               (RESPONSE_HEADER_CMD_SUCCESS | 0x20)
+#define RESPONSE_HEADER_INIT_SUCCESS            0x9a
+
+#define RESPONSE_STATUS_SUCCESS                                             0x00
+#define RESPONSE_STATUS_NOT_READY_FOR_NEW_CMD                               0x30
+#define RESPONSE_STATUS_CMD_NOT_SUPPORTED                                   0x31
+#define RESPONSE_STATUS_INTERNAL_ERROR1                                     0x32
+#define RESPONSE_STATUS_INTERNAL_ERROR2                                     0x33
+#define RESPONSE_STATUS_ERROR_CANNOT_CHANGE_PERIOD_WHILE_COLLECTING         0x34
+#define RESPONSE_STATUS_ERROR_CANNOT_READ_NV_MEM_BLK_WHILE_COLLECTING_FAST  0x35
+#define RESPONSE_STATUS_ERROR_INVALID_PARAMETER                             0x36
+#define RESPONSE_STATUS_ERROR_CANNOT_WRITE_FLASH_WHILE_COLLECTING           0x37
+#define RESPONSE_STATUS_ERROR_CANNOT_WRITE_FLASH_WHILE_HOST_FIFO_BUSY       0x38
+#define RESPONSE_STATUS_ERROR_OP_BLOCKED_WHILE_COLLECTING                   0x39
+#define RESPONSE_STATUS_ERROR_CALCULATOR_CANNOT_MEASURE_WITH_NO_BATTERIES   0x3A
+#define RESPONSE_STATUS_ERROR_SLAVE_POWERUP_INIT                            0x40
+#define RESPONSE_STATUS_ERROR_SLAVE_POWERRESTORE_INIT                       0x41
+
+static void gotemp_get_status(GoTempState *s, uint8_t *pkt)
+{
+    pkt[0] = RESPONSE_HEADER_GET_STATUS_JONAH;
+    pkt[1] = CMD_ID_GET_STATUS;
+    pkt[2] = s->status;
+    pkt[3] = MASTER_CPU_VERSION_MINOR;
+    pkt[4] = MASTER_CPU_VERSION_MAJOR;
+}
+
+static int16_t celsius_to_internal_temperature_unit(int v)
+{
+    return v * 128;
+}
+
+static void gotemp_fill_success_response(GoTempState *s, uint8_t cmd, uint8_t *pkt)
+{
+    /* Response format for most commands:
+     * pkt[0]: header
+     * pkt[1]: cmd
+     * pkt[2]: status
+     */
+
+    memset(pkt, 0, PACKET_SIZE);
+    if (cmd == CMD_ID_INIT)
+        pkt[0] = RESPONSE_HEADER_INIT_SUCCESS;
+    else
+        pkt[0] = RESPONSE_HEADER_CMD_SUCCESS;
+    pkt[1] = cmd;
+    pkt[2] = RESPONSE_STATUS_SUCCESS;
+}
+
+static void gotemp_queue_response(GoTempState *s, uint8_t *pkt)
+{
+    queue_put(&s->response_queue, pkt);
+}
+
+static int gotemp_respond(GoTempState *s, uint8_t *buf, int len)
+{
+    /* All Go!Temp response packets are 8 bytes */
+    uint8_t pkt[PACKET_SIZE];
+    int l;
+    queue_get(&s->response_queue, pkt);
+
+    l = len < PACKET_SIZE ? len : PACKET_SIZE;
+    if (pkt[0] == RESPONSE_HEADER_NV_MEM_READ) {
+        uint8_t cmd = pkt[1], addr = pkt[2], len = pkt[3], offset = pkt[4];
+        int t = len - offset;
+        if (offset == 0) {
+            if (t > 6) {
+                t = 6;
+                buf[0] = 0x49 + 0x06;
+            } else {
+                buf[0] = 0x49 + t + 0x10; /* first packet is also the last packet in NVRAM read */
+            }
+            buf[1] = cmd;
+            memcpy(buf + 2, gotemp_nv_mem + addr + offset, t);
+        } else {
+            if (t > 7) {
+                t = 7;
+                buf[0] = 0x40 + 0x07;
+            } else {
+                buf[0] = 0x40 + t + 0x10; /* last packet in NVRAM read */
+            }
+            memcpy(buf + 1, gotemp_nv_mem + addr + offset, t);
+        }
+        if (!(buf[0] & 0x10)) { /* not last packet, queue next transfer */
+            pkt[4] += t;
+            gotemp_queue_response(s, pkt);
+        }
+    } else {
+        memcpy(buf, pkt, l);
+    }
+    return l;
+}
+
+static void gotemp_read_nv_mem(GoTempState *s, uint8_t gotemp_cmd, uint8_t addr, uint8_t len, uint8_t *pkt)
+{
+    /* Need to send 'len' bytes in 6 (first packet) or 7 byte chunks.
+     * The responses to CMD_ID_*_NVRAM_READ are special cased in gotemp_respond and we're just filling
+     * an internal book keeping record here, not the real packet that gets sent over the wire.
+     * */
+    pkt[0] = RESPONSE_HEADER_NV_MEM_READ;
+    pkt[1] = gotemp_cmd;
+    pkt[2] = addr; /* requestes address */
+    pkt[3] = len;  /* requested length */
+    pkt[4] = 0x00; /* current transfer offset from address */
+}
+
+static void gotemp_set_led(GoTempState *s, uint8_t color, uint8_t brightness)
+{
+    if (brightness > LED_BRIGHTNESS_MAX)
+        brightness = LED_BRIGHTNESS_MAX;
+
+    switch (color) {
+        case LED_COLOR_RED:
+            s->red_led_brightness = brightness;
+            break;
+        case LED_COLOR_GREEN:
+            s->green_led_brightness = brightness;
+            break;
+        case LED_COLOR_RED_GREEN:
+            s->red_led_brightness = brightness;
+            s->green_led_brightness = brightness;
+            break;
+    }
+}
+
+static void gotemp_get_led(GoTempState *s, uint8_t *pkt)
+{
+    pkt[0] = RESPONSE_HEADER_GET_LED_STATUS;
+    pkt[1] = CMD_ID_GET_LED_STATE;
+    if (s->red_led_brightness && s->green_led_brightness) {
+        pkt[2] = LED_COLOR_RED_GREEN;
+        pkt[3] = s->red_led_brightness;
+    } else if (s->red_led_brightness) {
+        pkt[2] = LED_COLOR_RED;
+        pkt[3] = s->red_led_brightness;
+    } else if (s->green_led_brightness) {
+        pkt[2] = LED_COLOR_GREEN;
+        pkt[3] = s->green_led_brightness;
+    } else {
+        pkt[2] = 0x00;
+        pkt[3] = 0x00;
+    }
+}
+
+static uint32_t le32_unpack(uint8_t *buf)
+{
+    return (buf[3] << 24) | (buf[2] << 16) | (buf[1] << 8) | buf[0];
+}
+
+static void le32_pack(uint8_t *buf, uint32_t v)
+{
+    buf[0] = v & 0xff;
+    buf[1] = (v >> 8) & 0xff;
+    buf[2] = (v >> 16) & 0xff;
+    buf[3] = (v >> 24) & 0xff;
+}
+
+static void gotemp_get_serial(GoTempState *s, uint8_t *pkt)
+{
+    pkt[0] = RESPONSE_HEADER_GET_SERIAL_NUMBER;
+    pkt[1] = CMD_ID_GET_SERIAL_NUMBER;
+    pkt[2] = MANUFACTURE_DATE_WEEK_IN_YEAR_IN_BCD;
+    pkt[3] = MANUFACTURE_DATE_YEAR_LAST_TWO_DIGITS_IN_BCD;
+    le32_pack(pkt + 4, SERIAL_NUMBER);
+}
+
+static int64_t measurement_ticks_to_ms(uint32_t ticks)
+{
+    return ticks * MEASUREMENT_TICK_IN_SECONDS * 1000;
+}
+
+static void gotemp_get_measurement_period(GoTempState *s, uint8_t *pkt)
+{
+    pkt[0] = RESPONSE_HEADER_GET_MREASUREMENT_PERIOD;
+    pkt[1] = CMD_ID_GET_MEASUREMENT_PERIOD;
+    le32_pack(pkt + 2, s->measurement_period);
+}
+
+static void gotemp_reset(GoTempState *s)
+{
+    s->measuring = 1; /* device is measuring upon reset, CMD_ID_INIT stops the measuring */
+    s->measurement_period = MEASUREMENT_PERIOD_DEFAULT_JONAH;
+    s->last_measure_time = qemu_get_clock(rt_clock);
+    gotemp_set_led(s, LED_COLOR_RED_GREEN, LED_BRIGHTNESS_DEFAULT);
+    s->response_queue.wp = s-> response_queue.rp = 0;
+    s->rolling_counter = 0;
+    s->status = RESPONSE_STATUS_SUCCESS;
+    s->temperature = TEMPERATURE_AUTO_INCREMENT_MIN;
+}
+
+static void gotemp_handle_reset(USBDevice *dev)
+{
+    GoTempState *s = (GoTempState*)dev;
+    DPRINTF("%s\n", __func__);
+    gotemp_reset(s);
+}
+
+static int gotemp_handle_hid_set_report(GoTempState *s, uint8_t *data)
+{
+    uint8_t pkt[PACKET_SIZE];
+    uint8_t gotemp_cmd = data[0];
+    switch (gotemp_cmd) {
+        case CMD_ID_GET_STATUS:
+            gotemp_get_status(s, pkt);
+            break;
+        case CMD_ID_INIT:
+            s->measuring = 0;
+            s->measurement_period = MEASUREMENT_PERIOD_DEFAULT_JONAH;
+            gotemp_set_led(s, LED_COLOR_RED_GREEN, LED_BRIGHTNESS_DEFAULT);
+            s->response_queue.wp = s->response_queue.rp = 0;
+            s->status = RESPONSE_STATUS_SUCCESS;
+            gotemp_fill_success_response(s, gotemp_cmd, pkt);
+            break;
+        case CMD_ID_START_MEASUREMENTS:
+            s->measuring = 1;
+            s->last_measure_time = qemu_get_clock(rt_clock);
+            gotemp_fill_success_response(s, gotemp_cmd, pkt);
+            break;
+        case CMD_ID_STOP_MEASUREMENTS:
+            s->measuring = 0;
+            gotemp_fill_success_response(s, gotemp_cmd, pkt);
+            break;
+        case CMD_ID_READ_LOCAL_NV_MEM:
+        case CMD_ID_READ_REMOTE_NV_MEM:
+            gotemp_read_nv_mem(s, gotemp_cmd, data[1], data[2], pkt);
+            break;
+        case CMD_ID_SET_MEASUREMENT_PERIOD:
+            s->measurement_period = le32_unpack(data + 1);
+            gotemp_fill_success_response(s, gotemp_cmd, pkt);
+            break;
+        case CMD_ID_GET_MEASUREMENT_PERIOD:
+            gotemp_get_measurement_period(s, pkt);
+            break;
+        case CMD_ID_SET_LED_STATE:
+            gotemp_set_led(s, data[1], data[2]);
+            gotemp_fill_success_response(s, gotemp_cmd, pkt);
+            break;
+        case CMD_ID_GET_LED_STATE:
+            gotemp_get_led(s, pkt);
+            break;
+        case CMD_ID_GET_SERIAL_NUMBER:
+            gotemp_get_serial(s, pkt);
+            break;
+        default:
+            DPRINTF("%s: unsupported gotemp command: 0x%02x\n", __func__, gotemp_cmd);
+            pkt[0] = RESPONSE_HEADER_CMD_ERROR;
+            pkt[1] = gotemp_cmd;
+            pkt[2] = RESPONSE_STATUS_CMD_NOT_SUPPORTED;
+            break;
+    }
+    gotemp_queue_response(s, pkt);
+    return 0;
+}
+
+static int gotemp_handle_control(USBDevice *dev, int request, int value,
+        int index, int length, uint8_t *data)
+{
+    GoTempState *s = (GoTempState*)dev;
+    int ret = 0;
+
+    DPRINTF("%s(request: 0x%04x, value: 0x%04x, index: 0x%04x)\n",  __func__, request, value, index);
+    DPRINTF("\tdata: ");
+    DHEXDUMP(data, length > 32 ? 32 : length);
+    switch (request) {
+        case DeviceRequest | USB_REQ_GET_STATUS:
+            data[0] = (0 << USB_DEVICE_SELF_POWERED) |
+                (dev->remote_wakeup << USB_DEVICE_REMOTE_WAKEUP);
+            data[1] = 0x00;
+            ret = 2;
+            break;
+        case DeviceOutRequest | USB_REQ_CLEAR_FEATURE:
+            if (value == USB_DEVICE_REMOTE_WAKEUP) {
+                dev->remote_wakeup = 0;
+            } else {
+                goto fail;
+            }
+            ret = 0;
+            break;
+        case DeviceOutRequest | USB_REQ_SET_FEATURE:
+            if (value == USB_DEVICE_REMOTE_WAKEUP) {
+                dev->remote_wakeup = 1;
+            } else {
+                goto fail;
+            }
+            ret = 0;
+            break;
+        case DeviceOutRequest | USB_REQ_SET_ADDRESS:
+            dev->addr = value;
+            ret = 0;
+            break;
+        case DeviceRequest | USB_REQ_GET_DESCRIPTOR:
+            switch(value >> 8) {
+                case USB_DT_DEVICE:
+                    memcpy(data, gotemp_dev_descriptor,
+                            sizeof(gotemp_dev_descriptor));
+                    ret = sizeof(gotemp_dev_descriptor);
+                    break;
+                case USB_DT_CONFIG:
+                    memcpy(data, gotemp_config_descriptor,
+                            sizeof(gotemp_config_descriptor));
+                    ret = sizeof(gotemp_config_descriptor);
+                    break;
+                case USB_DT_STRING:
+                    switch(value & 0xff) {
+                        case 0:
+                            /* language ids */
+                            data[0] = 4;
+                            data[1] = 3;
+                            data[2] = 0x09; /* little endian 0x0409: en_US */
+                            data[3] = 0x04;
+                            ret = 4;
+                            break;
+                        case MANUFACTURER_STRING_INDEX:
+                            ret = set_usb_string(data, MANUFACTURER_STRING);
+                            break;
+                        case PRODUCT_STRING_INDEX:
+                            ret = set_usb_string(data, PRODUCT_STRING);
+                            break;
+                        default:
+                            goto fail;
+                    }
+                    break;
+                default:
+                    goto fail;
+            }
+            break;
+        case DeviceRequest | USB_REQ_GET_CONFIGURATION:
+            data[0] = 1;
+            ret = 1;
+            break;
+        case DeviceOutRequest | USB_REQ_SET_CONFIGURATION:
+            ret = 0;
+            break;
+        case DeviceRequest | USB_REQ_GET_INTERFACE:
+            data[0] = 0;
+            ret = 1;
+            break;
+        case InterfaceOutRequest | USB_REQ_SET_INTERFACE:
+            ret = 0;
+            break;
+            /* HID specific requests */
+        case InterfaceRequest | USB_REQ_GET_DESCRIPTOR:
+            if (value >> 8 != 0x22)
+                goto fail;
+            memcpy(data, gotemp_hid_report_descriptor, sizeof(gotemp_hid_report_descriptor));
+            ret =  sizeof(gotemp_hid_report_descriptor);
+            break;
+        case InterfaceRequest | USB_REQ_SET_CONFIGURATION:
+            break;
+        case USB_REQ_HID_GET_REPORT:
+            /* FIXME: mandatory for HID devices, verify behavior on real hardware */
+            break;
+        case USB_REQ_HID_SET_REPORT:
+            if (length < PACKET_SIZE)
+                goto fail;
+            ret = gotemp_handle_hid_set_report(s, data);
+            break;
+        default:
+fail:
+            DPRINTF("%s: unsupported request: 0x%04x, value: 0x%04x, index: 0x%04x\n", __func__, request, value, index);
+            ret = USB_RET_STALL;
+            break;
+    }
+    return ret;
+}
+
+static int gotemp_poll(GoTempState *s, uint8_t *buf, int len)
+{
+    int l;
+    int64_t now;
+    if (!s->measuring)
+        return USB_RET_NAK;
+
+    now = qemu_get_clock(rt_clock);
+    if ((now - s->last_measure_time) < measurement_ticks_to_ms(s->measurement_period))
+        return USB_RET_NAK;
+
+    s->last_measure_time = now;
+    l = 0;
+    if (len > l)
+        buf[l++] = 1; /* measurements in packet */
+    if (len > l)
+        buf[l++] = s->rolling_counter++;
+    if (len > l)
+        buf[l++] = s->temperature & 0xff;
+    if (len > l)
+        buf[l++] = s->temperature >> 8;
+    if (len > l)
+        buf[l++] = 0x00;
+    if (len > l)
+        buf[l++] = 0x00;
+    if (len > l)
+        buf[l++] = 0x00;
+    if (len > l)
+        buf[l++] = 0x00;
+
+    if (!s->temperature_controlled_by_monitor) {
+        s->temperature++;
+        if (s->temperature > TEMPERATURE_AUTO_INCREMENT_MAX)
+            s->temperature = TEMPERATURE_AUTO_INCREMENT_MIN;
+    }
+    return l;
+}
+
+static int gotemp_handle_data(USBDevice *dev, USBPacket *p)
+{
+    GoTempState *s = (GoTempState *)dev;
+    int ret = 0;
+
+    //    DPRINTF("%s: p: {pid: 0x%02x, devep: %d}\n", __func__, p->pid, p->devep);
+    switch(p->pid) {
+        case USB_TOKEN_IN:
+            if (p->devep != 1)
+                goto fail;
+            if (!queue_empty(&s->response_queue))
+                ret = gotemp_respond(s, p->data, p->len);
+            else
+                ret = gotemp_poll(s, p->data, p->len);
+            break;
+        case USB_TOKEN_OUT:
+        default:
+fail:
+            ret = USB_RET_STALL;
+            break;
+    }
+    return ret;
+}
+
+static void gotemp_handle_therm_temp(void *opaque, double temperature_celsius)
+{
+    GoTempState *s = opaque;
+    DPRINTF("%s(%p, %lf)\n", __func__, opaque, temperature_celsius);
+    s->temperature_controlled_by_monitor = 1;
+    s->temperature = celsius_to_internal_temperature_unit(temperature_celsius);
+}
+
+static void gotemp_handle_destroy(USBDevice *dev)
+{
+    GoTempState *s = DO_UPCAST(GoTempState, dev, dev);
+    qemu_del_therm_temp_handler(s->list_entry);
+}
+
+static int gotemp_initfn(USBDevice *dev)
+{
+    DPRINTF("%s called\n", __func__);
+    GoTempState *s = DO_UPCAST(GoTempState, dev, dev);
+    s->dev.speed = USB_SPEED_LOW;
+    gotemp_reset(s);
+    s->list_entry = qemu_add_therm_temp_handler(gotemp_handle_therm_temp, s,
+    		"QEMU USB Thermometer");
+    return 0;
+}
+
+static USBDevice *gotemp_init(const char *params)
+{
+    USBDevice *dev;
+    uint8_t temperature_controlled_by_monitor;
+    DPRINTF("%s(\"%s\") called\n", __func__, params);
+
+    if (params) {
+        if (strcmp(params, "controlled_by_monitor") != 0) {
+            qemu_error("bad thermometer option: \"%s\"\n", params);
+            return NULL;
+        }
+        temperature_controlled_by_monitor = 1;
+    } else {
+        temperature_controlled_by_monitor = 0;
+    }
+    dev = usb_create(NULL /* FIXME */, "QEMU USB Thermometer");
+    qdev_prop_set_uint8(&dev->qdev, "temperature_controlled_by_monitor", temperature_controlled_by_monitor);
+    DPRINTF("%s: DO_UPCAST(GoTempState, dev, dev)->temperature_controlled_by_monitor: %d\n", 
+            __func__, DO_UPCAST(GoTempState, dev, dev)->temperature_controlled_by_monitor);
+    qdev_init(&dev->qdev);
+    return dev;
+}
+
+static struct USBDeviceInfo gotemp_info = {
+    .qdev.name      = "QEMU USB Thermometer",
+    .qdev.alias     = "usb-gotemp",
+    .qdev.size      = sizeof(GoTempState),
+    .init           = gotemp_initfn,
+    .handle_packet  = usb_generic_handle_packet,
+    .handle_reset   = gotemp_handle_reset,
+    .handle_control = gotemp_handle_control,
+    .handle_data    = gotemp_handle_data,
+    .handle_destroy = gotemp_handle_destroy,
+    .usbdevice_name = "thermometer",
+    .usbdevice_init = gotemp_init,
+    .qdev.props     = (Property[]) {
+        DEFINE_PROP_UINT8("temperature_controlled_by_monitor", GoTempState, temperature_controlled_by_monitor, 0),
+        DEFINE_PROP_END_OF_LIST(),
+    },
+};
+
+static void gotemp_register_devices(void)
+{
+    usb_qdev_register(&gotemp_info);
+}
+device_init(gotemp_register_devices)
diff --git a/monitor.c b/monitor.c
index 132fb6e..d3f7333 100644
--- a/monitor.c
+++ b/monitor.c
@@ -42,6 +42,7 @@ 
 #include "disas.h"
 #include "balloon.h"
 #include "qemu-timer.h"
+#include "sensor.h"
 #include "migration.h"
 #include "kvm.h"
 #include "acl.h"
@@ -1945,6 +1946,96 @@  int monitor_get_fd(Monitor *mon, const char *fdname)
     return -1;
 }
 
+static QCIRCLEQ_HEAD(therm_temp_head, ThermTempEntry) qemu_therm_temp_head =
+QCIRCLEQ_HEAD_INITIALIZER(qemu_therm_temp_head);
+static ThermTempEntry *qemu_therm_temp_current;
+
+ThermTempEntry *qemu_add_therm_temp_handler(ThermTempHandler *cb, void *opaque, const char *name)
+{
+    ThermTempEntry *e;
+
+    e = qemu_mallocz(sizeof (*e));
+
+    e->cb = cb;
+    e->opaque = opaque;
+    e->name = strdup(name);
+    if (QCIRCLEQ_EMPTY(&qemu_therm_temp_head))
+        qemu_therm_temp_current = e;
+    QCIRCLEQ_INSERT_TAIL(&qemu_therm_temp_head, e, entries);
+    return e;
+}
+
+void qemu_del_therm_temp_handler(ThermTempEntry *e)
+{
+    if (qemu_therm_temp_current == e)
+        qemu_therm_temp_current = QCIRCLEQ_PREV(e, entries);
+    QCIRCLEQ_REMOVE(&qemu_therm_temp_head, e, entries);
+    qemu_free(e);
+}
+
+static void do_info_thermometers(Monitor *mon)
+{
+    ThermTempEntry *e;
+    int i = 0;
+
+    if (QCIRCLEQ_EMPTY(&qemu_therm_temp_head)) {
+        monitor_printf(mon, "No thermometer connected\n");
+        return;
+    }
+
+    monitor_printf(mon, "Thermometers available:\n");
+    QCIRCLEQ_FOREACH(e, &qemu_therm_temp_head, entries) {
+        monitor_printf(mon, "%c Thermometer #%d: %s\n",
+                (e == qemu_therm_temp_current ? '*' : ' '),
+                i, e->name);
+        i++;
+    }
+}
+
+static void do_therm_set(Monitor *mon, const QDict *qdict, QObject **ret_data)
+{
+    int i = 0;
+    int index = qdict_get_int(qdict, "index");
+    ThermTempEntry *e;
+
+    QCIRCLEQ_FOREACH(e, &qemu_therm_temp_head, entries) {
+        if (i++ == index)
+            break;
+    }
+    if (i == index + 1)
+        qemu_therm_temp_current = e;
+    else
+        monitor_printf(mon, "Thermometer at given index not found\n");
+}
+
+static void do_therm_temp(Monitor *mon, const QDict *qdict, QObject **ret_data)
+{
+    double temp_celsius;
+    const char *t = qdict_get_str(qdict, "temperature_str");
+    int l = strlen(t);
+    if (!l)
+        return;
+
+    switch (t[l-1]) {
+        case 'c':
+        case 'C':
+            temp_celsius = strtod(t, NULL);
+            break;
+        case 'f':
+        case 'F':
+            temp_celsius = (5.0/9.0) * (strtod(t, NULL) - 32.0);
+            break;
+        default:
+            monitor_printf(mon, "temperature value must end with 'c', 'C', 'f' or 'F'\n");
+            return;
+    }
+    if (QCIRCLEQ_EMPTY(&qemu_therm_temp_head)) {
+        monitor_printf(mon, "No thermometer connected\n");
+        return;
+    }
+    qemu_therm_temp_current->cb(qemu_therm_temp_current->opaque, temp_celsius);
+}
+
 static const mon_cmd_t mon_cmds[] = {
 #include "qemu-monitor.h"
     { NULL, NULL, },
@@ -2209,6 +2300,13 @@  static const mon_cmd_t info_cmds[] = {
         .mhandler.info = do_info_roms,
     },
     {
+        .name       = "thermometers",
+        .args_type  = "",
+        .params     = "",
+        .help       = "show thermometers",
+        .mhandler.info = do_info_thermometers,
+    },
+    {
         .name       = NULL,
     },
 };
diff --git a/qemu-monitor.hx b/qemu-monitor.hx
index bb01c14..2f987f7 100644
--- a/qemu-monitor.hx
+++ b/qemu-monitor.hx
@@ -111,6 +111,8 @@  show migration status
 show balloon information
 @item info qtree
 show device tree
+@item info thermometers
+show available thermometers
 @end table
 ETEXI
 
@@ -1040,6 +1042,39 @@  Close the file descriptor previously assigned to @var{fdname} using the
 used by another monitor command.
 ETEXI
 
+    {
+        .name       = "therm_set",
+        .args_type  = "index:i",
+        .params     = "index",
+        .help       = "set which thermometer device is affected by therm_temp",
+        .mhandler.cmd_new = do_therm_set,
+    },
+
+STEXI
+@item therm_set @var{index}
+Set which thermometer device is affected by @code{therm_temp}, index
+can be obtained with
+@example
+info thermometer
+@end example
+ETEXI
+
+    {
+        .name       = "therm_temp",
+        .args_type  = "temperature_str:s",
+        .params     = "temperature",
+        .help       = "set thermometer temperature",
+        .mhandler.cmd_new = do_therm_temp,
+    },
+
+STEXI
+@item therm_temp @var{temperature}
+Set the temperature of the thermometer selected with @code{therm_set}.
+@example
+therm_temp 0F
+therm_temp -17.8c
+@end example
+ETEXI
 STEXI
 @end table
 ETEXI
diff --git a/sensor.h b/sensor.h
new file mode 100644
index 0000000..45024c2
--- /dev/null
+++ b/sensor.h
@@ -0,0 +1,20 @@ 
+#ifndef SENSOR_H
+#define SENSOR_H
+
+#include "qemu-queue.h"
+
+/* thermometers */
+typedef void (ThermTempHandler)(void *opaque, double temperature_celsius);
+
+struct ThermTempEntry {
+    ThermTempHandler *cb;
+    void *opaque;
+    char *name;
+    QCIRCLEQ_ENTRY(ThermTempEntry) entries;
+};
+typedef struct ThermTempEntry ThermTempEntry;
+
+ThermTempEntry *qemu_add_therm_temp_handler(ThermTempHandler *cb, void *opaque, const char *name);
+void qemu_del_therm_temp_handler(ThermTempEntry *e);
+
+#endif