/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2016-2017 Richard Hughes <richard@hughsie.com>
 *
 * Licensed under the GNU Lesser General Public License Version 2.1
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
 */

#include "config.h"

#include <appstream-glib.h>
#include <fcntl.h>
#include <gudev/gudev.h>
#include <poll.h>
#include <string.h>
#include <termios.h>
#include <errno.h>

#include <libdfu/dfu.h>

#include "fu-device-altos.h"

typedef struct
{
	FuDeviceAltosKind	 kind;
	GUsbDevice		*usb_device;
	guint32			 serial[9];
	gchar			*guid;
	gchar			*tty;
	gchar			*version;
	guint64			 addr_base;
	guint64			 addr_bound;
	struct termios		 tty_termios;
	gint			 tty_fd;
} FuDeviceAltosPrivate;

G_DEFINE_TYPE_WITH_PRIVATE (FuDeviceAltos, fu_device_altos, FU_TYPE_DEVICE)

#define GET_PRIVATE(o) (fu_device_altos_get_instance_private (o))

#ifndef HAVE_GUDEV_232
G_DEFINE_AUTOPTR_CLEANUP_FUNC(GUdevClient, g_object_unref)
#endif

/**
 * fu_device_altos_kind_from_string:
 * @kind: the string.
 *
 * Converts the text representation to an enumerated value.
 *
 * Returns: (transfer full): a #FuDeviceAltosKind, or %FU_DEVICE_ALTOS_KIND_UNKNOWN for unknown.
 *
 * Since: 0.1.0
 **/
FuDeviceAltosKind
fu_device_altos_kind_from_string (const gchar *kind)
{
	if (g_strcmp0 (kind, "BOOTLOADER") == 0)
		return FU_DEVICE_ALTOS_KIND_BOOTLOADER;
	if (g_strcmp0 (kind, "CHAOSKEY") == 0)
		return FU_DEVICE_ALTOS_KIND_CHAOSKEY;
	return FU_DEVICE_ALTOS_KIND_UNKNOWN;
}

/**
 * fu_device_altos_kind_to_string:
 * @kind: the #FuDeviceAltosKind.
 *
 * Converts the enumerated value to an text representation.
 *
 * Returns: string version of @kind
 *
 * Since: 0.1.0
 **/
const gchar *
fu_device_altos_kind_to_string (FuDeviceAltosKind kind)
{
	if (kind == FU_DEVICE_ALTOS_KIND_BOOTLOADER)
		return "BOOTLOADER";
	if (kind == FU_DEVICE_ALTOS_KIND_CHAOSKEY)
		return "CHAOSKEY";
	return NULL;
}

static void
fu_device_altos_finalize (GObject *object)
{
	FuDeviceAltos *device = FU_DEVICE_ALTOS (object);
	FuDeviceAltosPrivate *priv = GET_PRIVATE (device);

	g_free (priv->guid);
	g_free (priv->tty);
	g_free (priv->version);
	if (priv->usb_device != NULL)
		g_object_unref (priv->usb_device);

	G_OBJECT_CLASS (fu_device_altos_parent_class)->finalize (object);
}

static void
fu_device_altos_init (FuDeviceAltos *device)
{
}

static void
fu_device_altos_class_init (FuDeviceAltosClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	object_class->finalize = fu_device_altos_finalize;
}

FuDeviceAltosKind
fu_device_altos_get_kind (FuDeviceAltos *device)
{
	FuDeviceAltosPrivate *priv = GET_PRIVATE (device);
	return priv->kind;
}

