/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * 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:
 *
 * Copyright (C) 2008 - 2009 Novell, Inc.
 * Copyright (C) 2009 - 2018 Red Hat, Inc.
 * Copyright (C) 2011 - 2018 Aleksander Morgado <aleksander@aleksander.es>
 */

#include "config.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <ModemManager.h>
#include <ModemManager-tags.h>

#include <mm-errors-types.h>

#include "mm-port-probe.h"
#include "mm-log.h"
#include "mm-port-serial-at.h"
#include "mm-port-serial.h"
#include "mm-serial-parsers.h"
#include "mm-port-probe-at.h"
#include "libqcdm/src/commands.h"
#include "libqcdm/src/utils.h"
#include "libqcdm/src/errors.h"
#include "mm-port-serial-qcdm.h"
#include "mm-daemon-enums-types.h"

#if defined WITH_QMI
#include "mm-port-qmi.h"
#endif

#if defined WITH_MBIM
#include "mm-port-mbim.h"
#endif

/*
 * Steps and flow of the Probing process:
 * ----> AT Serial Open
 *   |----> Custom Init
 *   |----> AT?
 *      |----> Vendor
 *      |----> Product
 *      |----> Is Icera?
 *      |----> Is Xmm?
 * ----> QCDM Serial Open
 *   |----> QCDM?
 * ----> QMI Device Open
 *   |----> QMI Version Info check
 * ----> MBIM Device Open
 *   |----> MBIM capabilities check
 */

G_DEFINE_TYPE (MMPortProbe, mm_port_probe, G_TYPE_OBJECT)

enum {
    PROP_0,
    PROP_DEVICE,
    PROP_PORT,
    PROP_LAST
};

static GParamSpec *properties[PROP_LAST];

struct _MMPortProbePrivate {
    /* Properties */
    MMDevice *device;
    MMKernelDevice *port;

    /* Probing results */
    guint32 flags;
    gboolean is_at;
    gboolean is_qcdm;
    gchar *vendor;
    gchar *product;
    gboolean is_icera;
    gboolean is_xmm;
    gboolean is_qmi;
    gboolean is_mbim;

    /* From udev tags */
    gboolean is_ignored;
    gboolean is_gps;
    gboolean is_audio;
    gboolean maybe_at_primary;
    gboolean maybe_at_secondary;
    gboolean maybe_at_ppp;
    gboolean maybe_qcdm;

    /* Current probing task. Only one can be available at a time */
    GTask *task;
};

/*****************************************************************************/
/* Probe task completions.
 * Always make sure that the stored task is NULL when the task is completed.
 */

static gboolean
port_probe_task_return_error_if_cancelled (MMPortProbe *self)
{
    GTask *task;

    task = self->priv->task;
    self->priv->task = NULL;

    if (g_task_return_error_if_cancelled (task)) {
        g_object_unref (task);
        return TRUE;
    }

    self->priv->task = task;
    return FALSE;
}

static void
port_probe_task_return_error (MMPortProbe *self,
                              GError      *error)
{
    GTask *task;

    task = self->priv->task;
    self->priv->task = NULL;
    g_task_return_error (task, error);
    g_object_unref (task);
}

static void
port_probe_task_return_boolean (MMPortProbe *self,
                                gboolean     result)
{
    GTask *task;

    task = self->priv->task;
    self->priv->task = NULL;
    g_task_return_boolean (task, result);
    g_object_unref (task);
}

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

void
mm_port_probe_set_result_at (MMPortProbe *self,
                             gboolean at)
{
    self->priv->is_at = at;
    self->priv->flags |= MM_PORT_PROBE_AT;

    if (self->priv->is_at) {
        mm_dbg ("(%s/%s) port is AT-capable",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));

        /* Also set as not a QCDM/QMI/MBIM port */
        self->priv->is_qcdm = FALSE;
        self->priv->is_qmi = FALSE;
        self->priv->is_mbim = FALSE;
        self->priv->flags |= (MM_PORT_PROBE_QCDM | MM_PORT_PROBE_QMI | MM_PORT_PROBE_MBIM);
    } else {
        mm_dbg ("(%s/%s) port is not AT-capable",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));
        self->priv->vendor = NULL;
        self->priv->product = NULL;
        self->priv->is_icera = FALSE;
        self->priv->is_xmm = FALSE;
        self->priv->flags |= (MM_PORT_PROBE_AT_VENDOR |
                              MM_PORT_PROBE_AT_PRODUCT |
                              MM_PORT_PROBE_AT_ICERA |
                              MM_PORT_PROBE_AT_XMM);
    }
}

void
mm_port_probe_set_result_at_vendor (MMPortProbe *self,
                                    const gchar *at_vendor)
{
    if (at_vendor) {
        mm_dbg ("(%s/%s) vendor probing finished",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));
        self->priv->vendor = g_utf8_casefold (at_vendor, -1);
        self->priv->flags |= MM_PORT_PROBE_AT_VENDOR;
    } else {
        mm_dbg ("(%s/%s) couldn't probe for vendor string",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));
        self->priv->vendor = NULL;
        self->priv->product = NULL;
        self->priv->flags |= (MM_PORT_PROBE_AT_VENDOR | MM_PORT_PROBE_AT_PRODUCT);
    }
}

void
mm_port_probe_set_result_at_product (MMPortProbe *self,
                                     const gchar *at_product)
{
    if (at_product) {
        mm_dbg ("(%s/%s) product probing finished",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));
        self->priv->product = g_utf8_casefold (at_product, -1);
        self->priv->flags |= MM_PORT_PROBE_AT_PRODUCT;
    } else {
        mm_dbg ("(%s/%s) couldn't probe for product string",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));
        self->priv->product = NULL;
        self->priv->flags |= MM_PORT_PROBE_AT_PRODUCT;
    }
}

void
mm_port_probe_set_result_at_icera (MMPortProbe *self,
                                   gboolean is_icera)
{
    if (is_icera) {
        mm_dbg ("(%s/%s) Modem is Icera-based",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));
        self->priv->is_icera = TRUE;
        self->priv->flags |= MM_PORT_PROBE_AT_ICERA;
    } else {
        mm_dbg ("(%s/%s) Modem is probably not Icera-based",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));
        self->priv->is_icera = FALSE;
        self->priv->flags |= MM_PORT_PROBE_AT_ICERA;
    }
}

void
mm_port_probe_set_result_at_xmm (MMPortProbe *self,
                                 gboolean is_xmm)
{
    if (is_xmm) {
        mm_dbg ("(%s/%s) Modem is XMM-based",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));
        self->priv->is_xmm = TRUE;
        self->priv->flags |= MM_PORT_PROBE_AT_XMM;
    } else {
        mm_dbg ("(%s/%s) Modem is probably not XMM-based",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));
        self->priv->is_xmm = FALSE;
        self->priv->flags |= MM_PORT_PROBE_AT_XMM;
    }
}

void
mm_port_probe_set_result_qcdm (MMPortProbe *self,
                               gboolean qcdm)
{
    self->priv->is_qcdm = qcdm;
    self->priv->flags |= MM_PORT_PROBE_QCDM;

    if (self->priv->is_qcdm) {
        mm_dbg ("(%s/%s) port is QCDM-capable",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));

        /* Also set as not an AT/QMI/MBIM port */
        self->priv->is_at = FALSE;
        self->priv->is_qmi = FALSE;
        self->priv->is_mbim = FALSE;
        self->priv->vendor = NULL;
        self->priv->product = NULL;
        self->priv->is_icera = FALSE;
        self->priv->is_xmm = FALSE;
        self->priv->flags |= (MM_PORT_PROBE_AT |
                              MM_PORT_PROBE_AT_VENDOR |
                              MM_PORT_PROBE_AT_PRODUCT |
                              MM_PORT_PROBE_AT_ICERA |
                              MM_PORT_PROBE_AT_XMM |
                              MM_PORT_PROBE_QMI |
                              MM_PORT_PROBE_MBIM);
    } else
        mm_dbg ("(%s/%s) port is not QCDM-capable",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));
}

