/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
* Qfu-firmware-update -- Command line tool to update firmware in QFU devices
*
* 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 .
*
* Copyright (C) 2016 Zodiac Inflight Innovations
* Copyright (C) 2016 Aleksander Morgado
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "qfu-log.h"
#include "qfu-at-device.h"
#include "qfu-utils.h"
#define QFU_AT_BUFFER_SIZE 128
static void initable_iface_init (GInitableIface *iface);
G_DEFINE_TYPE_EXTENDED (QfuAtDevice, qfu_at_device, G_TYPE_OBJECT, 0,
G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init))
enum {
PROP_0,
PROP_FILE,
PROP_LAST
};
static GParamSpec *properties[PROP_LAST];
struct _QfuAtDevicePrivate {
GFile *file;
gchar *name;
gint fd;
GByteArray *buffer;
};
/******************************************************************************/
/* Send */
static gboolean
send_request (QfuAtDevice *self,
const gchar *request,
GCancellable *cancellable,
GError **error)
{
gssize wlen;
fd_set wr;
gint aux;
struct timeval tv = {
.tv_sec = 2,
.tv_usec = 0,
};
/* Wait for the fd to be writable and don't wait forever */
FD_ZERO (&wr);
FD_SET (self->priv->fd, &wr);
aux = select (self->priv->fd + 1, NULL, &wr, NULL, &tv);
if (g_cancellable_set_error_if_cancelled (cancellable, error))
return FALSE;
if (aux < 0) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"error waiting to write: %s",
g_strerror (errno));
return FALSE;
}
if (aux == 0) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"timed out waiting to write");
return FALSE;
}
/* Debug output */
if (qfu_log_get_verbose ())
g_debug ("[qfu-at-device,%s] >> %s", self->priv->name, request);
wlen = write (self->priv->fd, request, strlen (request));
if (wlen < 0) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"error writting: %s",
g_strerror (errno));
return FALSE;
}
/* We treat EINTR as an error, so we also treat as an error if not all bytes
* were wlen */
if (wlen != strlen (request)) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"error writing: only %" G_GSSIZE_FORMAT "/%" G_GSIZE_FORMAT " bytes written",
wlen, wlen);
return FALSE;
}
wlen = write (self->priv->fd, "\r", 1);
if (wlen < 0) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"error writting : %s",
g_strerror (errno));
return FALSE;
}
return TRUE;
}
/******************************************************************************/
/* Receive */
static gssize
receive_response (QfuAtDevice *self,
guint timeout_secs,
gchar **response,
GCancellable *cancellable,
GError **error)
{
fd_set rd;
struct timeval tv;
gint aux;
gssize rlen;
gchar *start;
/* Use requested timeout */
tv.tv_sec = timeout_secs;
tv.tv_usec = 0;
FD_ZERO (&rd);
FD_SET (self->priv->fd, &rd);
aux = select (self->priv->fd + 1, &rd, NULL, NULL, &tv);
if (g_cancellable_set_error_if_cancelled (cancellable, error))
return -1;
if (aux < 0) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"error waiting to read response: %s",
g_strerror (errno));
return -1;
}
if (aux == 0) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"timed out waiting for the response");
return -1;
}
/* Receive in the primary buffer */
rlen = read (self->priv->fd, self->priv->buffer->data, self->priv->buffer->len);
if (rlen < 0) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"couldn't read response: %s",
g_strerror (errno));
return -1;
}
if (rlen == 0) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"couldn't read response: HUP detected");
return -1;
}
/* Force end of string */
start = (gchar *) &self->priv->buffer->data[0];
start[rlen] = '\0';
/* Remove trailing LFs and CRs */
while (rlen > 0 && (start[rlen - 1] == '\r' || start[rlen - 1] == '\n'))
start[--rlen] = '\0';
/* Remove heading LFs and CRs */
while (rlen > 0 && (start[0] == '\r' || start[0] == '\n')) {
start++;
rlen--;
}
/* Debug output */
if (qfu_log_get_verbose ())
g_debug ("[qfu-at-device,%s] << %s", self->priv->name, start);
if (response)
*response = start;
return rlen;
}
/******************************************************************************/
/* Send/receive */
static gssize
send_receive (QfuAtDevice *self,
const gchar *request,
guint response_timeout_secs,
gchar **response,
GCancellable *cancellable,
GError **error)
{
gboolean sent;
if (self->priv->fd < 0) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "device is closed");
return FALSE;
}
sent = send_request (self, request, cancellable, error);
if (!sent)
return -1;
if (!response)
return 0;
while (1) {
gssize rsplen;
rsplen = receive_response (self, response_timeout_secs, response, cancellable, error);
if (rsplen > 0 && g_str_has_prefix (*response, request)) {
*response += strlen (request);
rsplen -= strlen (request);
if (rsplen == 0)
continue;
}
return rsplen;
}
}
/******************************************************************************/
gboolean
qfu_at_device_boothold (QfuAtDevice *self,
GCancellable *cancellable,
GError **error)
{
gssize rsplen;
gchar *rsp = NULL;
rsplen = send_receive (self, "AT!BOOTHOLD", 3, &rsp, cancellable, error);
if (rsplen < 0)
return FALSE;
if (strstr (rsp, "OK"))
return TRUE;
if (strstr (rsp, "ERROR"))
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "unknown command");
else
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "unknown error");
return FALSE;
}
/******************************************************************************/
static gboolean
initable_init (GInitable *initable,
GCancellable *cancellable,
GError **error)
{
QfuAtDevice *self;
struct termios terminal_data;
gchar *path = NULL;
GError *inner_error = NULL;
self = QFU_AT_DEVICE (initable);
if (g_cancellable_set_error_if_cancelled (cancellable, &inner_error))
goto out;
path = g_file_get_path (self->priv->file);
g_debug ("[qfu-at-device,%s] opening TTY", self->priv->name);
self->priv->fd = open (path, O_RDWR | O_NOCTTY);
if (self->priv->fd < 0) {
inner_error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
"error opening serial device: %s",
g_strerror (errno));
goto out;
}
g_debug ("[qfu-at-device,%s] setting up serial port...", self->priv->name);
if (tcgetattr (self->priv->fd, &terminal_data) < 0) {
inner_error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
"error getting serial port attributes: %s",
g_strerror (errno));
goto out;
}
terminal_data.c_iflag &= ~(IGNCR | ICRNL | IUCLC | INPCK | IXON | IXANY );
terminal_data.c_oflag &= ~(OPOST | OLCUC | OCRNL | ONLCR | ONLRET);
terminal_data.c_lflag &= ~(ICANON | ECHO | ECHOE | ECHONL);
terminal_data.c_cc[VMIN] = 1;
terminal_data.c_cc[VTIME] = 0;
terminal_data.c_cc[VEOF] = 1;
terminal_data.c_iflag |= (IXON | IXOFF | IXANY | IGNPAR);
terminal_data.c_cflag &= ~(CBAUD | CSIZE | CSTOPB | PARENB | CRTSCTS);
terminal_data.c_cflag |= (CS8 | CREAD | CLOCAL); /* 8N1 */
errno = 0;
if (cfsetispeed (&terminal_data, B115200) != 0) {
inner_error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
"failed to set serial port input speed: %s",
g_strerror (errno));
goto out;
}
errno = 0;
if (cfsetospeed (&terminal_data, B115200) != 0) {
inner_error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
"failed to set serial port output speed: %s",
g_strerror (errno));
goto out;
}
if (tcsetattr (self->priv->fd, TCSANOW, &terminal_data) < 0) {
inner_error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
"error setting serial port attributes: %s",
g_strerror (errno));
goto out;
}
out:
g_free (path);
if (inner_error) {
if (!(self->priv->fd < 0)) {
close (self->priv->fd);
self->priv->fd = -1;
}
g_propagate_error (error, inner_error);
return FALSE;
}
return TRUE;
}
/******************************************************************************/
const gchar *
qfu_at_device_get_name (QfuAtDevice *self)
{
return self->priv->name;
}
/******************************************************************************/
QfuAtDevice *
qfu_at_device_new (GFile *file,
GCancellable *cancellable,
GError **error)
{
g_return_val_if_fail (G_IS_FILE (file), NULL);
return QFU_AT_DEVICE (g_initable_new (QFU_TYPE_AT_DEVICE,
cancellable,
error,
"file", file,
NULL));
}
static void
qfu_at_device_init (QfuAtDevice *self)
{
self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, QFU_TYPE_AT_DEVICE, QfuAtDevicePrivate);
self->priv->fd = -1;
/* Long buffer for I/O */
self->priv->buffer = g_byte_array_new ();
g_byte_array_set_size (self->priv->buffer, QFU_AT_BUFFER_SIZE);
}
static void
set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
QfuAtDevice *self = QFU_AT_DEVICE (object);
switch (prop_id) {
case PROP_FILE:
self->priv->file = g_value_dup_object (value);
if (self->priv->file)
self->priv->name = g_file_get_basename (self->priv->file);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
QfuAtDevice *self = QFU_AT_DEVICE (object);
switch (prop_id) {
case PROP_FILE:
g_value_set_object (value, self->priv->file);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
dispose (GObject *object)
{
QfuAtDevice *self = QFU_AT_DEVICE (object);
if (!(self->priv->fd < 0)) {
close (self->priv->fd);
self->priv->fd = -1;
}
g_clear_pointer (&self->priv->name, g_free);
g_clear_pointer (&self->priv->buffer, g_byte_array_unref);
g_clear_object (&self->priv->file);
G_OBJECT_CLASS (qfu_at_device_parent_class)->dispose (object);
}
static void
initable_iface_init (GInitableIface *iface)
{
iface->init = initable_init;
}
static void
qfu_at_device_class_init (QfuAtDeviceClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
g_type_class_add_private (object_class, sizeof (QfuAtDevicePrivate));
object_class->dispose = dispose;
object_class->get_property = get_property;
object_class->set_property = set_property;
properties[PROP_FILE] =
g_param_spec_object ("file",
"File",
"File object",
G_TYPE_FILE,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
g_object_class_install_property (object_class, PROP_FILE, properties[PROP_FILE]);
}