static gboolean
fu_device_altos_find_tty (FuDeviceAltos *device, GError **error)
{
	FuDeviceAltosPrivate *priv = GET_PRIVATE (device);
	g_autoptr(GList) devices = NULL;
	g_autoptr(GUdevClient) gudev_client = g_udev_client_new (NULL);

	/* find all tty devices */
	devices = g_udev_client_query_by_subsystem (gudev_client, "tty");
	for (GList *l = devices; l != NULL; l = l->next) {
		GUdevDevice *dev = G_UDEV_DEVICE (l->data);

		/* get the tty device */
		const gchar *dev_file = g_udev_device_get_device_file (dev);
		if (dev_file == NULL)
			continue;

		/* get grandparent */
		dev = g_udev_device_get_parent (dev);
		if (dev == NULL)
			continue;
		dev = g_udev_device_get_parent (dev);
		if (dev == NULL)
			continue;

		/* check correct device */
		if (g_udev_device_get_sysfs_attr_as_int (dev, "busnum") !=
		    g_usb_device_get_bus (priv->usb_device))
			continue;
		if (g_udev_device_get_sysfs_attr_as_int (dev, "devnum") !=
		    g_usb_device_get_address (priv->usb_device))
			continue;

		/* success */
		priv->tty = g_strdup (dev_file);
		return TRUE;
	}

	/* failure */
	g_set_error (error,
		     FWUPD_ERROR,
		     FWUPD_ERROR_NOT_SUPPORTED,
		     "failed to find tty for %u:%u",
		     g_usb_device_get_bus (priv->usb_device),
		     g_usb_device_get_address (priv->usb_device));
	return FALSE;
}

static gboolean
fu_device_altos_tty_write (FuDeviceAltos *device,
			   const gchar *data,
			   gssize data_len,
			   GError **error)
{
	FuDeviceAltosPrivate *priv = GET_PRIVATE (device);
	gint rc;
	gssize idx = 0;
	guint timeout_ms = 500;
	struct pollfd fds;

	/* lets assume this is text */
	if (data_len < 0)
		data_len = strlen (data);

	fds.fd = priv->tty_fd;
	fds.events = POLLOUT;

	g_debug ("write, with timeout %ums", timeout_ms);
	while (idx < data_len) {

		/* wait for data to be allowed to write without blocking */
		rc = poll (&fds, 1, (gint) timeout_ms);
		if (rc == 0)
			break;
		if (rc < 0) {
			g_set_error (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_READ,
				     "failed to poll %i",
				     priv->tty_fd);
			return FALSE;
		}

		/* we can write data */
		if (fds.revents & POLLOUT) {
			gssize len;
			g_debug ("writing %" G_GSSIZE_FORMAT " bytes: %s", data_len, data);
			len = write (priv->tty_fd, data + idx, data_len - idx);
			if (len < 0) {
				if (errno == EAGAIN) {
					g_debug ("got EAGAIN, trying harder");
					continue;
				}
				g_set_error (error,
					     FWUPD_ERROR,
					     FWUPD_ERROR_WRITE,
					     "failed to write %" G_GSSIZE_FORMAT
					     " bytes to %i: %s" ,
					     data_len,
					     priv->tty_fd,
					     strerror (errno));
				return FALSE;
			}
			g_debug ("wrote %" G_GSSIZE_FORMAT " bytes", len);
			idx += len;
		}
	}

	return TRUE;
}

static GString *
fu_device_altos_tty_read (FuDeviceAltos *device,
			  guint timeout_ms,
			  gssize max_size,
			  GError **error)
{
	FuDeviceAltosPrivate *priv = GET_PRIVATE (device);
	gint rc;
	struct pollfd fds;
	g_autoptr(GString) str = g_string_new (NULL);

	fds.fd = priv->tty_fd;
	fds.events = POLLIN;

	g_debug ("read, with timeout %ums", timeout_ms);
	for (;;) {
		/* wait for data to appear */
		rc = poll (&fds, 1, (gint) timeout_ms);
		if (rc == 0)
			break;
		if (rc < 0) {
			g_set_error (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_READ,
				     "failed to poll %i",
				     priv->tty_fd);
			return NULL;
		}

		/* we have data to read */
		if (fds.revents & POLLIN) {
			guint8 buf[1024];
			gssize len = read (priv->tty_fd, buf, sizeof (buf));
			if (len < 0) {
				if (errno == EAGAIN) {
					g_debug ("got EAGAIN, trying harder");
					continue;
				}
				g_set_error (error,
					     FWUPD_ERROR,
					     FWUPD_ERROR_READ,
					     "failed to read %i: %s",
					     priv->tty_fd,
					     strerror (errno));
				return NULL;
			}
			if (len > 0) {
				g_debug ("read %" G_GSSIZE_FORMAT " bytes from device", len);
				g_string_append_len (str, (gchar *) buf, len);
			}

			/* check maximum size */
			if (max_size > 0 && str->len >= (guint) max_size)
				break;
			continue;
		}
		if (fds.revents & POLLERR) {
			g_set_error_literal (error,
					     FWUPD_ERROR,
					     FWUPD_ERROR_READ,
					     "error condition");
			return NULL;
		}
		if (fds.revents & POLLHUP) {
			g_set_error_literal (error,
					     FWUPD_ERROR,
					     FWUPD_ERROR_READ,
					     "connection hung up");
			return NULL;
		}
		if (fds.revents & POLLNVAL) {
			g_set_error_literal (error,
					     FWUPD_ERROR,
					     FWUPD_ERROR_READ,
					     "invalid request");
			return NULL;
		}
	}

	/* no data */
	if (str->len == 0) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_READ,
			     "no data received from device in %ums",
			     timeout_ms);
		return NULL;
	}

	/* return blob */
	return g_steal_pointer (&str);
}