void
mm_port_probe_set_result_qmi (MMPortProbe *self,
                              gboolean qmi)
{
    self->priv->is_qmi = qmi;
    self->priv->flags |= MM_PORT_PROBE_QMI;

    if (self->priv->is_qmi) {
        mm_dbg ("(%s/%s) port is QMI-capable",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));

        /* Also set as not an AT/QCDM/MBIM port */
        self->priv->is_at = FALSE;
        self->priv->is_qcdm = FALSE;
        self->priv->is_mbim = FALSE;
        self->priv->vendor = NULL;
        self->priv->product = NULL;
        self->priv->flags |= (MM_PORT_PROBE_AT |
                              MM_PORT_PROBE_AT_VENDOR |
                              MM_PORT_PROBE_AT_PRODUCT |
                              MM_PORT_PROBE_AT_ICERA |
                              MM_PORT_PROBE_AT_XMM |
                              MM_PORT_PROBE_QCDM |
                              MM_PORT_PROBE_MBIM);
    } else
        mm_dbg ("(%s/%s) port is not QMI-capable",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));
}

void
mm_port_probe_set_result_mbim (MMPortProbe *self,
                               gboolean mbim)
{
    self->priv->is_mbim = mbim;
    self->priv->flags |= MM_PORT_PROBE_MBIM;

    if (self->priv->is_mbim) {
        mm_dbg ("(%s/%s) port is MBIM-capable",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));

        /* Also set as not an AT/QCDM/QMI port */
        self->priv->is_at = FALSE;
        self->priv->is_qcdm = FALSE;
        self->priv->is_qmi = FALSE;
        self->priv->vendor = NULL;
        self->priv->product = NULL;
        self->priv->flags |= (MM_PORT_PROBE_AT |
                              MM_PORT_PROBE_AT_VENDOR |
                              MM_PORT_PROBE_AT_PRODUCT |
                              MM_PORT_PROBE_AT_ICERA |
                              MM_PORT_PROBE_AT_XMM |
                              MM_PORT_PROBE_QCDM |
                              MM_PORT_PROBE_QMI);
    } else
        mm_dbg ("(%s/%s) port is not MBIM-capable",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));
}

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

typedef struct {
    /* ---- Generic task context ---- */
    guint32 flags;
    guint source_id;
    GCancellable *cancellable;

    /* ---- Serial probing specific context ---- */

    guint buffer_full_id;
    MMPortSerial *serial;

    /* ---- AT probing specific context ---- */

    GCancellable *at_probing_cancellable;
    gulong at_probing_cancellable_linked;
    /* Send delay for AT commands */
    guint64 at_send_delay;
    /* Flag to leave/remove echo in AT responses */
    gboolean at_remove_echo;
    /* Flag to send line-feed at the end of AT commands */
    gboolean at_send_lf;
    /* Number of times we tried to open the AT port */
    guint at_open_tries;
    /* Custom initialization setup */
    gboolean at_custom_init_run;
    MMPortProbeAtCustomInit at_custom_init;
    MMPortProbeAtCustomInitFinish at_custom_init_finish;
    /* Custom commands to look for AT support */
    const MMPortProbeAtCommand *at_custom_probe;
    /* Current group of AT commands to be sent */
    const MMPortProbeAtCommand *at_commands;
    /* Seconds between each AT command sent in the group */
    guint at_commands_wait_secs;
    /* Current AT Result processor */
    void (* at_result_processor) (MMPortProbe *self,
                                  GVariant *result);

#if defined WITH_QMI
    /* ---- QMI probing specific context ---- */
    MMPortQmi *port_qmi;
#endif

#if defined WITH_MBIM
    /* ---- MBIM probing specific context ---- */
    MMPortMbim *mbim_port;
#endif
} PortProbeRunContext;

static gboolean serial_probe_at       (MMPortProbe *self);
static gboolean serial_probe_qcdm     (MMPortProbe *self);
static void     serial_probe_schedule (MMPortProbe *self);

static void
port_probe_run_context_free (PortProbeRunContext *ctx)
{
    if (ctx->cancellable && ctx->at_probing_cancellable_linked) {
        g_cancellable_disconnect (ctx->cancellable, ctx->at_probing_cancellable_linked);
        ctx->at_probing_cancellable_linked = 0;
    }

    if (ctx->source_id) {
        g_source_remove (ctx->source_id);
        ctx->source_id = 0;
    }

    if (ctx->serial && ctx->buffer_full_id) {
        g_signal_handler_disconnect (ctx->serial, ctx->buffer_full_id);
        ctx->buffer_full_id = 0;
    }

    if (ctx->serial) {
        if (mm_port_serial_is_open (ctx->serial))
            mm_port_serial_close (ctx->serial);
        g_object_unref (ctx->serial);
    }

#if defined WITH_QMI
    if (ctx->port_qmi) {
        if (mm_port_qmi_is_open (ctx->port_qmi))
            mm_port_qmi_close (ctx->port_qmi);
        g_object_unref (ctx->port_qmi);
    }
#endif

#if defined WITH_MBIM
    if (ctx->mbim_port) {
        /* We should have closed it cleanly before */
        g_assert (!mm_port_mbim_is_open (ctx->mbim_port));
        g_object_unref (ctx->mbim_port);
    }
#endif

    if (ctx->at_probing_cancellable)
        g_object_unref (ctx->at_probing_cancellable);
    if (ctx->cancellable)
        g_object_unref (ctx->cancellable);

    g_slice_free (PortProbeRunContext, ctx);
}

/***************************************************************/
/* QMI & MBIM */

static gboolean wdm_probe (MMPortProbe *self);

#if defined WITH_QMI

static void
port_qmi_open_ready (MMPortQmi    *port_qmi,
                     GAsyncResult *res,
                     MMPortProbe  *self)
{
    GError              *error = NULL;
    PortProbeRunContext *ctx;
    gboolean             is_qmi;

    g_assert (self->priv->task);
    ctx = g_task_get_task_data (self->priv->task);

    is_qmi = mm_port_qmi_open_finish (port_qmi, res, &error);
    if (!is_qmi) {
        mm_dbg ("(%s/%s) error checking QMI support: '%s'",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port),
                error ? error->message : "unknown error");
        g_clear_error (&error);
    }

    /* Set probing result */
    mm_port_probe_set_result_qmi (self, is_qmi);
    mm_port_qmi_close (port_qmi);

    /* Keep on */
    ctx->source_id = g_idle_add ((GSourceFunc) wdm_probe, self);
}

#endif /* WITH_QMI */

static void
wdm_probe_qmi (MMPortProbe *self)
{
    PortProbeRunContext *ctx;

    g_assert (self->priv->task);
    ctx = g_task_get_task_data (self->priv->task);

#if defined WITH_QMI
    mm_dbg ("(%s/%s) probing QMI...",
            mm_kernel_device_get_subsystem (self->priv->port),
            mm_kernel_device_get_name (self->priv->port));

    /* Create a port and try to open it */
    ctx->port_qmi = mm_port_qmi_new (mm_kernel_device_get_name (self->priv->port));
    mm_port_qmi_open (ctx->port_qmi,
                      FALSE,
                      NULL,
                      (GAsyncReadyCallback) port_qmi_open_ready,
                      self);
#else
    /* If not compiled with QMI support, just assume we won't have any QMI port */
    mm_port_probe_set_result_qmi (self, FALSE);
    ctx->source_id = g_idle_add ((GSourceFunc) wdm_probe, self);
#endif /* WITH_QMI */
}

#if defined WITH_MBIM

static void
mbim_port_close_ready (MMPortMbim   *mbim_port,
                       GAsyncResult *res,
                       MMPortProbe  *self)
{
    PortProbeRunContext *ctx;

    g_assert (self->priv->task);
    ctx = g_task_get_task_data (self->priv->task);

    mm_port_mbim_close_finish (mbim_port, res, NULL);

    /* Keep on */
    ctx->source_id = g_idle_add ((GSourceFunc) wdm_probe, self);
}

static void
mbim_port_open_ready (MMPortMbim   *mbim_port,
                      GAsyncResult *res,
                      MMPortProbe  *self)
{
    GError              *error = NULL;
    PortProbeRunContext *ctx;
    gboolean             is_mbim;

    g_assert (self->priv->task);
    ctx = g_task_get_task_data (self->priv->task);

    is_mbim = mm_port_mbim_open_finish (mbim_port, res, &error);
    if (!is_mbim) {
        mm_dbg ("(%s/%s) error checking MBIM support: '%s'",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port),
                error ? error->message : "unknown error");
        g_clear_error (&error);
    }

    /* Set probing result */
    mm_port_probe_set_result_mbim (self, is_mbim);

    mm_port_mbim_close (ctx->mbim_port,
                        (GAsyncReadyCallback) mbim_port_close_ready,
                        self);
}

