/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
/* NetworkManager -- Network link manager
 *
 * 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, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Copyright (C) 2007 - 2008 Novell, Inc.
 * Copyright (C) 2007 - 2008 Red Hat, Inc.
 */

#include "config.h"
#include <termios.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <glib.h>

#include "nm-glib-compat.h"
#include "nm-serial-device.h"
#include "nm-device-interface.h"
#include "nm-device-private.h"
#include "ppp-manager/nm-ppp-manager.h"
#include "nm-setting-ppp.h"
#include "nm-marshal.h"
#include "nm-utils.h"
#include "nm-serial-device-glue.h"
#include "NetworkManagerUtils.h"

static gboolean serial_debug = FALSE;

#define SERIAL_BUF_SIZE 2048

G_DEFINE_TYPE (NMSerialDevice, nm_serial_device, NM_TYPE_DEVICE)

#define NM_SERIAL_DEVICE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), NM_TYPE_SERIAL_DEVICE, NMSerialDevicePrivate))

typedef struct {
	int fd;
	GIOChannel *channel;
	NMPPPManager *ppp_manager;
	NMIP4Config  *pending_ip4_config;
	struct termios old_t;

	guint pending_id;
	guint timeout_id;

	/* PPP stats */
	guint32 in_bytes;
	guint32 out_bytes;
} NMSerialDevicePrivate;

enum {
	PPP_STATS,

	LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

static int
parse_baudrate (guint i)
{
	int speed;

	switch (i) {
	case 0:
		speed = B0;
		break;
	case 50:
		speed = B50;
		break;
	case 75:
		speed = B75;
		break;
	case 110:
		speed = B110;
		break;
	case 150:
		speed = B150;
		break;
	case 300:
		speed = B300;
		break;
	case 600:
		speed = B600;
		break;
	case 1200:
		speed = B1200;
		break;
	case 2400:
		speed = B2400;
		break;
	case 4800:
		speed = B4800;
		break;
	case 9600:
		speed = B9600;
		break;
	case 19200:
		speed = B19200;
		break;
	case 38400:
		speed = B38400;
		break;
	case 57600:
		speed = B57600;
		break;
	case 115200:
		speed = B115200;
		break;
	case 460800:
		speed = B460800;
		break;
	default:
		g_warning ("Invalid baudrate '%d'", i);
		speed = B9600;
	}

	return speed;
}

static int
parse_bits (guint i)
{
	int bits;

	switch (i) {
	case 5:
		bits = CS5;
		break;
	case 6:
		bits = CS6;
		break;
	case 7:
		bits = CS7;
		break;
	case 8:
		bits = CS8;
		break;
	default:
		g_warning ("Invalid bits (%d). Valid values are 5, 6, 7, 8.", i);
		bits = CS8;
	}

	return bits;
}

static int
parse_parity (char c)
{
	int parity;

	switch (c) {
	case 'n':
	case 'N':
		parity = 0;
		break;
	case 'e':
	case 'E':
		parity = PARENB;
		break;
	case 'o':
	case 'O':
		parity = PARENB | PARODD;
		break;
	default:
		g_warning ("Invalid parity (%c). Valid values are n, e, o", c);
		parity = 0;
	}

	return parity;
}

static int
parse_stopbits (guint i)
{
	int stopbits;

	switch (i) {
	case 1:
		stopbits = 0;
		break;
	case 2:
		stopbits = CSTOPB;
		break;
	default:
		g_warning ("Invalid stop bits (%d). Valid values are 1 and 2)", i);
		stopbits = 0;
	}

	return stopbits;
}

static inline void
nm_serial_debug (const char *prefix, const char *data, int len)
{
	GString *str;
	int i;

	if (!serial_debug)
		return;

	str = g_string_sized_new (len);
	for (i = 0; i < len; i++) {
		if (data[i] == '\0')
			g_string_append_c (str, ' ');
		else if (data[i] == '\r')
			g_string_append_c (str, '\n');
		else
			g_string_append_c (str, data[i]);
	}

	nm_debug ("%s '%s'", prefix, str->str);
	g_string_free (str, TRUE);
}

static NMSetting *
serial_device_get_setting (NMSerialDevice *device, GType setting_type)
{
	NMActRequest *req;
	NMSetting *setting = NULL;

	req = nm_device_get_act_request (NM_DEVICE (device));
	if (req) {
		NMConnection *connection;

		connection = nm_act_request_get_connection (req);
		if (connection)
			setting = nm_connection_get_setting (connection, setting_type);
	}

	return setting;
}

/* Timeout handling */

static void
nm_serial_device_timeout_removed (gpointer data)
{
	NMSerialDevicePrivate *priv = NM_SERIAL_DEVICE_GET_PRIVATE (data);

	priv->timeout_id = 0;
}

static gboolean
nm_serial_device_timed_out (gpointer data)
{
	NMSerialDevicePrivate *priv = NM_SERIAL_DEVICE_GET_PRIVATE (data);

	/* Cancel data reading */
	if (priv->pending_id)
		g_source_remove (priv->pending_id);
	else
		nm_warning ("Timeout reached, but there's nothing to time out");

	return FALSE;
}

static void
nm_serial_device_add_timeout (NMSerialDevice *self, guint timeout)
{
	NMSerialDevicePrivate *priv = NM_SERIAL_DEVICE_GET_PRIVATE (self);

	if (priv->pending_id == 0)
		nm_warning ("Adding a time out while not waiting for any data");

	if (priv->timeout_id) {
		nm_warning ("Trying to add a new time out while the old one still exists");
		g_source_remove (priv->timeout_id);
	}

	priv->timeout_id = g_timeout_add_seconds_full (G_PRIORITY_DEFAULT,
	                                               timeout,
	                                               nm_serial_device_timed_out,
	                                               self,
	                                               nm_serial_device_timeout_removed);
	if (G_UNLIKELY (priv->timeout_id == 0))
		nm_warning ("Registering serial device time out failed.");
}

static void
nm_serial_device_remove_timeout (NMSerialDevice *self)
{
	NMSerialDevicePrivate *priv = NM_SERIAL_DEVICE_GET_PRIVATE (self);

	if (priv->timeout_id)
		g_source_remove (priv->timeout_id);
}

/* Pending data reading */

static guint
nm_serial_device_set_pending (NMSerialDevice *device,
						guint timeout,
						GIOFunc callback,
						gpointer user_data,
						GDestroyNotify notify)
{
	NMSerialDevicePrivate *priv = NM_SERIAL_DEVICE_GET_PRIVATE (device);

	if (G_UNLIKELY (priv->pending_id)) {
		/* FIXME: Probably should queue up pending calls instead? */
		/* Multiple pending calls on the same GIOChannel doesn't work, so let's cancel the previous one. */
		nm_warning ("Adding new pending call while previous one isn't finished.");
		nm_warning ("Cancelling the previous pending call.");
		g_source_remove (priv->pending_id);
	}

	priv->pending_id = g_io_add_watch_full (priv->channel,
									G_PRIORITY_DEFAULT,
									G_IO_IN | G_IO_ERR | G_IO_HUP,
									callback, user_data, notify);

	nm_serial_device_add_timeout (device, timeout);

	return priv->pending_id;
}

static void
nm_serial_device_pending_done (NMSerialDevice *self)
{
	NM_SERIAL_DEVICE_GET_PRIVATE (self)->pending_id = 0;
	nm_serial_device_remove_timeout (self);
}

/****/

static gboolean
config_fd (NMSerialDevice *device, NMSettingSerial *setting)
{
	NMSerialDevicePrivate *priv = NM_SERIAL_DEVICE_GET_PRIVATE (device);
	struct termios stbuf;
	int speed;
	int bits;
	int parity;
	int stopbits;

	speed = parse_baudrate (nm_setting_serial_get_baud (setting));
	bits = parse_bits (nm_setting_serial_get_bits (setting));
	parity = parse_parity (nm_setting_serial_get_parity (setting));
	stopbits = parse_stopbits (nm_setting_serial_get_stopbits (setting));

	tcgetattr (priv->fd, &stbuf);

	stbuf.c_iflag &= ~(IGNCR | ICRNL | IUCLC | INPCK | IXON | IXANY | IGNPAR );
	stbuf.c_oflag &= ~(OPOST | OLCUC | OCRNL | ONLCR | ONLRET);
	stbuf.c_lflag &= ~(ICANON | XCASE | ECHO | ECHOE | ECHONL);
	stbuf.c_lflag &= ~(ECHO | ECHOE);
	stbuf.c_cc[VMIN] = 1;
	stbuf.c_cc[VTIME] = 0;
	stbuf.c_cc[VEOF] = 1;

	stbuf.c_cflag &= ~(CBAUD | CSIZE | CSTOPB | CLOCAL | PARENB);
	stbuf.c_cflag |= (speed | bits | CREAD | 0 | parity | stopbits);

	if (tcsetattr (priv->fd, TCSANOW, &stbuf) < 0) {
		nm_warning ("(%s) cannot control device (errno %d)",
		            nm_device_get_iface (NM_DEVICE (device)), errno);
		return FALSE;
	}

	return TRUE;
}

gboolean
nm_serial_device_open (NMSerialDevice *device,
                       NMSettingSerial *setting)
{
	NMSerialDevicePrivate *priv;
	const char *iface;
	char *path;

	g_return_val_if_fail (NM_IS_SERIAL_DEVICE (device), FALSE);
	g_return_val_if_fail (NM_IS_SETTING_SERIAL (setting), FALSE);

	priv = NM_SERIAL_DEVICE_GET_PRIVATE (device);
	iface = nm_device_get_iface (NM_DEVICE (device));

	if (priv->fd >= 0) {
		nm_debug ("(%s) bug: device already open", iface);
		nm_serial_device_close (device);
		priv->fd = -1;
	}

	nm_debug ("(%s) opening device...", iface);

	path = g_build_filename ("/dev", iface, NULL);
	priv->fd = open (path, O_RDWR | O_EXCL | O_NONBLOCK | O_NOCTTY);
	g_free (path);

	if (priv->fd < 0) {
		nm_warning ("(%s) cannot open device (errno %d)", iface, errno);
		goto error;
	}

	if (tcgetattr (priv->fd, &priv->old_t) < 0) {
		nm_warning ("(%s) cannot control device (errno %d)", iface, errno);
		goto error;
	}

	if (!config_fd (device, setting))
		goto error;

	priv->channel = g_io_channel_unix_new (priv->fd);
	g_io_channel_set_encoding (priv->channel, NULL, NULL);
	return TRUE;

error:
	if (priv->fd >= 0)
		close (priv->fd);
	priv->fd = -1;
	return FALSE;
}

void
nm_serial_device_close (NMSerialDevice *device)
{
	NMSerialDevicePrivate *priv;

	g_return_if_fail (NM_IS_SERIAL_DEVICE (device));

	priv = NM_SERIAL_DEVICE_GET_PRIVATE (device);

	if (priv->pending_id)
		g_source_remove (priv->pending_id);

	if (priv->ppp_manager) {
		nm_ppp_manager_stop (priv->ppp_manager);
		g_object_unref (priv->ppp_manager);
		priv->ppp_manager = NULL;
	}

	if (priv->fd >= 0) {
		nm_debug ("Closing device '%s'", nm_device_get_iface (NM_DEVICE (device)));

		if (priv->channel) {
			g_io_channel_unref (priv->channel);
			priv->channel = NULL;
		}

		tcsetattr (priv->fd, TCSANOW, &priv->old_t);
		close (priv->fd);
		priv->fd = -1;
	}
}

gboolean
nm_serial_device_send_command (NMSerialDevice *device, GByteArray *command)
{
	int fd;
	NMSettingSerial *setting;
	int i, eagain_count = 1000;
	ssize_t written;
	guint32 send_delay = 0;

	g_return_val_if_fail (NM_IS_SERIAL_DEVICE (device), FALSE);
	g_return_val_if_fail (command != NULL, FALSE);

	fd = NM_SERIAL_DEVICE_GET_PRIVATE (device)->fd;
	setting = NM_SETTING_SERIAL (serial_device_get_setting (device, NM_TYPE_SETTING_SERIAL));
	if (setting)
		send_delay = nm_setting_serial_get_send_delay (setting);
	if (send_delay == 0)
		send_delay = G_USEC_PER_SEC / 500;

	nm_serial_debug ("Sending:", (char *) command->data, command->len);

	for (i = 0; i < command->len && eagain_count > 0;) {
		written = write (fd, command->data + i, 1);

		if (written > 0)
			i += written;
		else {
			/* Treat written == 0 as EAGAIN to ensure we break out of the
			 * for() loop eventually.
			 */
			if ((written < 0) && (errno != EAGAIN)) {
				g_warning ("Error in writing (errno %d)", errno);
				return FALSE;
			}
			eagain_count--;
		}
		g_usleep (send_delay);
	}

	if (eagain_count <= 0)
		nm_serial_debug ("Error: too many retries sending:", (char *) command->data, command->len);

	return TRUE;
}

gboolean
nm_serial_device_send_command_string (NMSerialDevice *device, const char *str)
{
	GByteArray *command;
	gboolean ret;

	g_return_val_if_fail (NM_IS_SERIAL_DEVICE (device), FALSE);
	g_return_val_if_fail (str != NULL, FALSE);

	command = g_byte_array_new ();
	g_byte_array_append (command, (guint8 *) str, strlen (str));
	g_byte_array_append (command, (guint8 *) "\r", 1);

	ret = nm_serial_device_send_command (device, command);
	g_byte_array_free (command, TRUE);

	return ret;
}

static gboolean
find_terminator (const char *line, const char **terminators)
{
	int i;

	for (i = 0; terminators[i]; i++) {
		if (!strncasecmp (line, terminators[i], strlen (terminators[i])))
			return TRUE;
	}
	return FALSE;
}

static const char *
find_response (const char *line, const char **responses, gint *idx)
{
	int i;

	/* Don't look for a result again if we got one previously */
	for (i = 0; responses[i]; i++) {
		if (strcasestr (line, responses[i])) {
			*idx = i;
			return line;
		}
	}
	return NULL;
}

#define RESPONSE_LINE_MAX 128

int
nm_serial_device_wait_reply_blocking (NMSerialDevice *device,
                                      guint32 timeout_secs,
                                      const char **needles,
                                      const char **terminators)
{
	char buf[SERIAL_BUF_SIZE + 1];
	int fd, reply_index = -1, bytes_read;
	GString *result = NULL;
	time_t end;
	const char *response = NULL;
	gboolean done = FALSE;

	g_return_val_if_fail (NM_IS_SERIAL_DEVICE (device), -1);
	g_return_val_if_fail (timeout_secs <= 60, -1);
	g_return_val_if_fail (needles != NULL, -1);

	fd = NM_SERIAL_DEVICE_GET_PRIVATE (device)->fd;
	if (fd < 0)
		return -1;

	end = time (NULL) + timeout_secs;
	result = g_string_sized_new (20);
	do {
		bytes_read = read (fd, buf, SERIAL_BUF_SIZE);
		if (bytes_read < 0 && errno != EAGAIN) {
			nm_warning ("%s: read error: %d (%s)",
			            nm_device_get_iface (NM_DEVICE (device)),
			            errno,
			            strerror (errno));
			return -1;
		}

		if (bytes_read == 0)
			break; /* EOF */
		else if (bytes_read > 0) {
			buf[bytes_read] = 0;
			g_string_append (result, buf);

			nm_serial_debug ("Got:", result->str, result->len);
		}

		/* Look for needles and terminators */
		if ((bytes_read > 0) && result->str) {
			char *p = result->str;

			/* Break the response up into lines and process each one */
			while ((p < result->str + strlen (result->str)) && !done) {
				char line[RESPONSE_LINE_MAX] = { '\0', };
				char *tmp;
				int i;
				gboolean got_something = FALSE;

				for (i = 0; *p && (i < RESPONSE_LINE_MAX - 1); p++) {
					/* Ignore front CR/LF */
					if ((*p == '\n') || (*p == '\r')) {
						if (got_something)
							break;
					} else {
						line[i++] = *p;
						got_something = TRUE;
					}
				}
				line[i] = '\0';

				tmp = g_strstrip (line);
				if (tmp && strlen (tmp)) {
					done = find_terminator (tmp, terminators);
					if (reply_index == -1)
						response = find_response (tmp, needles, &reply_index);
				}
			}
		}

		/* Limit the size of the buffer */
		if (result->len > SERIAL_BUF_SIZE) {
			g_warning ("%s (%s): response buffer filled before repsonse received",
			           __func__, nm_device_get_iface (NM_DEVICE (device)));
			break;
		}

		if (!done)
			g_usleep (100);
	} while (!done && (time (NULL) < end));

	return reply_index;
}

typedef struct {
	NMSerialDevice *device;
	char **str_needles;
	char **terminators;
	GString *result;
	NMSerialWaitForReplyFn callback;
	gpointer user_data;
	int reply_index;
	char *reply_line;
	time_t end;
} WaitForReplyInfo;

static void
wait_for_reply_done (gpointer data)
{
	WaitForReplyInfo *info = (WaitForReplyInfo *) data;

	nm_serial_device_pending_done (info->device);

	/* Call the callback */
	info->callback (info->device, info->reply_index, info->reply_line, info->user_data);

	/* Free info */
	if (info->result)
		g_string_free (info->result, TRUE);

	g_free (info->reply_line);

	g_strfreev (info->str_needles);
	g_strfreev (info->terminators);
	g_slice_free (WaitForReplyInfo, info);
}

static gboolean
wait_for_reply_got_data (GIOChannel *source,
					GIOCondition condition,
					gpointer data)
{
	WaitForReplyInfo *info = (WaitForReplyInfo *) data;
	gchar buf[SERIAL_BUF_SIZE + 1];
	gsize bytes_read;
	GIOStatus status;
	gboolean done = FALSE;

	if (condition & G_IO_HUP || condition & G_IO_ERR)
		return FALSE;

	do {
		GError *err = NULL;

		status = g_io_channel_read_chars (source, buf, SERIAL_BUF_SIZE, &bytes_read, &err);
		if (status == G_IO_STATUS_ERROR) {
			g_warning ("%s", err->message);
			g_error_free (err);
			err = NULL;
		}

		if (bytes_read > 0) {
			buf[bytes_read] = 0;
			g_string_append (info->result, buf);

			nm_serial_debug ("Got:", info->result->str, info->result->len);
		}

		/* Look for needles and terminators */
		if ((bytes_read > 0) && info->result->str) {
			char *p = info->result->str;

			/* Break the response up into lines and process each one */
			while ((p < info->result->str + strlen (info->result->str)) && !done) {
				char line[RESPONSE_LINE_MAX] = { '\0', };
				char *tmp;
				int i;
				gboolean got_something = FALSE;

				for (i = 0; *p && (i < RESPONSE_LINE_MAX - 1); p++) {
					/* Ignore front CR/LF */
					if ((*p == '\n') || (*p == '\r')) {
						if (got_something)
							break;
					} else {
						line[i++] = *p;
						got_something = TRUE;
					}
				}
				line[i] = '\0';

				tmp = g_strstrip (line);
				if (tmp && strlen (tmp)) {
					done = find_terminator (tmp, (const char **) info->terminators);
					if (info->reply_index == -1) {
						if (find_response (tmp, (const char **) info->str_needles, &(info->reply_index)))
							info->reply_line = g_strdup (tmp);
					}
				}
			}
		}

		/* Limit the size of the buffer */
		if (info->result->len > SERIAL_BUF_SIZE) {
			nm_warning ("(%s): response buffer filled before repsonse received",
			            nm_device_get_iface (NM_DEVICE (info->device)));
			done = TRUE;
			break;
		}

		/* Make sure we don't go over the timeout, in addition to the timeout
		 * handler that's been scheduled.  If for some reason this loop doesn't
		 * terminate (terminator not found, whatever) then this should make
		 * sure that NM doesn't spin the CPU forever.
		 */
		if (time (NULL) > info->end) {
			done = TRUE;
			break;
		} else if (!done)
			g_usleep (50);
	} while (!done || bytes_read == SERIAL_BUF_SIZE || status == G_IO_STATUS_AGAIN);

	return !done;
}

guint
nm_serial_device_wait_for_reply (NMSerialDevice *device,
                                 guint timeout,
                                 const char **responses,
                                 const char **terminators,
                                 NMSerialWaitForReplyFn callback,
                                 gpointer user_data)
{
	WaitForReplyInfo *info;

	g_return_val_if_fail (NM_IS_SERIAL_DEVICE (device), 0);
	g_return_val_if_fail (responses != NULL, 0);
	g_return_val_if_fail (callback != NULL, 0);

	info = g_slice_new0 (WaitForReplyInfo);
	info->device = device;
	info->str_needles = g_strdupv ((char **) responses);
	info->terminators = g_strdupv ((char **) terminators);
	info->result = g_string_new (NULL);
	info->callback = callback;
	info->user_data = user_data;
	info->reply_index = -1;
	info->end = time (NULL) + timeout;

	return nm_serial_device_set_pending (device, timeout, wait_for_reply_got_data, info, wait_for_reply_done);
}

#if 0
typedef struct {
	NMSerialDevice *device;
	gboolean timed_out;
	NMSerialWaitQuietFn callback;
	gpointer user_data;
} WaitQuietInfo;

static void
wait_quiet_done (gpointer data)
{
	WaitQuietInfo *info = (WaitQuietInfo *) data;

	nm_serial_device_pending_done (info->device);

	/* Call the callback */
	info->callback (info->device, info->timed_out, info->user_data);

	/* Free info */
	g_slice_free (WaitQuietInfo, info);
}

static gboolean
wait_quiet_quiettime (gpointer data)
{
	WaitQuietInfo *info = (WaitQuietInfo *) data;

	info->timed_out = FALSE;
	g_source_remove (NM_SERIAL_DEVICE_GET_PRIVATE (info->device)->pending);

	return FALSE;
}

static gboolean
wait_quiet_got_data (GIOChannel *source,
				 GIOCondition condition,
				 gpointer data)
{
	WaitQuietInfo *info = (WaitQuietInfo *) data;
	gsize bytes_read;
	char buf[4096];
	GIOStatus status;

	if (condition & G_IO_HUP || condition & G_IO_ERR)
		return FALSE;

	if (condition & G_IO_IN) {
		do {
			status = g_io_channel_read_chars (source, buf, 4096, &bytes_read, NULL);

			if (bytes_read) {
				/* Reset the quiet time timeout */
				g_source_remove (info->quiet_id);
				info->quiet_id = g_timeout_add (info->quiet_time, wait_quiet_quiettime, info);
			}
		} while (bytes_read == 4096 || status == G_IO_STATUS_AGAIN);
	}

	return TRUE;
}

void
nm_serial_device_wait_quiet (NMSerialDevice *device,
					    guint timeout, 
					    guint quiet_time,
					    NMSerialWaitQuietFn callback,
					    gpointer user_data)
{
	WaitQuietInfo *info;

	g_return_if_fail (NM_IS_SERIAL_DEVICE (device));
	g_return_if_fail (callback != NULL);

	info = g_slice_new0 (WaitQuietInfo);
	info->device = device;
	info->timed_out = TRUE;
	info->callback = callback;
	info->user_data = user_data;
	info->quiet_id = g_timeout_add (quiet_time,
							  wait_quiet_timeout,
							  info);

	return nm_serial_device_set_pending (device, timeout, wait_quiet_got_data, info, wait_quiet_done);
}

#endif

typedef struct {
	NMSerialDevice *device;
	speed_t current_speed;
	NMSerialFlashFn callback;
	gpointer user_data;
} FlashInfo;

static speed_t
get_speed (NMSerialDevice *device)
{
	struct termios options;

	tcgetattr (NM_SERIAL_DEVICE_GET_PRIVATE (device)->fd, &options);

	return cfgetospeed (&options);
}

static void
set_speed (NMSerialDevice *device, speed_t speed)
{
	struct termios options;
	int fd;

	fd = NM_SERIAL_DEVICE_GET_PRIVATE (device)->fd;
	tcgetattr (fd, &options);

	cfsetispeed (&options, speed);
	cfsetospeed (&options, speed);

	options.c_cflag |= (CLOCAL | CREAD);
	tcsetattr (fd, TCSANOW, &options);
}

static void
flash_done (gpointer data)
{
	FlashInfo *info = (FlashInfo *) data;

	NM_SERIAL_DEVICE_GET_PRIVATE (info->device)->pending_id = 0;

	info->callback (info->device, info->user_data);

	g_slice_free (FlashInfo, info);
}

static gboolean
flash_do (gpointer data)
{
	FlashInfo *info = (FlashInfo *) data;

	set_speed (info->device, info->current_speed);

	return FALSE;
}

guint
nm_serial_device_flash (NMSerialDevice *device,
				    guint32 flash_time,
				    NMSerialFlashFn callback,
				    gpointer user_data)
{
	FlashInfo *info;
	guint id;

	g_return_val_if_fail (NM_IS_SERIAL_DEVICE (device), 0);
	g_return_val_if_fail (callback != NULL, 0);

	info = g_slice_new0 (FlashInfo);
	info->device = device;
	info->current_speed = get_speed (device);
	info->callback = callback;
	info->user_data = user_data;

	set_speed (device, B0);

	id = g_timeout_add_full (G_PRIORITY_DEFAULT,
						flash_time,
						flash_do,
						info,
						flash_done);

	NM_SERIAL_DEVICE_GET_PRIVATE (device)->pending_id = id;

	return id;
}

GIOChannel *
nm_serial_device_get_io_channel (NMSerialDevice *device)
{
	NMSerialDevicePrivate *priv;

	g_return_val_if_fail (NM_IS_SERIAL_DEVICE (device), 0);

	priv = NM_SERIAL_DEVICE_GET_PRIVATE (device);
	if (priv->channel)
		return g_io_channel_ref (priv->channel);

	return NULL;
}

NMPPPManager *
nm_serial_device_get_ppp_manager (NMSerialDevice *device)
{
	g_return_val_if_fail (NM_IS_SERIAL_DEVICE (device), NULL);

	return NM_SERIAL_DEVICE_GET_PRIVATE (device)->ppp_manager;
}

static void
ppp_state_changed (NMPPPManager *ppp_manager, NMPPPStatus status, gpointer user_data)
{
	NMDevice *device = NM_DEVICE (user_data);

	switch (status) {
	case NM_PPP_STATUS_NETWORK:
		nm_device_state_changed (device, NM_DEVICE_STATE_IP_CONFIG, NM_DEVICE_STATE_REASON_NONE);
		break;
	case NM_PPP_STATUS_DISCONNECT:
		nm_device_state_changed (device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_PPP_DISCONNECT);
		break;
	case NM_PPP_STATUS_DEAD:
		nm_device_state_changed (device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_PPP_FAILED);
		break;
	case NM_PPP_STATUS_AUTHENTICATE:
		nm_device_state_changed (device, NM_DEVICE_STATE_NEED_AUTH, NM_DEVICE_STATE_REASON_NONE);
		break;
	default:
		break;
	}
}

static void
ppp_ip4_config (NMPPPManager *ppp_manager,
			 const char *iface,
			 NMIP4Config *config,
			 gpointer user_data)
{
	NMDevice *device = NM_DEVICE (user_data);

	nm_device_set_ip_iface (device, iface);
	NM_SERIAL_DEVICE_GET_PRIVATE (device)->pending_ip4_config = g_object_ref (config);
	nm_device_activate_schedule_stage4_ip_config_get (device);
}

static void
ppp_stats (NMPPPManager *ppp_manager,
		 guint32 in_bytes,
		 guint32 out_bytes,
		 gpointer user_data)
{
	NMSerialDevice *device = NM_SERIAL_DEVICE (user_data);
	NMSerialDevicePrivate *priv = NM_SERIAL_DEVICE_GET_PRIVATE (device);

	if (priv->in_bytes != in_bytes || priv->out_bytes != out_bytes) {
		priv->in_bytes = in_bytes;
		priv->out_bytes = out_bytes;

		g_signal_emit (device, signals[PPP_STATS], 0, in_bytes, out_bytes);
	}
}

static NMActStageReturn
real_act_stage2_config (NMDevice *device, NMDeviceStateReason *reason)
{
	NMSerialDevicePrivate *priv = NM_SERIAL_DEVICE_GET_PRIVATE (device);
	NMSerialDeviceClass *serial_class = NM_SERIAL_DEVICE_GET_CLASS (device);
	NMActRequest *req;
	GError *err = NULL;
	NMActStageReturn ret;
	const char *ppp_name = NULL;

	req = nm_device_get_act_request (device);
	g_assert (req);

	if (serial_class->get_ppp_name)
		ppp_name = serial_class->get_ppp_name (NM_SERIAL_DEVICE (device), req);

	priv->ppp_manager = nm_ppp_manager_new (nm_device_get_iface (device));
	if (nm_ppp_manager_start (priv->ppp_manager, req, ppp_name, 20, &err)) {
		g_signal_connect (priv->ppp_manager, "state-changed",
					   G_CALLBACK (ppp_state_changed),
					   device);
		g_signal_connect (priv->ppp_manager, "ip4-config",
					   G_CALLBACK (ppp_ip4_config),
					   device);
		g_signal_connect (priv->ppp_manager, "stats",
					   G_CALLBACK (ppp_stats),
					   device);

		ret = NM_ACT_STAGE_RETURN_POSTPONE;
	} else {
		nm_warning ("%s", err->message);
		g_error_free (err);

		g_object_unref (priv->ppp_manager);
		priv->ppp_manager = NULL;

		*reason = NM_DEVICE_STATE_REASON_PPP_START_FAILED;
		ret = NM_ACT_STAGE_RETURN_FAILURE;
	}

	return ret;
}

static NMActStageReturn
real_act_stage4_get_ip4_config (NMDevice *device,
                                NMIP4Config **config,
                                NMDeviceStateReason *reason)
{
	NMSerialDevicePrivate *priv = NM_SERIAL_DEVICE_GET_PRIVATE (device);
	NMConnection *connection;
	NMSettingIP4Config *s_ip4;

	g_return_val_if_fail (config != NULL, NM_ACT_STAGE_RETURN_FAILURE);
	g_return_val_if_fail (*config == NULL, NM_ACT_STAGE_RETURN_FAILURE);
	g_return_val_if_fail (reason != NULL, NM_ACT_STAGE_RETURN_FAILURE);

	connection = nm_act_request_get_connection (nm_device_get_act_request (device));
	g_assert (connection);

	s_ip4 = (NMSettingIP4Config *) nm_connection_get_setting (connection, NM_TYPE_SETTING_IP4_CONFIG);

	*config = priv->pending_ip4_config;
	priv->pending_ip4_config = NULL;
	nm_utils_merge_ip4_config (*config, s_ip4);

	return NM_ACT_STAGE_RETURN_SUCCESS;
}

static void
cleanup_device (NMSerialDevice *device)
{
	NMSerialDevicePrivate *priv = NM_SERIAL_DEVICE_GET_PRIVATE (device);

	nm_device_set_ip_iface (NM_DEVICE (device), NULL);

	if (priv->pending_ip4_config) {
		g_object_unref (priv->pending_ip4_config);
		priv->pending_ip4_config = NULL;
	}

	priv->in_bytes = priv->out_bytes = 0;
}

static void
real_deactivate_quickly (NMDevice *device)
{
	NMSerialDevice *self = NM_SERIAL_DEVICE (device);

	cleanup_device (self);
	nm_serial_device_close (self);
}

static guint32
real_get_generic_capabilities (NMDevice *dev)
{
	return NM_DEVICE_CAP_NM_SUPPORTED;
}

/*****************************************************************************/

static void
nm_serial_device_init (NMSerialDevice *self)
{
	if (getenv ("NM_SERIAL_DEBUG"))
		serial_debug = TRUE;

	NM_SERIAL_DEVICE_GET_PRIVATE (self)->fd = -1;
}

static void
finalize (GObject *object)
{
	NMSerialDevice *self = NM_SERIAL_DEVICE (object);

	cleanup_device (self);
	nm_serial_device_close (self);

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

static void
nm_serial_device_class_init (NMSerialDeviceClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	NMDeviceClass *parent_class = NM_DEVICE_CLASS (klass);

	g_type_class_add_private (object_class, sizeof (NMSerialDevicePrivate));

	/* Virtual methods */
	object_class->finalize = finalize;

	parent_class->get_generic_capabilities = real_get_generic_capabilities;
	parent_class->act_stage2_config = real_act_stage2_config;
	parent_class->act_stage4_get_ip4_config = real_act_stage4_get_ip4_config;
	parent_class->deactivate_quickly = real_deactivate_quickly;

	/* Signals */
	signals[PPP_STATS] =
		g_signal_new ("ppp-stats",
				    G_OBJECT_CLASS_TYPE (object_class),
				    G_SIGNAL_RUN_FIRST,
				    G_STRUCT_OFFSET (NMSerialDeviceClass, ppp_stats),
				    NULL, NULL,
				    _nm_marshal_VOID__UINT_UINT,
				    G_TYPE_NONE, 2,
				    G_TYPE_UINT, G_TYPE_UINT);

	dbus_g_object_type_install_info (G_TYPE_FROM_CLASS (klass),
							   &dbus_glib_nm_serial_device_object_info);
}