static gboolean
fu_device_altos_tty_open (FuDeviceAltos *device, GError **error)
{
	FuDeviceAltosPrivate *priv = GET_PRIVATE (device);
	struct termios termios;
	g_autoptr(GString) str = NULL;

	/* open device */
	priv->tty_fd = open (priv->tty, O_RDWR | O_NONBLOCK);
	if (priv->tty_fd < 0) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INVALID_FILE,
			     "failed to open %s",
			     priv->tty);
		return FALSE;
	}

	/* get the old termios settings so we can restore later */
	if (tcgetattr (priv->tty_fd, &termios) < 0) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_INTERNAL,
				     "failed to get attributes from fd");
		return FALSE;
	}
	priv->tty_termios = termios;
	cfmakeraw (&termios);

	/* set speed */
	if (cfsetspeed (&termios, B9600) < 0) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_INTERNAL,
				     "failed to set terminal speed");
		return FALSE;
	}

	/* one input byte is enough to return
	 * inter-character timer off */
	termios.c_cc[VMIN]  = 1;
	termios.c_cc[VTIME] = 0;

	/* set all new data */
	if (tcsetattr (priv->tty_fd, TCSAFLUSH, &termios) < 0) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_INTERNAL,
				     "failed to set attributes on fd");
		return FALSE;
	}

	/* dump any pending input */
	str = fu_device_altos_tty_read (device, 50, -1, NULL);
	if (str != NULL)
		g_debug ("dumping pending buffer: %s", str->str);

	return TRUE;
}

static gboolean
fu_device_altos_tty_close (FuDeviceAltos *device, GError **error)
{
	FuDeviceAltosPrivate *priv = GET_PRIVATE (device);

	tcsetattr (priv->tty_fd, TCSAFLUSH, &priv->tty_termios);
	close (priv->tty_fd);

	return TRUE;
}

static GString *
fu_device_altos_read_page (FuDeviceAltos *device, guint address, GError **error)
{
	g_autoptr(GString) str = NULL;
	g_autofree gchar *cmd = g_strdup_printf ("R %x\n", address);
	if (!fu_device_altos_tty_write (device, cmd, -1, error))
		return NULL;
	str = fu_device_altos_tty_read (device, 1500, 256, error);
	if (str == NULL)
		return NULL;
	return g_steal_pointer (&str);
}

static gboolean
fu_device_altos_write_page (FuDeviceAltos *device,
			    guint address,
			    const guint8 *data,
			    guint data_len,
			    GError **error)
{
	g_autofree gchar *cmd = g_strdup_printf ("W %x\n", address);
	if (!fu_device_altos_tty_write (device, cmd, -1, error))
		return FALSE;
	if (!fu_device_altos_tty_write (device, (const gchar *) data, data_len, error))
		return FALSE;
	return TRUE;
}

static gboolean
fu_device_check_firmware (FuDeviceAltos *device, DfuFirmware *firmware, GError **error)
{
	FuDeviceAltosPrivate *priv = GET_PRIVATE (device);
	DfuElement *dfu_element;
	DfuImage *dfu_image;

	/* get default image */
	dfu_image = dfu_firmware_get_image_default (firmware);
	if (dfu_image == NULL) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_INVALID_FILE,
				     "no firmware image");
		return FALSE;
	}

	/* get default element */
	dfu_element = dfu_image_get_element_default (dfu_image);
	if (dfu_element == NULL) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_INVALID_FILE,
				     "no firmware element");
		return FALSE;
	}

	/* check the start address */
	if (dfu_element_get_address (dfu_element) != priv->addr_base) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INVALID_FILE,
			     "start address not correct %" G_GUINT32_FORMAT ":"
			     "%" G_GUINT64_FORMAT,
			     dfu_element_get_address (dfu_element),
			     priv->addr_base);
		return FALSE;
	}

	/* success */
	return TRUE;
}