#endif /* WITH_MBIM */

static void
wdm_probe_mbim (MMPortProbe *self)
{
    PortProbeRunContext *ctx;

    g_assert (self->priv->task);
    ctx = g_task_get_task_data (self->priv->task);

#if defined WITH_MBIM
    mm_dbg ("(%s/%s) probing MBIM...",
            mm_kernel_device_get_subsystem (self->priv->port),
            mm_kernel_device_get_name (self->priv->port));

    /* Create a port and try to open it */
    ctx->mbim_port = mm_port_mbim_new (mm_kernel_device_get_name (self->priv->port));
    mm_port_mbim_open (ctx->mbim_port,
#if defined WITH_QMI && QMI_MBIM_QMUX_SUPPORTED
                       FALSE, /* Don't check QMI over MBIM support at this stage */
#endif
                       NULL,
                       (GAsyncReadyCallback) mbim_port_open_ready,
                       self);
#else
    /* If not compiled with MBIM support, just assume we won't have any MBIM port */
    mm_port_probe_set_result_mbim (self, FALSE);
    ctx->source_id = g_idle_add ((GSourceFunc) wdm_probe, self);
#endif /* WITH_MBIM */
}

static gboolean
wdm_probe (MMPortProbe *self)
{
    PortProbeRunContext *ctx;

    g_assert (self->priv->task);
    ctx = g_task_get_task_data (self->priv->task);
    ctx->source_id = 0;

    /* If already cancelled, do nothing else */
    if (port_probe_task_return_error_if_cancelled (self))
        return G_SOURCE_REMOVE;

    /* QMI probing needed? */
    if ((ctx->flags & MM_PORT_PROBE_QMI) &&
        !(self->priv->flags & MM_PORT_PROBE_QMI)) {
        wdm_probe_qmi (self);
        return G_SOURCE_REMOVE;
    }

    /* MBIM probing needed */
    if ((ctx->flags & MM_PORT_PROBE_MBIM) &&
        !(self->priv->flags & MM_PORT_PROBE_MBIM)) {
        wdm_probe_mbim (self);
        return G_SOURCE_REMOVE;
    }

    /* All done now */
    port_probe_task_return_boolean (self, TRUE);
    return G_SOURCE_REMOVE;
}

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

static void
common_serial_port_setup (MMPortProbe  *self,
                          MMPortSerial *serial)
{
    const gchar *flow_control_tag;

    if (mm_kernel_device_has_property (self->priv->port, ID_MM_TTY_BAUDRATE))
        g_object_set (serial,
                      MM_PORT_SERIAL_BAUD, mm_kernel_device_get_property_as_int (self->priv->port, ID_MM_TTY_BAUDRATE),
                      NULL);

    flow_control_tag = mm_kernel_device_get_property (self->priv->port, ID_MM_TTY_FLOW_CONTROL);
    if (flow_control_tag) {
        MMFlowControl flow_control;
        GError *error = NULL;

        flow_control = mm_flow_control_from_string (flow_control_tag, &error);
        if (flow_control == MM_FLOW_CONTROL_UNKNOWN) {
            mm_warn ("(%s/%s) Unsupported flow control settings in port: %s",
                     mm_kernel_device_get_subsystem (self->priv->port),
                     mm_kernel_device_get_name (self->priv->port),
                     error->message);
            g_error_free (error);
        } else {
            g_object_set (serial,
                          MM_PORT_SERIAL_FLOW_CONTROL, flow_control,
                          NULL);
        }
    }
}

/***************************************************************/
/* QCDM */

static void
serial_probe_qcdm_parse_response (MMPortSerialQcdm *port,
                                  GAsyncResult     *res,
                                  MMPortProbe      *self)
{
    QcdmResult          *result;
    gint                 err = QCDM_SUCCESS;
    gboolean             is_qcdm = FALSE;
    gboolean             retry = FALSE;
    GError              *error = NULL;
    GByteArray          *response;
    PortProbeRunContext *ctx;

    ctx = g_task_get_task_data (self->priv->task);

    /* If already cancelled, do nothing else */
    if (port_probe_task_return_error_if_cancelled (self))
        return;

    response = mm_port_serial_qcdm_command_finish (port, res, &error);
    if (!error) {
        /* Parse the response */
        result = qcdm_cmd_version_info_result ((const gchar *) response->data, response->len, &err);
        if (!result) {
            mm_warn ("(%s/%s) failed to parse QCDM version info command result: %d",
                     mm_kernel_device_get_subsystem (self->priv->port),
                     mm_kernel_device_get_name (self->priv->port),
                     err);
            retry = TRUE;
        } else {
            /* yay, probably a QCDM port */
            is_qcdm = TRUE;
            qcdm_result_unref (result);
        }
        g_byte_array_unref (response);
    } else if (g_error_matches (error, MM_SERIAL_ERROR, MM_SERIAL_ERROR_PARSE_FAILED)) {
        /* Failed to unescape QCDM packet: don't retry */
        mm_dbg ("QCDM parsing error: %s", error->message);
        g_error_free (error);
    } else {
        if (!g_error_matches (error, MM_SERIAL_ERROR, MM_SERIAL_ERROR_RESPONSE_TIMEOUT))
            mm_dbg ("QCDM probe error: (%d) %s", error->code, error->message);
        g_error_free (error);
        retry = TRUE;
    }

    if (retry) {
        GByteArray *cmd2;

        cmd2 = g_object_steal_data (G_OBJECT (self), "cmd2");
        if (cmd2) {
            /* second try */
            mm_port_serial_qcdm_command (MM_PORT_SERIAL_QCDM (ctx->serial),
                                         cmd2,
                                         3,
                                         NULL,
                                         (GAsyncReadyCallback) serial_probe_qcdm_parse_response,
                                         self);
            g_byte_array_unref (cmd2);
            return;
        }
        /* no more retries left */
    }

    /* Set probing result */
    mm_port_probe_set_result_qcdm (self, is_qcdm);
    /* Reschedule probing */
    serial_probe_schedule (self);
}

