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