gboolean
fu_device_altos_write_firmware (FuDeviceAltos *device,
				GBytes *fw,
				FuDeviceAltosWriteFirmwareFlag flags,
				GFileProgressCallback progress_cb,
				gpointer progress_data,
				GError **error)
{
	FuDeviceAltosPrivate *priv = GET_PRIVATE (device);
	const gchar *data;
	const gsize data_len;
	guint flash_len;
	g_autoptr(DfuFirmware) firmware = NULL;
	g_autoptr(GBytes) fw_blob = NULL;
	g_autoptr(GString) buf = g_string_new (NULL);

	/* check kind */
	if (priv->kind != FU_DEVICE_ALTOS_KIND_BOOTLOADER) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "verification only supported in bootloader");
		return FALSE;
	}

	/* check sizes */
	if (priv->addr_base == 0x0 || priv->addr_bound == 0x0) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "address base and bound are unset");
		return FALSE;
	}

	/* read in blocks of 256 bytes */
	flash_len = priv->addr_bound - priv->addr_base;
	if (flash_len == 0x0 || flash_len > 0x100000) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "address range was icorrect");
		return FALSE;
	}

	/* load ihex blob */
	firmware = dfu_firmware_new ();
	if (!dfu_firmware_parse_data (firmware, fw,
				      DFU_FIRMWARE_PARSE_FLAG_NONE,
				      error)) {
		return FALSE;
	}
	if (!fu_device_check_firmware (device, firmware, error))
		return FALSE;

	/* convert from ihex to a blob */
	dfu_firmware_set_format (firmware, DFU_FIRMWARE_FORMAT_RAW);
	fw_blob = dfu_firmware_write_data (firmware, error);
	if (fw_blob == NULL)
		return FALSE;

	/* check firmware will fit */
	data = g_bytes_get_data (fw_blob, (gsize *) &data_len);
	if (data_len > flash_len) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INVALID_FILE,
			     "firmware too large for device %" G_GSIZE_FORMAT ":%u",
			     data_len, flash_len);
		return FALSE;
	}

	/* open tty for download */
	if (!fu_device_altos_tty_open (device, error))
		return FALSE;
	for (guint i = 0; i < flash_len; i+= 0x100) {
		g_autoptr(GString) str = NULL;
		guint8 buf_tmp[0x100];

		/* copy remaining data into buf if required */
		memset (buf_tmp, 0xff, sizeof (buf));
		if (i < data_len) {
			gsize chunk_len = 0x100;
			if (i + 0x100 > data_len)
				chunk_len = data_len - i;
			memcpy (buf_tmp, data + i, chunk_len);
		}

		/* verify data from device */
		if (!fu_device_altos_write_page (device,
						 priv->addr_base + i,
						 buf_tmp,
						 0x100,
						 error))
			return FALSE;

		/* verify data written on device */
		str = fu_device_altos_read_page (device,
						 priv->addr_base + i,
						 error);
		if (str == NULL)
			return FALSE;
		if (str->len != 0x100) {
			g_set_error (error,
					     FWUPD_ERROR,
					     FWUPD_ERROR_WRITE,
					     "failed to verify @%x, "
					     "not enough data returned",
					     (guint) (priv->addr_base + i));
			return FALSE;
		}
		if (memcmp (str->str, buf_tmp, 0x100) != 0) {
			g_set_error (error,
					     FWUPD_ERROR,
					     FWUPD_ERROR_WRITE,
					     "failed to verify @%x",
					     (guint) (priv->addr_base + i));
			return FALSE;
		}

		/* progress */
		if (progress_cb != NULL) {
			progress_cb ((goffset) i,
				     (goffset) flash_len,
				     progress_data);
		}
		g_string_append_len (buf, str->str, str->len);
	}

	/* go to application mode */
	if (flags & FU_DEVICE_ALTOS_WRITE_FIRMWARE_FLAG_REBOOT) {
		if (!fu_device_altos_tty_write (device, "a\n", -1, error))
			return FALSE;
	}

	/* progress complete */
	if (progress_cb != NULL) {
		progress_cb ((goffset) flash_len,
			     (goffset) flash_len,
			     progress_data);
	}

	/* done */
	if (!fu_device_altos_tty_close (device, error))
		return FALSE;

	/* success */
	return TRUE;
}