static gboolean
serial_probe_qcdm (MMPortProbe *self)
{
    GError              *error = NULL;
    GByteArray          *verinfo = NULL;
    GByteArray          *verinfo2;
    gint                 len;
    guint8               marker = 0x7E;
    PortProbeRunContext *ctx;

    g_assert (self->priv->task);
    ctx = g_task_get_task_data (self->priv->task);
    ctx->source_id = 0;

    /* If already cancelled, do nothing else */
    if (port_probe_task_return_error_if_cancelled (self))
        return G_SOURCE_REMOVE;

    mm_dbg ("(%s/%s) probing QCDM...",
            mm_kernel_device_get_subsystem (self->priv->port),
            mm_kernel_device_get_name (self->priv->port));

    /* If open, close the AT port */
    if (ctx->serial) {
        /* Explicitly clear the buffer full signal handler */
        if (ctx->buffer_full_id) {
            g_signal_handler_disconnect (ctx->serial, ctx->buffer_full_id);
            ctx->buffer_full_id = 0;
        }
        mm_port_serial_close (ctx->serial);
        g_object_unref (ctx->serial);
    }

    /* Open the QCDM port */
    ctx->serial = MM_PORT_SERIAL (mm_port_serial_qcdm_new (mm_kernel_device_get_name (self->priv->port)));
    if (!ctx->serial) {
        port_probe_task_return_error (self,
                                      g_error_new (MM_CORE_ERROR,
                                                   MM_CORE_ERROR_FAILED,
                                                   "(%s/%s) Couldn't create QCDM port",
                                                   mm_kernel_device_get_subsystem (self->priv->port),
                                                   mm_kernel_device_get_name (self->priv->port)));
        return G_SOURCE_REMOVE;
    }

    /* Setup port if needed */
    common_serial_port_setup (self, ctx->serial);

    /* Try to open the port */
    if (!mm_port_serial_open (ctx->serial, &error)) {
        port_probe_task_return_error (self,
                                      g_error_new (MM_SERIAL_ERROR,
                                                   MM_SERIAL_ERROR_OPEN_FAILED,
                                                   "(%s/%s) Failed to open QCDM port: %s",
                                                   mm_kernel_device_get_subsystem (self->priv->port),
                                                   mm_kernel_device_get_name (self->priv->port),
                                                   (error ? error->message : "unknown error")));
        g_clear_error (&error);
        return G_SOURCE_REMOVE;
    }

    /* Build up the probe command; 0x7E is the frame marker, so put one at the
     * beginning of the buffer to ensure that the device discards any AT
     * commands that probing might have sent earlier.  Should help devices
     * respond more quickly and speed up QCDM probing.
     */
    verinfo = g_byte_array_sized_new (10);
    g_byte_array_append (verinfo, &marker, 1);
    len = qcdm_cmd_version_info_new ((char *) (verinfo->data + 1), 9);
    if (len <= 0) {
        g_byte_array_unref (verinfo);
        port_probe_task_return_error (self,
                                      g_error_new (MM_SERIAL_ERROR,
                                                   MM_SERIAL_ERROR_OPEN_FAILED,
                                                   "(%s/%s) Failed to create QCDM version info command",
                                                   mm_kernel_device_get_subsystem (self->priv->port),
                                                   mm_kernel_device_get_name (self->priv->port)));
        return G_SOURCE_REMOVE;
    }
    verinfo->len = len + 1;

    /* Queuing the command takes ownership over it; save it for the second try */
    verinfo2 = g_byte_array_sized_new (verinfo->len);
    g_byte_array_append (verinfo2, verinfo->data, verinfo->len);
    g_object_set_data_full (G_OBJECT (self), "cmd2", verinfo2, (GDestroyNotify) g_byte_array_unref);

    mm_port_serial_qcdm_command (MM_PORT_SERIAL_QCDM (ctx->serial),
                                 verinfo,
                                 3,
                                 NULL,
                                 (GAsyncReadyCallback) serial_probe_qcdm_parse_response,
                                 self);
    g_byte_array_unref (verinfo);

    return G_SOURCE_REMOVE;
}

/***************************************************************/
/* AT */

static const gchar *non_at_strings[] = {
    /* Option Icera-based devices */
    "option/faema_",
    "os_logids.h",
    /* Sierra CnS port */
    "NETWORK SERVICE CHANGE",
    NULL
};

static const guint8 zerobuf[32] = {
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};

static gboolean
is_non_at_response (const guint8 *data, gsize len)
{
    const gchar **iter;
    gsize iter_len;
    gsize i;

    /* Some devices (observed on a ZTE branded "QUALCOMM INCORPORATED" model
     * "154") spew NULLs from some ports.
     */
    for (i = 0; (len >= sizeof (zerobuf)) && (i < len - sizeof (zerobuf)); i++) {
        if (!memcmp (&data[i], zerobuf, sizeof (zerobuf)))
            return TRUE;
    }

    /* Check for a well-known non-AT response.  There are some ports (eg many
     * Icera-based chipsets, Qualcomm Gobi devices before their firmware is
     * loaded, Sierra CnS ports) that just shouldn't be probed for AT capability
     * if we get a certain response since that response means they aren't AT
     * ports.  Also, kernel bugs (at least with 2.6.31 and 2.6.32) trigger port
     * flow control kernel oopses if we read too much data for these ports.
     */
    for (iter = &non_at_strings[0]; iter && *iter; iter++) {
        /* Search in the response for the item; the response could have embedded
         * nulls so we can't use memcmp() or strstr() on the whole response.
         */
        iter_len = strlen (*iter);
        for (i = 0; (len >= iter_len) && (i < len - iter_len); i++) {
            if (!memcmp (&data[i], *iter, iter_len))
                return TRUE;
        }
    }

    return FALSE;
}

static void
serial_probe_at_xmm_result_processor (MMPortProbe *self,
                                      GVariant *result)
{
    if (result) {
        /* If any result given, it must be a string */
        g_assert (g_variant_is_of_type (result, G_VARIANT_TYPE_STRING));
        if (strstr (g_variant_get_string (result, NULL), "XACT:")) {
            mm_port_probe_set_result_at_xmm (self, TRUE);
            return;
        }
    }

    mm_port_probe_set_result_at_xmm (self, FALSE);
}

static void
serial_probe_at_icera_result_processor (MMPortProbe *self,
                                        GVariant *result)
{
    if (result) {
        /* If any result given, it must be a string */
        g_assert (g_variant_is_of_type (result, G_VARIANT_TYPE_STRING));
        if (strstr (g_variant_get_string (result, NULL), "%IPSYS:")) {
            mm_port_probe_set_result_at_icera (self, TRUE);
            return;
        }
    }

    mm_port_probe_set_result_at_icera (self, FALSE);
}

static void
serial_probe_at_product_result_processor (MMPortProbe *self,
                                          GVariant *result)
{
    if (result) {
        /* If any result given, it must be a string */
        g_assert (g_variant_is_of_type (result, G_VARIANT_TYPE_STRING));
        mm_port_probe_set_result_at_product (self,
                                             g_variant_get_string (result, NULL));
        return;
    }

    mm_port_probe_set_result_at_product (self, NULL);
}

static void
serial_probe_at_vendor_result_processor (MMPortProbe *self,
                                         GVariant *result)
{
    if (result) {
        /* If any result given, it must be a string */
        g_assert (g_variant_is_of_type (result, G_VARIANT_TYPE_STRING));
        mm_port_probe_set_result_at_vendor (self,
                                            g_variant_get_string (result, NULL));
        return;
    }

    mm_port_probe_set_result_at_vendor (self, NULL);
}

static void
serial_probe_at_result_processor (MMPortProbe *self,
                                  GVariant *result)
{
    if (result) {
        /* If any result given, it must be a boolean */
        g_assert (g_variant_is_of_type (result, G_VARIANT_TYPE_BOOLEAN));

        if (g_variant_get_boolean (result)) {
            mm_port_probe_set_result_at (self, TRUE);
            return;
        }
    }

    mm_port_probe_set_result_at (self, FALSE);
}

static void
serial_probe_at_parse_response (MMPortSerialAt *port,
                                GAsyncResult   *res,
                                MMPortProbe    *self)
{
    GVariant            *result = NULL;
    GError              *result_error = NULL;
    const gchar         *response = NULL;
    GError              *error = NULL;
    PortProbeRunContext *ctx;

    g_assert (self->priv->task);
    ctx = g_task_get_task_data (self->priv->task);

    /* If already cancelled, do nothing else */
    if (port_probe_task_return_error_if_cancelled (self))
        return;

    /* If AT probing cancelled, end this partial probing */
    if (g_cancellable_is_cancelled (ctx->at_probing_cancellable)) {
        mm_dbg ("(%s/%s) no need to keep on probing the port for AT support",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));
        ctx->at_result_processor (self, NULL);
        serial_probe_schedule (self);
        return;
    }

    response = mm_port_serial_at_command_finish (port, res, &error);

    if (!ctx->at_commands->response_processor (ctx->at_commands->command,
                                               response,
                                               !!ctx->at_commands[1].command,
                                               error,
                                               &result,
                                               &result_error)) {
        /* Were we told to abort the whole probing? */
        if (result_error) {
            port_probe_task_return_error (self,
                                          g_error_new (MM_CORE_ERROR,
                                                       MM_CORE_ERROR_UNSUPPORTED,
                                                       "(%s/%s) error while probing AT features: %s",
                                                       mm_kernel_device_get_subsystem (self->priv->port),
                                                       mm_kernel_device_get_name (self->priv->port),
                                                       result_error->message));
            goto out;
        }

        /* Go on to next command */
        ctx->at_commands++;
        if (!ctx->at_commands->command) {
            /* Was it the last command in the group? If so,
             * end this partial probing */
            ctx->at_result_processor (self, NULL);
            /* Reschedule */
            serial_probe_schedule (self);
            goto out;
        }

        /* Schedule the next command in the probing group */
        if (ctx->at_commands_wait_secs == 0)
            ctx->source_id = g_idle_add ((GSourceFunc) serial_probe_at, self);
        else {
            mm_dbg ("(%s/%s) re-scheduling next command in probing group in %u seconds...",
                    mm_kernel_device_get_subsystem (self->priv->port),
                    mm_kernel_device_get_name (self->priv->port),
                    ctx->at_commands_wait_secs);
            ctx->source_id = g_timeout_add_seconds (ctx->at_commands_wait_secs, (GSourceFunc) serial_probe_at, self);
        }
        goto out;
    }

    /* Run result processor.
     * Note that custom init commands are allowed to not return anything */
    ctx->at_result_processor (self, result);

    /* Reschedule probing */
    serial_probe_schedule (self);

