From patchwork Wed Nov 11 00:06:40 2009 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Scott Tsai X-Patchwork-Id: 38118 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits)) (Client did not present a certificate) by ozlabs.org (Postfix) with ESMTPS id E22C9B6F2B for ; Wed, 11 Nov 2009 11:42:55 +1100 (EST) Received: from localhost ([127.0.0.1]:40826 helo=lists.gnu.org) by lists.gnu.org with esmtp (Exim 4.43) id 1N81IN-0006IE-Rr for incoming@patchwork.ozlabs.org; Tue, 10 Nov 2009 19:42:47 -0500 Received: from mailman by lists.gnu.org with tmda-scanned (Exim 4.43) id 1N80mU-0007ZI-Fj for qemu-devel@nongnu.org; Tue, 10 Nov 2009 19:09:50 -0500 Received: from exim by lists.gnu.org with spam-scanned (Exim 4.43) id 1N80mP-0007Wb-VE for qemu-devel@nongnu.org; Tue, 10 Nov 2009 19:09:49 -0500 Received: from [199.232.76.173] (port=52809 helo=monty-python.gnu.org) by lists.gnu.org with esmtp (Exim 4.43) id 1N80mP-0007WW-OL for qemu-devel@nongnu.org; Tue, 10 Nov 2009 19:09:45 -0500 Received: from mail-ew0-f206.google.com ([209.85.219.206]:60204) by monty-python.gnu.org with esmtp (Exim 4.60) (envelope-from ) id 1N80mO-0007jF-BG for qemu-devel@nongnu.org; Tue, 10 Nov 2009 19:09:45 -0500 Received: by ewy2 with SMTP id 2so632802ewy.34 for ; Tue, 10 Nov 2009 16:09:43 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=gamma; h=domainkey-signature:received:received:content-type:cc:subject:from :to:in-reply-to:references:date:message-id:user-agent :content-transfer-encoding; bh=3/GOk8YtHSxLKl21lyqDWGMaAinVDH2hhg1K2wtix1M=; b=rcwCLS0q0qkiNNiSzawDUotfuU7PnWKqqpwYmzjrSujzBYtWO6nPQWuDGOKukfym7w 70ou8qnOFBzVCmKRes5o5X/Dg4MiTiEwL893jxPgYlYbfMauCnJJN7dAzUQVjs2Py1cO R4/yFzL8RHA2rixf59D/6AC9Uvc4hL4bfv6ws= DomainKey-Signature: a=rsa-sha1; c=nofws; d=gmail.com; s=gamma; h=content-type:cc:subject:from:to:in-reply-to:references:date :message-id:user-agent:content-transfer-encoding; b=davJuERBKNtaYhWbQCgk2cpU/XjnGMMWB1VJPLL4xNSSK15esx6oU72iYhECe9uO4s QidxJhvWFIMis/dSLL9Rf0TiVoxKcGbU0h7HWDLZXQWgdnNWxGBfx30SNN/nzsiAbhYu J1QhAkrDa8kbB/VwWo9bEDbdI4640dfKnTUoA= Received: by 10.216.90.7 with SMTP id d7mr276769wef.81.1257898183153; Tue, 10 Nov 2009 16:09:43 -0800 (PST) Received: from localhost (220-136-191-52.dynamic.hinet.net [220.136.191.52]) by mx.google.com with ESMTPS id g9sm3721209gvc.25.2009.11.10.16.09.34 (version=TLSv1/SSLv3 cipher=RC4-MD5); Tue, 10 Nov 2009 16:09:41 -0800 (PST) Subject: [Qemu-devel] [PATCH V3 2/3] usb-gotemp: reworked to add monitor commands From: Scott Tsai To: Luiz Capitulino In-reply-to: <20091110185218.40468bdf@doriath> References: <1257845850-4660-1-git-send-email-scottt.tw@gmail.com> <1257845850-4660-2-git-send-email-scottt.tw@gmail.com> <1257845850-4660-3-git-send-email-scottt.tw@gmail.com> <4AF97D3B.8080601@redhat.com> <374D220D-FE6E-410B-83F3-E8988BA61A6D@suse.de> <20091110150630.19b0c3fa@doriath> <20091110185218.40468bdf@doriath> Date: Wed, 11 Nov 2009 08:06:40 +0800 Message-Id: <1257896005-sup-4478@xpc65.scottt> User-Agent: Sup/0.9 X-detected-operating-system: by monty-python.gnu.org: GNU/Linux 2.6 (newer, 2) Cc: qemu-devel , Alexander Graf , Avi Kivity X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.5 Precedence: list List-Id: qemu-devel.nongnu.org List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Sender: qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org Errors-To: qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org I reworked the patch to add generic monitor commands to change the temperature reported from thermometers. Thermometer devices can now include "sendor.h" and call 'qemu_add_therm_temp_handler' to register themselves. I went with separate 'therm_set DEVICE_INDEX' and 'therm_temp TEMPERATURE' commands since a 'therm_temp' command that only requires one argument seems easier to use on the monitor command line and doesn't require searching the list of thermometers repeatedly. To cater to my original "driver tutorial" use case, by default the temperature would still automatically increment unless the 'controlled_by_monitor' qdev property is set. Even when auto incrementing the temperature is now always bounded between 25C ~ 40C. (previously the temperature would increment until int16_t overflows) # START OF PATCH 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 DEVICE_INDEX therm_temp TEMPERATURE modeled after 'info mice', 'mouse_set' and 'mouse_move'. Signed-off-by: Scott Tsai --- Makefile | 2 +- hw/usb-gotemp.c | 762 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ monitor.c | 94 +++++++ qemu-monitor.hx | 35 +++ sensor.h | 20 ++ 5 files changed, 912 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 + * + * 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 . + */ + +#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..983c212 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,92 @@ 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; + } + 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 +2296,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