GBytes *
fu_device_altos_read_firmware (FuDeviceAltos *device,
			       GFileProgressCallback progress_cb,
			       gpointer progress_data,
			       GError **error)
{
	FuDeviceAltosPrivate *priv = GET_PRIVATE (device);
	guint flash_len;
	g_autoptr(GString) buf = g_string_new (NULL);

	/* check kind */
	if (priv->kind != FU_DEVICE_ALTOS_KIND_BOOTLOADER) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "verification only supported in bootloader");
		return NULL;
	}

	/* check sizes */
	if (priv->addr_base == 0x0 || priv->addr_bound == 0x0) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "address base and bound are unset");
		return NULL;
	}

	/* read in blocks of 256 bytes */
	flash_len = priv->addr_bound - priv->addr_base;
	if (flash_len == 0x0 || flash_len > 0x100000) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "address range was icorrect");
		return NULL;
	}

	/* open tty for download */
	if (!fu_device_altos_tty_open (device, error))
		return NULL;
	for (guint i = priv->addr_base; i < priv->addr_bound; i+= 0x100) {
		g_autoptr(GString) str = NULL;

		/* request data from device */
		str = fu_device_altos_read_page (device, i, error);
		if (str == NULL)
			return NULL;

		/* progress */
		if (progress_cb != NULL) {
			progress_cb ((goffset) (i - priv->addr_base),
				     (goffset) (priv->addr_bound - priv->addr_base),
				     progress_data);
		}
		g_string_append_len (buf, str->str, str->len);
	}

	/* done */
	if (!fu_device_altos_tty_close (device, error))
		return NULL;

	/* success */
	return g_bytes_new (buf->str, buf->len);
}

static gboolean
fu_device_altos_probe_bootloader (FuDeviceAltos *device, GError **error)
{
	FuDeviceAltosPrivate *priv = GET_PRIVATE (device);
	g_auto(GStrv) lines = NULL;
	g_autoptr(GString) str = NULL;

	/* get tty for upload */
	if (!fu_device_altos_find_tty (device, error))
		return FALSE;
	if (!fu_device_altos_tty_open (device, error))
		return FALSE;

	/* get the version information */
	if (!fu_device_altos_tty_write (device, "v\n", -1, error))
		return FALSE;
	str = fu_device_altos_tty_read (device, 100, -1, error);
	if (str == NULL) {
		fu_device_altos_tty_close (device, NULL);
		return FALSE;
	}

	/* parse each line */
	lines = g_strsplit_set (str->str, "\n\r", -1);
	for (guint i = 0; lines[i] != NULL; i++) {

		/* ignore */
		if (lines[i][0] == '\0')
			continue;
		if (g_str_has_prefix (lines[i], "manufacturer     "))
			continue;
		if (g_str_has_prefix (lines[i], "product          "))
			continue;

		/* we can flash firmware */
		if (g_strcmp0 (lines[i], "altos-loader") == 0) {
			fu_device_remove_flag (FU_DEVICE (device),
					       FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER);
			continue;
		}

		/* version number */
		if (g_str_has_prefix (lines[i], "software-version ")) {
			fu_device_set_version (FU_DEVICE (device), lines[i] + 17);
			continue;
		}

		/* address base and bound */
		if (g_str_has_prefix (lines[i], "flash-range      ")) {
			g_auto(GStrv) addrs = g_strsplit (lines[i] + 17, " ", -1);
			priv->addr_base = g_ascii_strtoull (addrs[0], NULL, 16);
			priv->addr_bound = g_ascii_strtoull (addrs[1], NULL, 16);
			g_debug ("base: %x, bound: %x",
				 (guint) priv->addr_base,
				 (guint) priv->addr_bound);
			continue;
		}

		/* unknown line */
		g_warning ("unknown data: '%s'", lines[i]);
	}

	/* done */
	if (!fu_device_altos_tty_close (device, error))
		return FALSE;

	return TRUE;
}