out:
    g_clear_pointer (&result, g_variant_unref);
    g_clear_error (&error);
    g_clear_error (&result_error);
}

static gboolean
serial_probe_at (MMPortProbe *self)
{
    PortProbeRunContext *ctx;

    g_assert (self->priv->task);
    ctx = g_task_get_task_data (self->priv->task);
    ctx->source_id = 0;

    /* If already cancelled, do nothing else */
    if (port_probe_task_return_error_if_cancelled (self))
        return G_SOURCE_REMOVE;

    /* If AT probing cancelled, end this partial probing */
    if (g_cancellable_is_cancelled (ctx->at_probing_cancellable)) {
        mm_dbg ("(%s/%s) no need to launch probing for AT support",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));
        ctx->at_result_processor (self, NULL);
        serial_probe_schedule (self);
        return G_SOURCE_REMOVE;
    }

    mm_port_serial_at_command (
        MM_PORT_SERIAL_AT (ctx->serial),
        ctx->at_commands->command,
        ctx->at_commands->timeout,
        FALSE,
        FALSE,
        ctx->at_probing_cancellable,
        (GAsyncReadyCallback)serial_probe_at_parse_response,
        self);
    return G_SOURCE_REMOVE;
}

static const MMPortProbeAtCommand at_probing[] = {
    { "AT",  3, mm_port_probe_response_processor_is_at },
    { "AT",  3, mm_port_probe_response_processor_is_at },
    { "AT",  3, mm_port_probe_response_processor_is_at },
    { NULL }
};

static const MMPortProbeAtCommand vendor_probing[] = {
    { "+CGMI", 3, mm_port_probe_response_processor_string },
    { "+GMI",  3, mm_port_probe_response_processor_string },
    { "I",     3, mm_port_probe_response_processor_string },
    { NULL }
};

static const MMPortProbeAtCommand product_probing[] = {
    { "+CGMM", 3, mm_port_probe_response_processor_string },
    { "+GMM",  3, mm_port_probe_response_processor_string },
    { "I",     3, mm_port_probe_response_processor_string },
    { NULL }
};

static const MMPortProbeAtCommand icera_probing[] = {
    { "%IPSYS?", 3, mm_port_probe_response_processor_string },
    { "%IPSYS?", 3, mm_port_probe_response_processor_string },
    { "%IPSYS?", 3, mm_port_probe_response_processor_string },
    { NULL }
};

static const MMPortProbeAtCommand xmm_probing[] = {
    { "+XACT=?", 3, mm_port_probe_response_processor_string },
    { NULL }
};

static void
at_custom_init_ready (MMPortProbe *self,
                      GAsyncResult *res)
{
    GError              *error = NULL;
    PortProbeRunContext *ctx;

    g_assert (self->priv->task);
    ctx = g_task_get_task_data (self->priv->task);

    if (!ctx->at_custom_init_finish (self, res, &error)) {
        /* All errors propagated up end up forcing an UNSUPPORTED result */
        port_probe_task_return_error (self, error);
        return;
    }

    /* Keep on with remaining probings */
    ctx->at_custom_init_run = TRUE;
    serial_probe_schedule (self);
}

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

static void
serial_probe_schedule (MMPortProbe *self)
{
    PortProbeRunContext *ctx;

    g_assert (self->priv->task);
    ctx = g_task_get_task_data (self->priv->task);

    /* If already cancelled, do nothing else */
    if (port_probe_task_return_error_if_cancelled (self))
        return;

    /* If we got some custom initialization setup requested, go on with it
     * first. We completely ignore the custom initialization if the serial port
     * that we receive in the context isn't an AT port (e.g. if it was flagged
     * as not being an AT port early) */
    if (!ctx->at_custom_init_run &&
        ctx->at_custom_init &&
        ctx->at_custom_init_finish &&
        MM_IS_PORT_SERIAL_AT (ctx->serial)) {
        ctx->at_custom_init (self,
                             MM_PORT_SERIAL_AT (ctx->serial),
                             ctx->at_probing_cancellable,
                             (GAsyncReadyCallback) at_custom_init_ready,
                             NULL);
        return;
    }

    /* Cleanup */
    ctx->at_result_processor   = NULL;
    ctx->at_commands           = NULL;
    ctx->at_commands_wait_secs = 0;

    /* AT check requested and not already probed? */
    if ((ctx->flags & MM_PORT_PROBE_AT) &&
        !(self->priv->flags & MM_PORT_PROBE_AT)) {
        /* Prepare AT probing */
        if (ctx->at_custom_probe)
            ctx->at_commands = ctx->at_custom_probe;
        else
            ctx->at_commands = at_probing;
        ctx->at_result_processor = serial_probe_at_result_processor;
    }
    /* Vendor requested and not already probed? */
    else if ((ctx->flags & MM_PORT_PROBE_AT_VENDOR) &&
        !(self->priv->flags & MM_PORT_PROBE_AT_VENDOR)) {
        /* Prepare AT vendor probing */
        ctx->at_result_processor = serial_probe_at_vendor_result_processor;
        ctx->at_commands = vendor_probing;
    }
    /* Product requested and not already probed? */
    else if ((ctx->flags & MM_PORT_PROBE_AT_PRODUCT) &&
             !(self->priv->flags & MM_PORT_PROBE_AT_PRODUCT)) {
        /* Prepare AT product probing */
        ctx->at_result_processor = serial_probe_at_product_result_processor;
        ctx->at_commands = product_probing;
    }
    /* Icera support check requested and not already done? */
    else if ((ctx->flags & MM_PORT_PROBE_AT_ICERA) &&
             !(self->priv->flags & MM_PORT_PROBE_AT_ICERA)) {
        /* Prepare AT product probing */
        ctx->at_result_processor = serial_probe_at_icera_result_processor;
        ctx->at_commands = icera_probing;
        /* By default, wait 2 seconds between ICERA probing retries */
        ctx->at_commands_wait_secs = 2;
    }
    /* XMM support check requested and not already done? */
    else if ((ctx->flags & MM_PORT_PROBE_AT_XMM) &&
             !(self->priv->flags & MM_PORT_PROBE_AT_XMM)) {
        /* Prepare AT product probing */
        ctx->at_result_processor = serial_probe_at_xmm_result_processor;
        ctx->at_commands = xmm_probing;
    }

    /* If a next AT group detected, go for it */
    if (ctx->at_result_processor &&
        ctx->at_commands) {
        ctx->source_id = g_idle_add ((GSourceFunc) serial_probe_at, self);
        return;
    }

    /* QCDM requested and not already probed? */
    if ((ctx->flags & MM_PORT_PROBE_QCDM) &&
        !(self->priv->flags & MM_PORT_PROBE_QCDM)) {
        ctx->source_id = g_idle_add ((GSourceFunc) serial_probe_qcdm, self);
        return;
    }

    /* All done! */
    port_probe_task_return_boolean (self, TRUE);
}

static void
serial_flash_ready (MMPortSerial *port,
                    GAsyncResult *res,
                    MMPortProbe *self)
{
    mm_port_serial_flash_finish (port, res, NULL);

    /* Schedule probing */
    serial_probe_schedule (self);
}

static void
serial_buffer_full (MMPortSerial *serial,
                    GByteArray   *buffer,
                    MMPortProbe  *self)
{
    PortProbeRunContext *ctx;

    if (!is_non_at_response (buffer->data, buffer->len))
        return;

    g_assert (self->priv->task);
    ctx = g_task_get_task_data (self->priv->task);

    mm_dbg ("(%s/%s) serial buffer full",
            mm_kernel_device_get_subsystem (self->priv->port),
            mm_kernel_device_get_name (self->priv->port));
    /* Don't explicitly close the AT port, just end the AT probing
     * (or custom init probing) */
    mm_port_probe_set_result_at (self, FALSE);
    g_cancellable_cancel (ctx->at_probing_cancellable);
}

