
fwupd expects device IDs which are unique for each device and its variants. However, Quectel re-uses the same USB VID & PID among different variants such as EG25, EC25, EC20, etc. Moreover, each variant may have subvariants such as EG25GGB, EG25GGC, EG25AFF, EG25AFX, etc. Add the name of the modem to the device IDs to build more unique device IDs such as USB\VID_2C7C&PID_0125&REV_0001&NAME_EC25GGB.
994 lines
35 KiB
C
994 lines
35 KiB
C
/* -*- 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) 2018-2020 Aleksander Morgado <aleksander@aleksander.es>
|
|
*/
|
|
|
|
#include <config.h>
|
|
|
|
#include <glib-object.h>
|
|
#include <gio/gio.h>
|
|
|
|
#define _LIBMM_INSIDE_MM
|
|
#include <libmm-glib.h>
|
|
|
|
#include "mm-log-object.h"
|
|
#include "mm-iface-modem.h"
|
|
#include "mm-iface-modem-firmware.h"
|
|
#include "mm-iface-modem-location.h"
|
|
#include "mm-base-modem.h"
|
|
#include "mm-base-modem-at.h"
|
|
#include "mm-shared-quectel.h"
|
|
#include "mm-modem-helpers-quectel.h"
|
|
|
|
#if defined WITH_MBIM
|
|
#include "mm-broadband-modem-mbim.h"
|
|
#endif
|
|
|
|
/*****************************************************************************/
|
|
/* Private context */
|
|
|
|
#define PRIVATE_TAG "shared-quectel-private-tag"
|
|
static GQuark private_quark;
|
|
|
|
typedef enum {
|
|
FEATURE_SUPPORT_UNKNOWN,
|
|
FEATURE_NOT_SUPPORTED,
|
|
FEATURE_SUPPORTED,
|
|
} FeatureSupport;
|
|
|
|
typedef struct {
|
|
MMBroadbandModemClass *broadband_modem_class_parent;
|
|
MMIfaceModem *iface_modem_parent;
|
|
MMIfaceModemLocation *iface_modem_location_parent;
|
|
MMModemLocationSource provided_sources;
|
|
MMModemLocationSource enabled_sources;
|
|
FeatureSupport qgps_supported;
|
|
GRegex *qgpsurc_regex;
|
|
GRegex *qlwurc_regex;
|
|
} Private;
|
|
|
|
static void
|
|
private_free (Private *priv)
|
|
{
|
|
g_regex_unref (priv->qgpsurc_regex);
|
|
g_regex_unref (priv->qlwurc_regex);
|
|
g_slice_free (Private, priv);
|
|
}
|
|
|
|
static Private *
|
|
get_private (MMSharedQuectel *self)
|
|
{
|
|
Private *priv;
|
|
|
|
if (G_UNLIKELY (!private_quark))
|
|
private_quark = g_quark_from_static_string (PRIVATE_TAG);
|
|
|
|
priv = g_object_get_qdata (G_OBJECT (self), private_quark);
|
|
if (!priv) {
|
|
priv = g_slice_new0 (Private);
|
|
|
|
priv->provided_sources = MM_MODEM_LOCATION_SOURCE_NONE;
|
|
priv->enabled_sources = MM_MODEM_LOCATION_SOURCE_NONE;
|
|
priv->qgps_supported = FEATURE_SUPPORT_UNKNOWN;
|
|
priv->qgpsurc_regex = g_regex_new ("\\r\\n\\+QGPSURC:.*", G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
|
|
priv->qlwurc_regex = g_regex_new ("\\r\\n\\+QLWURC:.*", G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
|
|
|
|
g_assert (MM_SHARED_QUECTEL_GET_INTERFACE (self)->peek_parent_broadband_modem_class);
|
|
priv->broadband_modem_class_parent = MM_SHARED_QUECTEL_GET_INTERFACE (self)->peek_parent_broadband_modem_class (self);
|
|
|
|
g_assert (MM_SHARED_QUECTEL_GET_INTERFACE (self)->peek_parent_modem_location_interface);
|
|
priv->iface_modem_location_parent = MM_SHARED_QUECTEL_GET_INTERFACE (self)->peek_parent_modem_location_interface (self);
|
|
|
|
g_assert (MM_SHARED_QUECTEL_GET_INTERFACE (self)->peek_parent_modem_interface);
|
|
priv->iface_modem_parent = MM_SHARED_QUECTEL_GET_INTERFACE (self)->peek_parent_modem_interface (self);
|
|
|
|
g_object_set_qdata_full (G_OBJECT (self), private_quark, priv, (GDestroyNotify)private_free);
|
|
}
|
|
return priv;
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/* Setup ports (Broadband modem class) */
|
|
|
|
void
|
|
mm_shared_quectel_setup_ports (MMBroadbandModem *self)
|
|
{
|
|
Private *priv;
|
|
MMPortSerialAt *ports[2];
|
|
guint i;
|
|
|
|
priv = get_private (MM_SHARED_QUECTEL (self));
|
|
g_assert (priv->broadband_modem_class_parent);
|
|
g_assert (priv->broadband_modem_class_parent->setup_ports);
|
|
|
|
/* Parent setup always first */
|
|
priv->broadband_modem_class_parent->setup_ports (self);
|
|
|
|
ports[0] = mm_base_modem_peek_port_primary (MM_BASE_MODEM (self));
|
|
ports[1] = mm_base_modem_peek_port_secondary (MM_BASE_MODEM (self));
|
|
|
|
/* Enable/disable unsolicited events in given port */
|
|
for (i = 0; i < G_N_ELEMENTS (ports); i++) {
|
|
if (!ports[i])
|
|
continue;
|
|
|
|
/* Ignore +QGPSURC */
|
|
mm_port_serial_at_add_unsolicited_msg_handler (
|
|
ports[i],
|
|
priv->qgpsurc_regex,
|
|
NULL, NULL, NULL);
|
|
|
|
/* Ignore +QLWURC */
|
|
mm_port_serial_at_add_unsolicited_msg_handler (
|
|
ports[i],
|
|
priv->qlwurc_regex,
|
|
NULL, NULL, NULL);
|
|
}
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/* Firmware update settings loading (Firmware interface) */
|
|
|
|
MMFirmwareUpdateSettings *
|
|
mm_shared_quectel_firmware_load_update_settings_finish (MMIfaceModemFirmware *self,
|
|
GAsyncResult *res,
|
|
GError **error)
|
|
{
|
|
return g_task_propagate_pointer (G_TASK (res), error);
|
|
}
|
|
|
|
static gboolean
|
|
quectel_is_sahara_supported (MMBaseModem *modem,
|
|
MMPort *port)
|
|
{
|
|
return mm_kernel_device_get_global_property_as_boolean (mm_port_peek_kernel_device (port), "ID_MM_QUECTEL_SAHARA");
|
|
}
|
|
|
|
static gboolean
|
|
quectel_is_firehose_supported (MMBaseModem *modem,
|
|
MMPort *port)
|
|
{
|
|
return mm_kernel_device_get_global_property_as_boolean (mm_port_peek_kernel_device (port), "ID_MM_QUECTEL_FIREHOSE");
|
|
}
|
|
|
|
static MMModemFirmwareUpdateMethod
|
|
quectel_get_firmware_update_methods (MMBaseModem *modem,
|
|
MMPort *port)
|
|
{
|
|
MMModemFirmwareUpdateMethod update_methods;
|
|
|
|
update_methods = MM_MODEM_FIRMWARE_UPDATE_METHOD_NONE;
|
|
|
|
if (quectel_is_firehose_supported (modem, port))
|
|
update_methods |= MM_MODEM_FIRMWARE_UPDATE_METHOD_FIREHOSE;
|
|
if (quectel_is_sahara_supported (modem, port))
|
|
update_methods |= MM_MODEM_FIRMWARE_UPDATE_METHOD_SAHARA;
|
|
|
|
return update_methods;
|
|
}
|
|
|
|
static void
|
|
quectel_at_port_get_firmware_version_ready (MMBaseModem *modem,
|
|
GAsyncResult *res,
|
|
GTask *task)
|
|
{
|
|
MMFirmwareUpdateSettings *update_settings;
|
|
const gchar *version;
|
|
|
|
update_settings = g_task_get_task_data (task);
|
|
|
|
version = mm_base_modem_at_command_finish (modem, res, NULL);
|
|
if (version)
|
|
mm_firmware_update_settings_set_version (update_settings, version);
|
|
|
|
g_task_return_pointer (task, g_object_ref (update_settings), g_object_unref);
|
|
g_object_unref (task);
|
|
}
|
|
|
|
#if defined WITH_MBIM
|
|
static void
|
|
quectel_mbim_port_get_firmware_version_ready (MbimDevice *device,
|
|
GAsyncResult *res,
|
|
GTask *task)
|
|
{
|
|
g_autoptr(MbimMessage) response = NULL;
|
|
guint32 version_id;
|
|
g_autofree gchar *version_str = NULL;
|
|
MMFirmwareUpdateSettings *update_settings;
|
|
|
|
update_settings = g_task_get_task_data (task);
|
|
|
|
response = mbim_device_command_finish (device, res, NULL);
|
|
if (response && mbim_message_response_get_result (response, MBIM_MESSAGE_TYPE_COMMAND_DONE, NULL) &&
|
|
mbim_message_qdu_quectel_read_version_response_parse (response, &version_id, &version_str, NULL)) {
|
|
mm_firmware_update_settings_set_version (update_settings, version_str);
|
|
}
|
|
|
|
g_task_return_pointer (task, g_object_ref (update_settings), g_object_unref);
|
|
g_object_unref (task);
|
|
}
|
|
#endif
|
|
|
|
static void
|
|
qfastboot_test_ready (MMBaseModem *self,
|
|
GAsyncResult *res,
|
|
GTask *task)
|
|
{
|
|
MMFirmwareUpdateSettings *update_settings;
|
|
|
|
update_settings = g_task_get_task_data (task);
|
|
|
|
/* Set update method */
|
|
if (mm_base_modem_at_command_finish (self, res, NULL)) {
|
|
mm_firmware_update_settings_set_method (update_settings, MM_MODEM_FIRMWARE_UPDATE_METHOD_FASTBOOT);
|
|
mm_firmware_update_settings_set_fastboot_at (update_settings, "AT+QFASTBOOT");
|
|
} else
|
|
mm_firmware_update_settings_set_method (update_settings, MM_MODEM_FIRMWARE_UPDATE_METHOD_NONE);
|
|
|
|
/* Fetch full firmware info */
|
|
mm_base_modem_at_command (MM_BASE_MODEM (self),
|
|
"+QGMR?",
|
|
3,
|
|
FALSE,
|
|
(GAsyncReadyCallback) quectel_at_port_get_firmware_version_ready,
|
|
task);
|
|
}
|
|
|
|
static void
|
|
quectel_at_port_get_firmware_revision_ready (MMBaseModem *self,
|
|
GAsyncResult *res,
|
|
GTask *task)
|
|
{
|
|
MMFirmwareUpdateSettings *update_settings;
|
|
MMModemFirmwareUpdateMethod update_methods;
|
|
const gchar *revision;
|
|
const gchar *name;
|
|
const gchar *id;
|
|
g_autoptr(GPtrArray) ids = NULL;
|
|
GError *error = NULL;
|
|
|
|
update_settings = g_task_get_task_data (task);
|
|
update_methods = mm_firmware_update_settings_get_method (update_settings);
|
|
|
|
/* Set device ids */
|
|
ids = mm_iface_firmware_build_generic_device_ids (MM_IFACE_MODEM_FIRMWARE (self), &error);
|
|
if (error) {
|
|
mm_obj_warn (self, "failed to build generic device ids: %s", error->message);
|
|
g_task_return_error (task, error);
|
|
g_object_unref (task);
|
|
return;
|
|
}
|
|
|
|
/* Add device id based on modem name */
|
|
revision = mm_base_modem_at_command_finish (self, res, NULL);
|
|
if (revision && g_utf8_validate (revision, -1, NULL)) {
|
|
name = g_strndup (revision, 7);
|
|
mm_obj_dbg (self, "revision %s converted to modem name %s", revision, name);
|
|
id = (const gchar *) g_ptr_array_index (ids, 0);
|
|
g_ptr_array_insert (ids, 0, g_strdup_printf ("%s&NAME_%s", id, name));
|
|
}
|
|
|
|
mm_firmware_update_settings_set_device_ids (update_settings, (const gchar **)ids->pdata);
|
|
|
|
/* Set update methods */
|
|
if (update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_FIREHOSE) {
|
|
/* Fetch full firmware info */
|
|
mm_base_modem_at_command (self,
|
|
"+QGMR?",
|
|
3,
|
|
TRUE,
|
|
(GAsyncReadyCallback) quectel_at_port_get_firmware_version_ready,
|
|
task);
|
|
} else {
|
|
/* Check fastboot support */
|
|
mm_base_modem_at_command (self,
|
|
"AT+QFASTBOOT=?",
|
|
3,
|
|
TRUE,
|
|
(GAsyncReadyCallback) qfastboot_test_ready,
|
|
task);
|
|
}
|
|
}
|
|
|
|
void
|
|
mm_shared_quectel_firmware_load_update_settings (MMIfaceModemFirmware *self,
|
|
GAsyncReadyCallback callback,
|
|
gpointer user_data)
|
|
{
|
|
GTask *task;
|
|
MMPortSerialAt *at_port;
|
|
MMModemFirmwareUpdateMethod update_methods;
|
|
MMFirmwareUpdateSettings *update_settings;
|
|
#if defined WITH_MBIM
|
|
MMPortMbim *mbim;
|
|
#endif
|
|
|
|
task = g_task_new (self, NULL, callback, user_data);
|
|
|
|
at_port = mm_base_modem_peek_best_at_port (MM_BASE_MODEM (self), NULL);
|
|
if (at_port) {
|
|
update_methods = quectel_get_firmware_update_methods (MM_BASE_MODEM (self), MM_PORT (at_port));
|
|
update_settings = mm_firmware_update_settings_new (update_methods);
|
|
g_task_set_task_data (task, update_settings, g_object_unref);
|
|
|
|
/* Fetch modem name */
|
|
mm_base_modem_at_command (MM_BASE_MODEM (self),
|
|
"+CGMR",
|
|
3,
|
|
TRUE,
|
|
(GAsyncReadyCallback) quectel_at_port_get_firmware_revision_ready,
|
|
task);
|
|
|
|
return;
|
|
}
|
|
|
|
#if defined WITH_MBIM
|
|
mbim = mm_broadband_modem_mbim_peek_port_mbim (MM_BROADBAND_MODEM_MBIM (self));
|
|
if (mbim) {
|
|
g_autoptr(MbimMessage) message = NULL;
|
|
|
|
update_methods = quectel_get_firmware_update_methods (MM_BASE_MODEM (self), MM_PORT (mbim));
|
|
update_settings = mm_firmware_update_settings_new (update_methods);
|
|
|
|
/* Fetch firmware info */
|
|
g_task_set_task_data (task, update_settings, g_object_unref);
|
|
message = mbim_message_qdu_quectel_read_version_set_new (MBIM_QDU_QUECTEL_VERSION_TYPE_FW_BUILD_ID, NULL);
|
|
mbim_device_command (mm_port_mbim_peek_device (mbim),
|
|
message,
|
|
5,
|
|
NULL,
|
|
(GAsyncReadyCallback) quectel_mbim_port_get_firmware_version_ready,
|
|
task);
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
g_task_return_new_error (task,
|
|
MM_CORE_ERROR,
|
|
MM_CORE_ERROR_FAILED,
|
|
"Couldn't find a port to fetch firmware info");
|
|
g_object_unref (task);
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/* "+QUSIM: 1" URC is emitted by Quectel modems after the USIM has been
|
|
* (re)initialized. We register a handler for this URC and perform a check
|
|
* for SIM swap when it is encountered. The motivation for this is to detect
|
|
* M2M eUICC profile switches. According to SGP.02 chapter 3.2.1, the eUICC
|
|
* shall trigger a REFRESH operation with eUICC reset when a new profile is
|
|
* enabled. The +QUSIM URC appears after the eUICC has restarted and can act
|
|
* as a trigger for profile switch check. This should basically be handled
|
|
* the same as a physical SIM swap, so the existing SIM hot swap mechanism
|
|
* is used.
|
|
*/
|
|
|
|
static void
|
|
quectel_qusim_check_for_sim_swap_ready (MMIfaceModem *self,
|
|
GAsyncResult *res)
|
|
{
|
|
g_autoptr(GError) error = NULL;
|
|
|
|
if (!MM_IFACE_MODEM_GET_INTERFACE (self)->check_for_sim_swap_finish (self, res, &error))
|
|
mm_obj_warn (self, "couldn't check SIM swap: %s", error->message);
|
|
else
|
|
mm_obj_dbg (self, "check SIM swap completed");
|
|
}
|
|
|
|
static void
|
|
quectel_qusim_unsolicited_handler (MMPortSerialAt *port,
|
|
GMatchInfo *match_info,
|
|
MMIfaceModem *self)
|
|
{
|
|
if (!MM_IFACE_MODEM_GET_INTERFACE (self)->check_for_sim_swap ||
|
|
!MM_IFACE_MODEM_GET_INTERFACE (self)->check_for_sim_swap_finish)
|
|
return;
|
|
|
|
mm_obj_dbg (self, "checking SIM swap");
|
|
MM_IFACE_MODEM_GET_INTERFACE (self)->check_for_sim_swap (
|
|
self,
|
|
NULL,
|
|
(GAsyncReadyCallback)quectel_qusim_check_for_sim_swap_ready,
|
|
NULL);
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/* Setup SIM hot swap context (Modem interface) */
|
|
|
|
gboolean
|
|
mm_shared_quectel_setup_sim_hot_swap_finish (MMIfaceModem *self,
|
|
GAsyncResult *res,
|
|
GError **error)
|
|
{
|
|
return g_task_propagate_boolean (G_TASK (res), error);
|
|
}
|
|
|
|
static void
|
|
parent_setup_sim_hot_swap_ready (MMIfaceModem *self,
|
|
GAsyncResult *res,
|
|
GTask *task)
|
|
{
|
|
Private *priv;
|
|
g_autoptr(GError) error = NULL;
|
|
|
|
priv = get_private (MM_SHARED_QUECTEL (self));
|
|
|
|
if (!priv->iface_modem_parent->setup_sim_hot_swap_finish (self, res, &error))
|
|
mm_obj_dbg (self, "additional SIM hot swap detection setup failed: %s", error->message);
|
|
|
|
/* The +QUSIM based setup never fails, so we can safely return success here */
|
|
g_task_return_boolean (task, TRUE);
|
|
g_object_unref (task);
|
|
}
|
|
|
|
void
|
|
mm_shared_quectel_setup_sim_hot_swap (MMIfaceModem *self,
|
|
GAsyncReadyCallback callback,
|
|
gpointer user_data)
|
|
{
|
|
Private *priv;
|
|
MMPortSerialAt *ports[2];
|
|
GTask *task;
|
|
GRegex *pattern;
|
|
guint i;
|
|
|
|
priv = get_private (MM_SHARED_QUECTEL (self));
|
|
|
|
task = g_task_new (self, NULL, callback, user_data);
|
|
|
|
ports[0] = mm_base_modem_peek_port_primary (MM_BASE_MODEM (self));
|
|
ports[1] = mm_base_modem_peek_port_secondary (MM_BASE_MODEM (self));
|
|
|
|
pattern = g_regex_new ("\\+QUSIM:\\s*1\\r\\n", G_REGEX_RAW, 0, NULL);
|
|
g_assert (pattern);
|
|
|
|
for (i = 0; i < G_N_ELEMENTS (ports); i++) {
|
|
if (ports[i])
|
|
mm_port_serial_at_add_unsolicited_msg_handler (
|
|
ports[i],
|
|
pattern,
|
|
(MMPortSerialAtUnsolicitedMsgFn)quectel_qusim_unsolicited_handler,
|
|
self,
|
|
NULL);
|
|
}
|
|
|
|
g_regex_unref (pattern);
|
|
mm_obj_dbg (self, "+QUSIM detection set up");
|
|
|
|
/* Now, if available, setup parent logic */
|
|
if (priv->iface_modem_parent->setup_sim_hot_swap &&
|
|
priv->iface_modem_parent->setup_sim_hot_swap_finish) {
|
|
priv->iface_modem_parent->setup_sim_hot_swap (self,
|
|
(GAsyncReadyCallback) parent_setup_sim_hot_swap_ready,
|
|
task);
|
|
return;
|
|
}
|
|
|
|
/* Otherwise, we're done */
|
|
g_task_return_boolean (task, TRUE);
|
|
g_object_unref (task);
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/* GPS trace received */
|
|
|
|
static void
|
|
trace_received (MMPortSerialGps *port,
|
|
const gchar *trace,
|
|
MMIfaceModemLocation *self)
|
|
{
|
|
mm_iface_modem_location_gps_update (self, trace);
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/* Location capabilities loading (Location interface) */
|
|
|
|
MMModemLocationSource
|
|
mm_shared_quectel_location_load_capabilities_finish (MMIfaceModemLocation *self,
|
|
GAsyncResult *res,
|
|
GError **error)
|
|
{
|
|
GError *inner_error = NULL;
|
|
gssize value;
|
|
|
|
value = g_task_propagate_int (G_TASK (res), &inner_error);
|
|
if (inner_error) {
|
|
g_propagate_error (error, inner_error);
|
|
return MM_MODEM_LOCATION_SOURCE_NONE;
|
|
}
|
|
return (MMModemLocationSource)value;
|
|
}
|
|
|
|
static void
|
|
probe_qgps_ready (MMBaseModem *_self,
|
|
GAsyncResult *res,
|
|
GTask *task)
|
|
{
|
|
MMSharedQuectel *self;
|
|
Private *priv;
|
|
MMModemLocationSource sources;
|
|
|
|
self = MM_SHARED_QUECTEL (g_task_get_source_object (task));
|
|
priv = get_private (self);
|
|
|
|
priv->qgps_supported = (!!mm_base_modem_at_command_finish (_self, res, NULL) ?
|
|
FEATURE_SUPPORTED : FEATURE_NOT_SUPPORTED);
|
|
|
|
mm_obj_dbg (self, "GPS management with +QGPS is %ssupported",
|
|
priv->qgps_supported ? "" : "not ");
|
|
|
|
/* Recover parent sources */
|
|
sources = GPOINTER_TO_UINT (g_task_get_task_data (task));
|
|
|
|
/* Only flag as provided those sources not already provided by the parent */
|
|
if (priv->qgps_supported == FEATURE_SUPPORTED) {
|
|
if (!(sources & MM_MODEM_LOCATION_SOURCE_GPS_NMEA))
|
|
priv->provided_sources |= MM_MODEM_LOCATION_SOURCE_GPS_NMEA;
|
|
if (!(sources & MM_MODEM_LOCATION_SOURCE_GPS_RAW))
|
|
priv->provided_sources |= MM_MODEM_LOCATION_SOURCE_GPS_RAW;
|
|
if (!(sources & MM_MODEM_LOCATION_SOURCE_GPS_UNMANAGED))
|
|
priv->provided_sources |= MM_MODEM_LOCATION_SOURCE_GPS_UNMANAGED;
|
|
|
|
sources |= priv->provided_sources;
|
|
|
|
/* Add handler for the NMEA traces in the GPS data port */
|
|
mm_port_serial_gps_add_trace_handler (mm_base_modem_peek_port_gps (MM_BASE_MODEM (self)),
|
|
(MMPortSerialGpsTraceFn)trace_received,
|
|
self,
|
|
NULL);
|
|
}
|
|
|
|
/* So we're done, complete */
|
|
g_task_return_int (task, sources);
|
|
g_object_unref (task);
|
|
}
|
|
|
|
static void
|
|
parent_load_capabilities_ready (MMIfaceModemLocation *self,
|
|
GAsyncResult *res,
|
|
GTask *task)
|
|
{
|
|
Private *priv;
|
|
MMModemLocationSource sources;
|
|
GError *error = NULL;
|
|
|
|
priv = get_private (MM_SHARED_QUECTEL (self));
|
|
sources = priv->iface_modem_location_parent->load_capabilities_finish (self, res, &error);
|
|
if (error) {
|
|
g_task_return_error (task, error);
|
|
g_object_unref (task);
|
|
return;
|
|
}
|
|
|
|
/* Now our own check. If we don't have any GPS port, we're done */
|
|
if (!mm_base_modem_peek_port_gps (MM_BASE_MODEM (self))) {
|
|
mm_obj_dbg (self, "no GPS data port found: no GPS capabilities");
|
|
g_task_return_int (task, sources);
|
|
g_object_unref (task);
|
|
return;
|
|
}
|
|
|
|
/* Store parent supported sources in task data */
|
|
g_task_set_task_data (task, GUINT_TO_POINTER (sources), NULL);
|
|
|
|
/* Probe QGPS support */
|
|
g_assert (priv->qgps_supported == FEATURE_SUPPORT_UNKNOWN);
|
|
mm_base_modem_at_command (MM_BASE_MODEM (self),
|
|
"+QGPS=?",
|
|
3,
|
|
TRUE, /* cached */
|
|
(GAsyncReadyCallback)probe_qgps_ready,
|
|
task);
|
|
}
|
|
|
|
void
|
|
mm_shared_quectel_location_load_capabilities (MMIfaceModemLocation *_self,
|
|
GAsyncReadyCallback callback,
|
|
gpointer user_data)
|
|
{
|
|
GTask *task;
|
|
Private *priv;
|
|
|
|
task = g_task_new (_self, NULL, callback, user_data);
|
|
priv = get_private (MM_SHARED_QUECTEL (_self));
|
|
|
|
/* Chain up parent's setup */
|
|
priv->iface_modem_location_parent->load_capabilities (_self,
|
|
(GAsyncReadyCallback)parent_load_capabilities_ready,
|
|
task);
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/* Enable location gathering (Location interface) */
|
|
|
|
/* NOTES:
|
|
* 1) "+QGPSCFG=\"nmeasrc\",1" will be necessary for getting location data
|
|
* without the nmea port.
|
|
* 2) may be necessary to set "+QGPSCFG=\"gpsnmeatype\".
|
|
* 3) QGPSXTRA=1 is necessary to support XTRA assistance data for
|
|
* faster GNSS location locks.
|
|
*/
|
|
static const MMBaseModemAtCommand gps_startup[] = {
|
|
{ "+QGPSCFG=\"outport\",\"usbnmea\"", 3, FALSE, mm_base_modem_response_processor_no_result_continue },
|
|
{ "+QGPS=1", 3, FALSE, mm_base_modem_response_processor_no_result_continue },
|
|
{ "+QGPSXTRA=1", 3, FALSE, mm_base_modem_response_processor_no_result_continue },
|
|
{ NULL }
|
|
};
|
|
|
|
gboolean
|
|
mm_shared_quectel_enable_location_gathering_finish (MMIfaceModemLocation *self,
|
|
GAsyncResult *res,
|
|
GError **error)
|
|
{
|
|
return g_task_propagate_boolean (G_TASK (res), error);
|
|
}
|
|
|
|
static void
|
|
gps_startup_ready (MMBaseModem *self,
|
|
GAsyncResult *res,
|
|
GTask *task)
|
|
{
|
|
MMModemLocationSource source;
|
|
GError *error = NULL;
|
|
Private *priv;
|
|
|
|
priv = get_private (MM_SHARED_QUECTEL (self));
|
|
|
|
mm_base_modem_at_sequence_finish (self, res, NULL, &error);
|
|
if (error) {
|
|
g_task_return_error (task, error);
|
|
g_object_unref (task);
|
|
return;
|
|
}
|
|
|
|
source = GPOINTER_TO_UINT (g_task_get_task_data (task));
|
|
|
|
/* Check if the nmea/raw gps port exists and is available */
|
|
if (source & (MM_MODEM_LOCATION_SOURCE_GPS_NMEA | MM_MODEM_LOCATION_SOURCE_GPS_RAW)) {
|
|
MMPortSerialGps *gps_port;
|
|
|
|
gps_port = mm_base_modem_peek_port_gps (MM_BASE_MODEM (self));
|
|
if (!gps_port || !mm_port_serial_open (MM_PORT_SERIAL (gps_port), &error)) {
|
|
if (error)
|
|
g_task_return_error (task, error);
|
|
else
|
|
g_task_return_new_error (task,
|
|
MM_CORE_ERROR,
|
|
MM_CORE_ERROR_FAILED,
|
|
"Couldn't open raw GPS serial port");
|
|
} else {
|
|
/* GPS port was successfully opened */
|
|
priv->enabled_sources |= source;
|
|
g_task_return_boolean (task, TRUE);
|
|
}
|
|
} else {
|
|
/* No need to open GPS port */
|
|
priv->enabled_sources |= source;
|
|
g_task_return_boolean (task, TRUE);
|
|
}
|
|
g_object_unref (task);
|
|
}
|
|
|
|
static void
|
|
parent_enable_location_gathering_ready (MMIfaceModemLocation *self,
|
|
GAsyncResult *res,
|
|
GTask *task)
|
|
{
|
|
GError *error = NULL;
|
|
Private *priv;
|
|
|
|
priv = get_private (MM_SHARED_QUECTEL (self));
|
|
if (!priv->iface_modem_location_parent->enable_location_gathering_finish (self, res, &error))
|
|
g_task_return_error (task, error);
|
|
else
|
|
g_task_return_boolean (task, TRUE);
|
|
g_object_unref (task);
|
|
}
|
|
|
|
void
|
|
mm_shared_quectel_enable_location_gathering (MMIfaceModemLocation *self,
|
|
MMModemLocationSource source,
|
|
GAsyncReadyCallback callback,
|
|
gpointer user_data)
|
|
{
|
|
GTask *task;
|
|
Private *priv;
|
|
gboolean start_gps = FALSE;
|
|
|
|
priv = get_private (MM_SHARED_QUECTEL (self));
|
|
g_assert (priv->iface_modem_location_parent);
|
|
g_assert (priv->iface_modem_location_parent->enable_location_gathering);
|
|
g_assert (priv->iface_modem_location_parent->enable_location_gathering_finish);
|
|
|
|
task = g_task_new (self, NULL, callback, user_data);
|
|
g_task_set_task_data (task, GUINT_TO_POINTER (source), NULL);
|
|
|
|
/* Check if the source is provided by the parent */
|
|
if (!(priv->provided_sources & source)) {
|
|
priv->iface_modem_location_parent->enable_location_gathering (
|
|
self,
|
|
source,
|
|
(GAsyncReadyCallback)parent_enable_location_gathering_ready,
|
|
task);
|
|
return;
|
|
}
|
|
|
|
/* Only start GPS engine if not done already */
|
|
start_gps = ((source & (MM_MODEM_LOCATION_SOURCE_GPS_NMEA |
|
|
MM_MODEM_LOCATION_SOURCE_GPS_RAW |
|
|
MM_MODEM_LOCATION_SOURCE_GPS_UNMANAGED)) &&
|
|
!(priv->enabled_sources & (MM_MODEM_LOCATION_SOURCE_GPS_NMEA |
|
|
MM_MODEM_LOCATION_SOURCE_GPS_RAW |
|
|
MM_MODEM_LOCATION_SOURCE_GPS_UNMANAGED)));
|
|
|
|
if (start_gps) {
|
|
mm_base_modem_at_sequence (
|
|
MM_BASE_MODEM (self),
|
|
gps_startup,
|
|
NULL, /* response_processor_context */
|
|
NULL, /* response_processor_context_free */
|
|
(GAsyncReadyCallback)gps_startup_ready,
|
|
task);
|
|
return;
|
|
}
|
|
|
|
/* If the GPS is already running just return */
|
|
priv->enabled_sources |= source;
|
|
g_task_return_boolean (task, TRUE);
|
|
g_object_unref (task);
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/* Disable location gathering (Location interface) */
|
|
|
|
gboolean
|
|
mm_shared_quectel_disable_location_gathering_finish (MMIfaceModemLocation *self,
|
|
GAsyncResult *res,
|
|
GError **error)
|
|
{
|
|
return g_task_propagate_boolean (G_TASK (res), error);
|
|
}
|
|
|
|
static void
|
|
qgps_end_ready (MMBaseModem *self,
|
|
GAsyncResult *res,
|
|
GTask *task)
|
|
{
|
|
GError *error = NULL;
|
|
|
|
if (!mm_base_modem_at_command_finish (self, res, &error))
|
|
g_task_return_error (task, error);
|
|
else
|
|
g_task_return_boolean (task, TRUE);
|
|
g_object_unref (task);
|
|
}
|
|
|
|
static void
|
|
disable_location_gathering_parent_ready (MMIfaceModemLocation *self,
|
|
GAsyncResult *res,
|
|
GTask *task)
|
|
{
|
|
GError *error = NULL;
|
|
Private *priv;
|
|
|
|
priv = get_private (MM_SHARED_QUECTEL (self));
|
|
if (!priv->iface_modem_location_parent->disable_location_gathering_finish (self, res, &error))
|
|
g_task_return_error (task, error);
|
|
else
|
|
g_task_return_boolean (task, TRUE);
|
|
g_object_unref (task);
|
|
}
|
|
|
|
void
|
|
mm_shared_quectel_disable_location_gathering (MMIfaceModemLocation *self,
|
|
MMModemLocationSource source,
|
|
GAsyncReadyCallback callback,
|
|
gpointer user_data)
|
|
{
|
|
GTask *task;
|
|
Private *priv;
|
|
|
|
priv = get_private (MM_SHARED_QUECTEL (self));
|
|
g_assert (priv->iface_modem_location_parent);
|
|
|
|
task = g_task_new (self, NULL, callback, user_data);
|
|
priv->enabled_sources &= ~source;
|
|
|
|
/* Pass handling to parent if we don't handle it */
|
|
if (!(source & priv->provided_sources)) {
|
|
/* The step to disable location gathering may not exist */
|
|
if (priv->iface_modem_location_parent->disable_location_gathering &&
|
|
priv->iface_modem_location_parent->disable_location_gathering_finish) {
|
|
priv->iface_modem_location_parent->disable_location_gathering (self,
|
|
source,
|
|
(GAsyncReadyCallback)disable_location_gathering_parent_ready,
|
|
task);
|
|
return;
|
|
}
|
|
|
|
g_task_return_boolean (task, TRUE);
|
|
g_object_unref (task);
|
|
return;
|
|
}
|
|
|
|
/* Turn off gps on the modem if the source uses gps,
|
|
* and there are no other gps sources enabled */
|
|
if ((source & (MM_MODEM_LOCATION_SOURCE_GPS_NMEA |
|
|
MM_MODEM_LOCATION_SOURCE_GPS_RAW |
|
|
MM_MODEM_LOCATION_SOURCE_GPS_UNMANAGED)) &&
|
|
!(priv->enabled_sources & (MM_MODEM_LOCATION_SOURCE_GPS_NMEA |
|
|
MM_MODEM_LOCATION_SOURCE_GPS_RAW |
|
|
MM_MODEM_LOCATION_SOURCE_GPS_UNMANAGED))) {
|
|
/* Close the data port if we don't need it anymore */
|
|
if (source & (MM_MODEM_LOCATION_SOURCE_GPS_RAW | MM_MODEM_LOCATION_SOURCE_GPS_NMEA)) {
|
|
MMPortSerialGps *gps_port;
|
|
|
|
gps_port = mm_base_modem_peek_port_gps (MM_BASE_MODEM (self));
|
|
if (gps_port)
|
|
mm_port_serial_close (MM_PORT_SERIAL (gps_port));
|
|
}
|
|
|
|
mm_base_modem_at_command (MM_BASE_MODEM (self),
|
|
"+QGPSEND",
|
|
3,
|
|
FALSE,
|
|
(GAsyncReadyCallback)qgps_end_ready,
|
|
task);
|
|
return;
|
|
}
|
|
|
|
g_task_return_boolean (task, TRUE);
|
|
g_object_unref (task);
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/* Check support (Time interface) */
|
|
|
|
gboolean
|
|
mm_shared_quectel_time_check_support_finish (MMIfaceModemTime *self,
|
|
GAsyncResult *res,
|
|
GError **error)
|
|
{
|
|
return g_task_propagate_boolean (G_TASK (res), error);
|
|
}
|
|
|
|
static void
|
|
support_cclk_query_ready (MMBaseModem *self,
|
|
GAsyncResult *res,
|
|
GTask *task)
|
|
{
|
|
/* error never returned */
|
|
g_task_return_boolean (task, !!mm_base_modem_at_command_finish (self, res, NULL));
|
|
g_object_unref (task);
|
|
}
|
|
|
|
static void
|
|
support_cclk_query (GTask *task)
|
|
{
|
|
MMBaseModem *self;
|
|
|
|
self = g_task_get_source_object (task);
|
|
mm_base_modem_at_command (MM_BASE_MODEM (self),
|
|
"+CCLK?",
|
|
3,
|
|
FALSE,
|
|
(GAsyncReadyCallback)support_cclk_query_ready,
|
|
task);
|
|
}
|
|
|
|
static void
|
|
ctzu_set_ready (MMBaseModem *self,
|
|
GAsyncResult *res,
|
|
GTask *task)
|
|
{
|
|
g_autoptr(GError) error = NULL;
|
|
|
|
if (!mm_base_modem_at_command_finish (self, res, &error))
|
|
mm_obj_warn (self, "couldn't enable automatic time zone update: %s", error->message);
|
|
|
|
support_cclk_query (task);
|
|
}
|
|
|
|
static void
|
|
ctzu_test_ready (MMBaseModem *self,
|
|
GAsyncResult *res,
|
|
GTask *task)
|
|
{
|
|
g_autoptr(GError) error = NULL;
|
|
const gchar *response;
|
|
gboolean supports_disable;
|
|
gboolean supports_enable;
|
|
gboolean supports_enable_update_rtc;
|
|
const gchar *cmd = NULL;
|
|
|
|
/* If CTZU isn't supported, run CCLK right away */
|
|
response = mm_base_modem_at_command_finish (self, res, NULL);
|
|
if (!response) {
|
|
support_cclk_query (task);
|
|
return;
|
|
}
|
|
|
|
if (!mm_quectel_parse_ctzu_test_response (response,
|
|
self,
|
|
&supports_disable,
|
|
&supports_enable,
|
|
&supports_enable_update_rtc,
|
|
&error)) {
|
|
mm_obj_warn (self, "couldn't parse +CTZU test response: %s", error->message);
|
|
support_cclk_query (task);
|
|
return;
|
|
}
|
|
|
|
/* Custom time support check because some Quectel modems (e.g. EC25) require
|
|
* +CTZU=3 in order to have the CCLK? time reported in localtime, instead of
|
|
* UTC time. */
|
|
if (supports_enable_update_rtc)
|
|
cmd = "+CTZU=3";
|
|
else if (supports_enable)
|
|
cmd = "+CTZU=1";
|
|
|
|
if (!cmd) {
|
|
mm_obj_warn (self, "unknown +CTZU support");
|
|
support_cclk_query (task);
|
|
return;
|
|
}
|
|
|
|
mm_base_modem_at_command (MM_BASE_MODEM (self),
|
|
cmd,
|
|
3,
|
|
FALSE,
|
|
(GAsyncReadyCallback)ctzu_set_ready,
|
|
task);
|
|
}
|
|
|
|
void
|
|
mm_shared_quectel_time_check_support (MMIfaceModemTime *self,
|
|
GAsyncReadyCallback callback,
|
|
gpointer user_data)
|
|
{
|
|
GTask *task;
|
|
|
|
task = g_task_new (self, NULL, callback, user_data);
|
|
mm_base_modem_at_command (MM_BASE_MODEM (self),
|
|
"+CTZU=?",
|
|
3,
|
|
TRUE, /* cached! */
|
|
(GAsyncReadyCallback)ctzu_test_ready,
|
|
task);
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
|
|
static void
|
|
shared_quectel_init (gpointer g_iface)
|
|
{
|
|
}
|
|
|
|
GType
|
|
mm_shared_quectel_get_type (void)
|
|
{
|
|
static GType shared_quectel_type = 0;
|
|
|
|
if (!G_UNLIKELY (shared_quectel_type)) {
|
|
static const GTypeInfo info = {
|
|
sizeof (MMSharedQuectel), /* class_size */
|
|
shared_quectel_init, /* base_init */
|
|
NULL, /* base_finalize */
|
|
};
|
|
|
|
shared_quectel_type = g_type_register_static (G_TYPE_INTERFACE, "MMSharedQuectel", &info, 0);
|
|
g_type_interface_add_prerequisite (shared_quectel_type, MM_TYPE_IFACE_MODEM_FIRMWARE);
|
|
}
|
|
|
|
return shared_quectel_type;
|
|
}
|