gboolean
fu_device_altos_probe (FuDeviceAltos *device, GError **error)
{
	FuDeviceAltosPrivate *priv = GET_PRIVATE (device);

	/* bootloader uses tty */
	if (priv->kind == FU_DEVICE_ALTOS_KIND_BOOTLOADER)
		return fu_device_altos_probe_bootloader (device, error);

	/* get version */
	if (priv->kind == FU_DEVICE_ALTOS_KIND_CHAOSKEY) {
		const gchar *version_prefix = "ChaosKey-hw-1.0-sw-";
		guint8 version_idx;
		g_autofree gchar *version = NULL;

		/* open */
		if (!g_usb_device_open (priv->usb_device, error))
			return FALSE;

		/* get string */
		version_idx = g_usb_device_get_product_index (priv->usb_device);
		version = g_usb_device_get_string_descriptor (priv->usb_device,
							      version_idx,
							      error);
		if (version == NULL) {
			g_usb_device_close (priv->usb_device, NULL);
			return FALSE;
		}
		if (!g_str_has_prefix (version, version_prefix)) {
			g_set_error (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "not a ChaosKey v1.0 device: %s",
				     version);
			g_usb_device_close (priv->usb_device, NULL);
			return FALSE;
		}
		fu_device_set_version (FU_DEVICE (device), version + 19);
		if (!g_usb_device_close (priv->usb_device, error))
			return FALSE;
	}

	/* success */
	return TRUE;
}

/* now with kind and usb_device set */
static void
fu_device_altos_init_real (FuDeviceAltos *device)
{
	FuDeviceAltosPrivate *priv = GET_PRIVATE (device);
	g_autofree gchar *devid1 = NULL;

	/* allowed, but requires manual bootloader step */
	fu_device_add_flag (FU_DEVICE (device),
			    FWUPD_DEVICE_FLAG_ALLOW_ONLINE);

	/* set default vendor */
	fu_device_set_vendor (FU_DEVICE (device), "altusmetrum.org");

	/* set name */
	switch (priv->kind) {
	case FU_DEVICE_ALTOS_KIND_BOOTLOADER:
		fu_device_set_name (FU_DEVICE (device), "Altos [bootloader]");
		break;
	case FU_DEVICE_ALTOS_KIND_CHAOSKEY:
		fu_device_set_name (FU_DEVICE (device), "Altos ChaosKey");
		break;
	default:
		g_assert_not_reached ();
		break;
	}

	/* add USB\VID_0000&PID_0000 */
	devid1 = g_strdup_printf ("USB\\VID_%04X&PID_%04X",
				  g_usb_device_get_vid (priv->usb_device),
				  g_usb_device_get_pid (priv->usb_device));
	fu_device_add_guid (FU_DEVICE (device), devid1);
	g_debug ("saving runtime GUID of %s", devid1);

	/* only the bootloader can do the update */
	if (priv->kind != FU_DEVICE_ALTOS_KIND_BOOTLOADER) {
		fu_device_add_flag (FU_DEVICE (device),
				    FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER);
	}
}

typedef struct {
	guint16			 vid;
	guint16			 pid;
	FuDeviceAltosKind	 kind;
} FuDeviceAltosVidPid;

FuDeviceAltos *
fu_device_altos_new (GUsbDevice *usb_device)
{
	FuDeviceAltos *device;
	FuDeviceAltosPrivate *priv;
	const FuDeviceAltosVidPid vidpids[] = {
		{ 0xfffe, 0x000a, FU_DEVICE_ALTOS_KIND_BOOTLOADER },
		{ 0x1d50, 0x60c6, FU_DEVICE_ALTOS_KIND_CHAOSKEY },
		{ 0x0000, 0x0000, FU_DEVICE_ALTOS_KIND_UNKNOWN }
	};

	/* set kind */
	for (guint j = 0; vidpids[j].vid != 0x0000; j++) {
		if (g_usb_device_get_vid (usb_device) != vidpids[j].vid)
			continue;
		if (g_usb_device_get_pid (usb_device) != vidpids[j].pid)
			continue;
		device = g_object_new (FU_TYPE_DEVICE_ALTOS, NULL);
		priv = GET_PRIVATE (device);
		priv->kind = vidpids[j].kind;
		priv->usb_device = g_object_ref (usb_device);
		fu_device_altos_init_real (device);
		return device;
	}
	return NULL;
}