static gboolean
serial_parser_filter_cb (gpointer   filter,
                         gpointer   user_data,
                         GString   *response,
                         GError   **error)
{
    if (is_non_at_response ((const guint8 *) response->str, response->len)) {
        g_set_error (error,
                     MM_SERIAL_ERROR,
                     MM_SERIAL_ERROR_PARSE_FAILED,
                     "Not an AT response");
        return FALSE;
    }

    return TRUE;
}

static gboolean
serial_open_at (MMPortProbe *self)
{
    GError              *error = NULL;
    PortProbeRunContext *ctx;

    g_assert (self->priv->task);
    ctx = g_task_get_task_data (self->priv->task);
    ctx->source_id = 0;

    /* If already cancelled, do nothing else */
    if (port_probe_task_return_error_if_cancelled (self))
        return G_SOURCE_REMOVE;

    /* Create AT serial port if not done before */
    if (!ctx->serial) {
        gpointer parser;
        MMPortSubsys subsys = MM_PORT_SUBSYS_TTY;

        if (g_str_has_prefix (mm_kernel_device_get_subsystem (self->priv->port), "usb"))
            subsys = MM_PORT_SUBSYS_USB;

        ctx->serial = MM_PORT_SERIAL (mm_port_serial_at_new (mm_kernel_device_get_name (self->priv->port), subsys));
        if (!ctx->serial) {
            port_probe_task_return_error (self,
                                          g_error_new (MM_CORE_ERROR,
                                                       MM_CORE_ERROR_FAILED,
                                                       "(%s/%s) couldn't create AT port",
                                                       mm_kernel_device_get_subsystem (self->priv->port),
                                                       mm_kernel_device_get_name (self->priv->port)));
            return G_SOURCE_REMOVE;
        }

        g_object_set (ctx->serial,
                      MM_PORT_SERIAL_SPEW_CONTROL,   TRUE,
                      MM_PORT_SERIAL_SEND_DELAY,     (guint64)(subsys == MM_PORT_SUBSYS_TTY ? ctx->at_send_delay : 0),
                      MM_PORT_SERIAL_AT_REMOVE_ECHO, ctx->at_remove_echo,
                      MM_PORT_SERIAL_AT_SEND_LF,     ctx->at_send_lf,
                      NULL);

        common_serial_port_setup (self, ctx->serial);

        parser = mm_serial_parser_v1_new ();
        mm_serial_parser_v1_add_filter (parser,
                                        serial_parser_filter_cb,
                                        NULL);
        mm_port_serial_at_set_response_parser (MM_PORT_SERIAL_AT (ctx->serial),
                                               mm_serial_parser_v1_parse,
                                               parser,
                                               mm_serial_parser_v1_destroy);
    }

    /* Try to open the port */
    if (!mm_port_serial_open (ctx->serial, &error)) {
        /* Abort if maximum number of open tries reached */
        if (++ctx->at_open_tries > 4) {
            /* took too long to open the port; give up */
            port_probe_task_return_error (self,
                                          g_error_new (MM_CORE_ERROR,
                                                       MM_CORE_ERROR_FAILED,
                                                       "(%s/%s) failed to open port after 4 tries",
                                                       mm_kernel_device_get_subsystem (self->priv->port),
                                                       mm_kernel_device_get_name (self->priv->port)));
            g_clear_error (&error);
            return G_SOURCE_REMOVE;
        }

        if (g_error_matches (error, MM_SERIAL_ERROR, MM_SERIAL_ERROR_OPEN_FAILED_NO_DEVICE)) {
            /* this is nozomi being dumb; try again */
            ctx->source_id = g_timeout_add_seconds (1, (GSourceFunc) serial_open_at, self);
            g_clear_error (&error);
            return G_SOURCE_REMOVE;
        }

        port_probe_task_return_error (self,
                                      g_error_new (MM_SERIAL_ERROR,
                                                   MM_SERIAL_ERROR_OPEN_FAILED,
                                                   "(%s/%s) failed to open port: %s",
                                                   mm_kernel_device_get_subsystem (self->priv->port),
                                                   mm_kernel_device_get_name (self->priv->port),
                                                   (error ? error->message : "unknown error")));
        g_clear_error (&error);
        return G_SOURCE_REMOVE;
    }

    /* success, start probing */
    ctx->buffer_full_id = g_signal_connect (ctx->serial, "buffer-full",
                                            G_CALLBACK (serial_buffer_full), self);
    mm_port_serial_flash (MM_PORT_SERIAL (ctx->serial),
                          100,
                          TRUE,
                          (GAsyncReadyCallback) serial_flash_ready,
                          self);
    return G_SOURCE_REMOVE;
}

static void
at_cancellable_cancel (GCancellable        *cancellable,
                       PortProbeRunContext *ctx)
{
    /* Avoid trying to disconnect cancellable on the handler, or we'll deadlock */
    ctx->at_probing_cancellable_linked = 0;
    g_cancellable_cancel (ctx->at_probing_cancellable);
}

gboolean
mm_port_probe_run_cancel_at_probing (MMPortProbe *self)
{
    PortProbeRunContext *ctx;

    g_return_val_if_fail (MM_IS_PORT_PROBE (self), FALSE);

    if (!self->priv->task)
        return FALSE;

    ctx = g_task_get_task_data (self->priv->task);
    if (g_cancellable_is_cancelled (ctx->at_probing_cancellable))
        return FALSE;

    mm_dbg ("(%s/%s) requested to cancel all AT probing",
            mm_kernel_device_get_subsystem (self->priv->port),
            mm_kernel_device_get_name (self->priv->port));
    g_cancellable_cancel (ctx->at_probing_cancellable);
    return TRUE;
}

gboolean
mm_port_probe_run_finish (MMPortProbe   *self,
                          GAsyncResult  *result,
                          GError       **error)
{
    g_return_val_if_fail (MM_IS_PORT_PROBE (self), FALSE);
    g_return_val_if_fail (G_IS_TASK (result), FALSE);

    return g_task_propagate_boolean (G_TASK (result), error);
}

void
mm_port_probe_run (MMPortProbe                *self,
                   MMPortProbeFlag             flags,
                   guint64                     at_send_delay,
                   gboolean                    at_remove_echo,
                   gboolean                    at_send_lf,
                   const MMPortProbeAtCommand *at_custom_probe,
                   const MMAsyncMethod        *at_custom_init,
                   GCancellable               *cancellable,
                   GAsyncReadyCallback         callback,
                   gpointer                    user_data)
{
    PortProbeRunContext *ctx;
    gchar               *probe_list_str;
    guint32              i;

    g_return_if_fail (MM_IS_PORT_PROBE (self));
    g_return_if_fail (flags != MM_PORT_PROBE_NONE);
    g_return_if_fail (callback != NULL);

    /* Shouldn't schedule more than one probing at a time */
    g_assert (self->priv->task == NULL);
    self->priv->task = g_task_new (self, cancellable, callback, user_data);

    /* Task context */
    ctx = g_slice_new0 (PortProbeRunContext);
    ctx->at_send_delay = at_send_delay;
    ctx->at_remove_echo = at_remove_echo;
    ctx->at_send_lf = at_send_lf;
    ctx->flags = MM_PORT_PROBE_NONE;
    ctx->at_custom_probe = at_custom_probe;
    ctx->at_custom_init = at_custom_init ? (MMPortProbeAtCustomInit)at_custom_init->async : NULL;
    ctx->at_custom_init_finish = at_custom_init ? (MMPortProbeAtCustomInitFinish)at_custom_init->finish : NULL;
    ctx->cancellable = cancellable ? g_object_ref (cancellable) : NULL;

    /* The context will be owned by the task */
    g_task_set_task_data (self->priv->task, ctx, (GDestroyNotify) port_probe_run_context_free);

    /* If we're told to completely ignore the port, don't do any probing */
    if (self->priv->is_ignored) {
        mm_dbg ("(%s/%s) port probing finished: skipping for blacklisted port",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));
        port_probe_task_return_boolean (self, TRUE);
        return;
    }

    /* If this is a port flagged as a GPS port, don't do any AT or QCDM probing */
    if (self->priv->is_gps) {
        mm_dbg ("(%s/%s) GPS port detected",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));
        mm_port_probe_set_result_at (self, FALSE);
        mm_port_probe_set_result_qcdm (self, FALSE);
    }

    /* If this is a port flagged as an audio port, don't do any AT or QCDM probing */
    if (self->priv->is_audio) {
        mm_dbg ("(%s/%s) audio port detected",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));
        mm_port_probe_set_result_at (self, FALSE);
        mm_port_probe_set_result_qcdm (self, FALSE);
    }

    /* If this is a port flagged as being an AT port, don't do any QCDM probing */
    if (self->priv->maybe_at_primary || self->priv->maybe_at_secondary || self->priv->maybe_at_ppp) {
        mm_dbg ("(%s/%s) no QCDM probing in possible AT port",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));
        mm_port_probe_set_result_qcdm (self, FALSE);
    }

    /* If this is a port flagged as being a QCDM port, don't do any AT probing */
    if (self->priv->maybe_qcdm) {
        mm_dbg ("(%s/%s) no AT probing in possible QCDM port",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));
        mm_port_probe_set_result_at (self, FALSE);
    }

    /* Check if we already have the requested probing results.
     * We will fix here the 'ctx->flags' so that we only request probing
     * for the missing things. */
    for (i = MM_PORT_PROBE_AT; i <= MM_PORT_PROBE_MBIM; i = (i << 1)) {
        if ((flags & i) && !(self->priv->flags & i))
            ctx->flags += i;
    }

    /* All requested probings already available? If so, we're done */
    if (!ctx->flags) {
        mm_dbg ("(%s/%s) port probing finished: no more probings needed",
                mm_kernel_device_get_subsystem (self->priv->port),
                mm_kernel_device_get_name (self->priv->port));
        port_probe_task_return_boolean (self, TRUE);
        return;
    }

    /* Log the probes scheduled to be run */
    probe_list_str = mm_port_probe_flag_build_string_from_mask (ctx->flags);
    mm_dbg ("(%s/%s) launching port probing: '%s'",
            mm_kernel_device_get_subsystem (self->priv->port),
            mm_kernel_device_get_name (self->priv->port),
            probe_list_str);
    g_free (probe_list_str);

    /* If any AT probing is needed, start by opening as AT port */
    if (ctx->flags & MM_PORT_PROBE_AT ||
        ctx->flags & MM_PORT_PROBE_AT_VENDOR ||
        ctx->flags & MM_PORT_PROBE_AT_PRODUCT ||
        ctx->flags & MM_PORT_PROBE_AT_ICERA ||
        ctx->flags & MM_PORT_PROBE_AT_XMM) {
        ctx->at_probing_cancellable = g_cancellable_new ();
        /* If the main cancellable is cancelled, so will be the at-probing one */
        if (cancellable)
            ctx->at_probing_cancellable_linked = g_cancellable_connect (cancellable,
                                                                        (GCallback) at_cancellable_cancel,
                                                                        ctx,
                                                                        NULL);
        ctx->source_id = g_idle_add ((GSourceFunc) serial_open_at, self);
        return;
    }

    /* If QCDM probing needed, start by opening as QCDM port */
    if (ctx->flags & MM_PORT_PROBE_QCDM) {
        ctx->source_id = g_idle_add ((GSourceFunc) serial_probe_qcdm, self);
        return;
    }

    /* If QMI/MBIM probing needed, go on */
    if (ctx->flags & MM_PORT_PROBE_QMI || ctx->flags & MM_PORT_PROBE_MBIM) {
        ctx->source_id = g_idle_add ((GSourceFunc) wdm_probe, self);
        return;
    }

    /* Shouldn't happen */
    g_assert_not_reached ();
}

gboolean
mm_port_probe_is_at (MMPortProbe *self)
{
    const gchar *subsys;

    g_return_val_if_fail (MM_IS_PORT_PROBE (self), FALSE);

    subsys = mm_kernel_device_get_subsystem (self->priv->port);
    if (g_str_equal (subsys, "net"))
        return FALSE;

    return (self->priv->flags & MM_PORT_PROBE_AT ?
            self->priv->is_at :
            FALSE);
}

gboolean
mm_port_probe_list_has_at_port (GList *list)
{
    GList *l;

    for (l = list; l; l = g_list_next (l)){
        MMPortProbe *probe = MM_PORT_PROBE (l->data);

        if (!probe->priv->is_ignored &&
            probe->priv->flags & MM_PORT_PROBE_AT &&
            probe->priv->is_at)
            return TRUE;
    }

    return FALSE;
}

gboolean
mm_port_probe_is_qcdm (MMPortProbe *self)
{
    const gchar *subsys;
    const gchar *name;

    g_return_val_if_fail (MM_IS_PORT_PROBE (self), FALSE);

    subsys = mm_kernel_device_get_subsystem (self->priv->port);
    name = mm_kernel_device_get_name (self->priv->port);
    if (g_str_equal (subsys, "net") ||
        (g_str_has_prefix (subsys, "usb") &&
         g_str_has_prefix (name, "cdc-wdm")))
        return FALSE;

    return (self->priv->flags & MM_PORT_PROBE_QCDM ?
            self->priv->is_qcdm :
            FALSE);
}

gboolean
mm_port_probe_is_qmi (MMPortProbe *self)
{
    const gchar *subsys;
    const gchar *name;

    g_return_val_if_fail (MM_IS_PORT_PROBE (self), FALSE);

    subsys = mm_kernel_device_get_subsystem (self->priv->port);
    name = mm_kernel_device_get_name (self->priv->port);
    if (!g_str_has_prefix (subsys, "usb") ||
        !name ||
        !g_str_has_prefix (name, "cdc-wdm"))
        return FALSE;

    return self->priv->is_qmi;
}

gboolean
mm_port_probe_list_has_qmi_port (GList *list)
{
    GList *l;

    for (l = list; l; l = g_list_next (l)) {
        MMPortProbe *probe = MM_PORT_PROBE (l->data);

        if (!probe->priv->is_ignored &&
            mm_port_probe_is_qmi (probe))
            return TRUE;
    }

    return FALSE;
}

gboolean
mm_port_probe_is_mbim (MMPortProbe *self)
{
    const gchar *subsys;
    const gchar *name;

    g_return_val_if_fail (MM_IS_PORT_PROBE (self), FALSE);

    subsys = mm_kernel_device_get_subsystem (self->priv->port);
    name = mm_kernel_device_get_name (self->priv->port);
    if (!g_str_has_prefix (subsys, "usb") ||
        !name ||
        !g_str_has_prefix (name, "cdc-wdm"))
        return FALSE;

    return self->priv->is_mbim;
}

gboolean
mm_port_probe_list_has_mbim_port (GList *list)
{
    GList *l;

    for (l = list; l; l = g_list_next (l)) {
        MMPortProbe *probe = MM_PORT_PROBE (l->data);

        if (!probe->priv->is_ignored &&
            mm_port_probe_is_mbim (probe))
            return TRUE;
    }

    return FALSE;
}

MMPortType
mm_port_probe_get_port_type (MMPortProbe *self)
{
    const gchar *subsys;

    g_return_val_if_fail (MM_IS_PORT_PROBE (self), FALSE);

    subsys = mm_kernel_device_get_subsystem (self->priv->port);

    if (g_str_equal (subsys, "net"))
        return MM_PORT_TYPE_NET;

    if (g_str_has_prefix (subsys, "usb")) {
        const gchar *name;

        name = mm_kernel_device_get_name (self->priv->port);
        if (g_str_has_prefix (name, "cdc-wdm")) {
#if defined WITH_QMI
            if (self->priv->is_qmi)
                return MM_PORT_TYPE_QMI;
#endif
#if defined WITH_MBIM
            if (self->priv->is_mbim)
                return MM_PORT_TYPE_MBIM;
#endif
        }
    }

    if (self->priv->flags & MM_PORT_PROBE_QCDM &&
        self->priv->is_qcdm)
        return MM_PORT_TYPE_QCDM;

    if (self->priv->flags & MM_PORT_PROBE_AT &&
        self->priv->is_at)
        return MM_PORT_TYPE_AT;

    if (self->priv->is_gps)
        return MM_PORT_TYPE_GPS;

    if (self->priv->is_audio)
        return MM_PORT_TYPE_AUDIO;

    return MM_PORT_TYPE_UNKNOWN;
}

MMDevice *
mm_port_probe_peek_device (MMPortProbe *self)
{
    g_return_val_if_fail (MM_IS_PORT_PROBE (self), NULL);

    return self->priv->device;
}

MMDevice *
mm_port_probe_get_device (MMPortProbe *self)
{
    g_return_val_if_fail (MM_IS_PORT_PROBE (self), NULL);

    return MM_DEVICE (g_object_ref (self->priv->device));
}

MMKernelDevice *
mm_port_probe_peek_port (MMPortProbe *self)
{
    g_return_val_if_fail (MM_IS_PORT_PROBE (self), NULL);

    return self->priv->port;
};

MMKernelDevice *
mm_port_probe_get_port (MMPortProbe *self)
{
    g_return_val_if_fail (MM_IS_PORT_PROBE (self), NULL);

    return MM_KERNEL_DEVICE (g_object_ref (self->priv->port));
};

const gchar *
mm_port_probe_get_vendor (MMPortProbe *self)
{
    const gchar *subsys;
    const gchar *name;

    g_return_val_if_fail (MM_IS_PORT_PROBE (self), FALSE);

    subsys = mm_kernel_device_get_subsystem (self->priv->port);
    name = mm_kernel_device_get_name (self->priv->port);
    if (g_str_equal (subsys, "net") ||
        (g_str_has_prefix (subsys, "usb") &&
         g_str_has_prefix (name, "cdc-wdm")))
        return NULL;

    return (self->priv->flags & MM_PORT_PROBE_AT_VENDOR ?
            self->priv->vendor :
            NULL);
}

const gchar *
mm_port_probe_get_product (MMPortProbe *self)
{
    const gchar *subsys;
    const gchar *name;

    g_return_val_if_fail (MM_IS_PORT_PROBE (self), FALSE);

    subsys = mm_kernel_device_get_subsystem (self->priv->port);
    name = mm_kernel_device_get_name (self->priv->port);
    if (g_str_equal (subsys, "net") ||
        (g_str_has_prefix (subsys, "usb") &&
         g_str_has_prefix (name, "cdc-wdm")))
        return NULL;

    return (self->priv->flags & MM_PORT_PROBE_AT_PRODUCT ?
            self->priv->product :
            NULL);
}

gboolean
mm_port_probe_is_icera (MMPortProbe *self)
{
    g_return_val_if_fail (MM_IS_PORT_PROBE (self), FALSE);

    if (g_str_equal (mm_kernel_device_get_subsystem (self->priv->port), "net"))
        return FALSE;

    return (self->priv->flags & MM_PORT_PROBE_AT_ICERA ?
            self->priv->is_icera :
            FALSE);
}

gboolean
mm_port_probe_list_is_icera (GList *probes)
{
    GList *l;

    for (l = probes; l; l = g_list_next (l)) {
        if (mm_port_probe_is_icera (MM_PORT_PROBE (l->data)))
            return TRUE;
    }

    return FALSE;
}

gboolean
mm_port_probe_is_xmm (MMPortProbe *self)
{
    g_return_val_if_fail (MM_IS_PORT_PROBE (self), FALSE);

    if (g_str_equal (mm_kernel_device_get_subsystem (self->priv->port), "net"))
        return FALSE;

    return (self->priv->flags & MM_PORT_PROBE_AT_XMM ?
            self->priv->is_xmm :
            FALSE);
}

gboolean
mm_port_probe_list_is_xmm (GList *probes)
{
    GList *l;

    for (l = probes; l; l = g_list_next (l)) {
        if (mm_port_probe_is_xmm (MM_PORT_PROBE (l->data)))
            return TRUE;
    }

    return FALSE;
}

gboolean
mm_port_probe_is_ignored (MMPortProbe *self)
{
    g_return_val_if_fail (MM_IS_PORT_PROBE (self), FALSE);

    return self->priv->is_ignored;
}

const gchar *
mm_port_probe_get_port_name (MMPortProbe *self)
{
    g_return_val_if_fail (MM_IS_PORT_PROBE (self), NULL);

    return mm_kernel_device_get_name (self->priv->port);
}

const gchar *
mm_port_probe_get_port_subsys (MMPortProbe *self)
{
    g_return_val_if_fail (MM_IS_PORT_PROBE (self), NULL);

    return mm_kernel_device_get_subsystem (self->priv->port);
}

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

MMPortProbe *
mm_port_probe_new (MMDevice       *device,
                   MMKernelDevice *port)
{
    return MM_PORT_PROBE (g_object_new (MM_TYPE_PORT_PROBE,
                                        MM_PORT_PROBE_DEVICE, device,
                                        MM_PORT_PROBE_PORT,   port,
                                        NULL));
}

static void
mm_port_probe_init (MMPortProbe *self)
{
    self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
                                              MM_TYPE_PORT_PROBE,
                                              MMPortProbePrivate);
}

static void
set_property (GObject *object,
              guint prop_id,
              const GValue *value,
              GParamSpec *pspec)
{
    MMPortProbe *self = MM_PORT_PROBE (object);

    switch (prop_id) {
    case PROP_DEVICE:
        /* construct only, no new reference! */
        self->priv->device = g_value_get_object (value);
        break;
    case PROP_PORT:
        /* construct only */
        self->priv->port = g_value_dup_object (value);
        self->priv->is_ignored = mm_kernel_device_get_property_as_boolean (self->priv->port, ID_MM_PORT_IGNORE);
        self->priv->is_gps = mm_kernel_device_get_property_as_boolean (self->priv->port, ID_MM_PORT_TYPE_GPS);
        self->priv->is_audio = mm_kernel_device_get_property_as_boolean (self->priv->port, ID_MM_PORT_TYPE_AUDIO);
        self->priv->maybe_at_primary = mm_kernel_device_get_property_as_boolean (self->priv->port, ID_MM_PORT_TYPE_AT_PRIMARY);
        self->priv->maybe_at_secondary = mm_kernel_device_get_property_as_boolean (self->priv->port, ID_MM_PORT_TYPE_AT_SECONDARY);
        self->priv->maybe_at_ppp = mm_kernel_device_get_property_as_boolean (self->priv->port, ID_MM_PORT_TYPE_AT_PPP);
        self->priv->maybe_qcdm = mm_kernel_device_get_property_as_boolean (self->priv->port, ID_MM_PORT_TYPE_QCDM);
        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)
{
    MMPortProbe *self = MM_PORT_PROBE (object);

    switch (prop_id) {
    case PROP_DEVICE:
        g_value_set_object (value, self->priv->device);
        break;
    case PROP_PORT:
        g_value_set_object (value, self->priv->port);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static void
finalize (GObject *object)
{
    MMPortProbe *self = MM_PORT_PROBE (object);

    /* We should never have a task here */
    g_assert (self->priv->task == NULL);

    g_free (self->priv->vendor);
    g_free (self->priv->product);

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

static void
dispose (GObject *object)
{
    MMPortProbe *self = MM_PORT_PROBE (object);

    /* We didn't get a reference to the device */
    self->priv->device = NULL;

    g_clear_object (&self->priv->port);

    G_OBJECT_CLASS (mm_port_probe_parent_class)->dispose (object);
}

static void
mm_port_probe_class_init (MMPortProbeClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS (klass);

    g_type_class_add_private (object_class, sizeof (MMPortProbePrivate));

    /* Virtual methods */
    object_class->get_property = get_property;
    object_class->set_property = set_property;
    object_class->finalize = finalize;
    object_class->dispose = dispose;

    properties[PROP_DEVICE] =
        g_param_spec_object (MM_PORT_PROBE_DEVICE,
                             "Device",
                             "Device owning this probe",
                             MM_TYPE_DEVICE,
                             G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
    g_object_class_install_property (object_class, PROP_DEVICE, properties[PROP_DEVICE]);

    properties[PROP_PORT] =
        g_param_spec_object (MM_PORT_PROBE_PORT,
                             "Port",
                             "kernel device object of the port",
                             MM_TYPE_KERNEL_DEVICE,
                             G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
    g_object_class_install_property (object_class, PROP_PORT, properties[PROP_PORT]);
}
