diff options
Diffstat (limited to 'src/plugins/quectel/mm-shared-quectel.c')
-rw-r--r-- | src/plugins/quectel/mm-shared-quectel.c | 1039 |
1 files changed, 1039 insertions, 0 deletions
diff --git a/src/plugins/quectel/mm-shared-quectel.c b/src/plugins/quectel/mm-shared-quectel.c new file mode 100644 index 00000000..47d7cd33 --- /dev/null +++ b/src/plugins/quectel/mm-shared-quectel.c @@ -0,0 +1,1039 @@ +/* -*- 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> + * Copyright (c) 2022 Qualcomm Innovation Center, Inc. + */ + +#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; + GRegex *rdy_regex; +} Private; + +static void +private_free (Private *priv) +{ + g_regex_unref (priv->qgpsurc_regex); + g_regex_unref (priv->qlwurc_regex); + g_regex_unref (priv->rdy_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); + priv->rdy_regex = g_regex_new ("\\r\\nRDY", G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL); + + g_assert (priv->qgpsurc_regex); + g_assert (priv->qlwurc_regex); + g_assert (priv->rdy_regex); + + 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; +} + +/*****************************************************************************/ +/* RDY unsolicited event handler */ + +static void +rdy_handler (MMPortSerialAt *port, + GMatchInfo *match_info, + MMBroadbandModem *self) +{ + /* The RDY URC indicates a modem reset that may or may not go hand-in-hand + * with USB re-enumeration. For the latter case, we must make sure to + * re-synchronize modem and ModemManager states by re-probing. + */ + mm_obj_warn (self, "modem reset detected, triggering reprobe"); + mm_base_modem_set_reprobe (MM_BASE_MODEM (self), TRUE); + mm_base_modem_set_valid (MM_BASE_MODEM (self), FALSE); +} + +/*****************************************************************************/ +/* 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); + + /* Handle RDY */ + mm_port_serial_at_add_unsolicited_msg_handler ( + ports[i], + priv->rdy_regex, + (MMPortSerialAtUnsolicitedMsgFn)rdy_handler, + self, + 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, + 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; + guint i; + g_autoptr(GRegex) pattern = NULL; + g_autoptr(GError) error = NULL; + + 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); + } + + mm_obj_dbg (self, "+QUSIM detection set up"); + + if (!mm_broadband_modem_sim_hot_swap_ports_context_init (MM_BROADBAND_MODEM (self), &error)) + mm_obj_warn (self, "failed to initialize SIM hot swap ports context: %s", error->message); + + /* 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); +} + +/*****************************************************************************/ +/* SIM hot swap cleanup (Modem interface) */ + +void +mm_shared_quectel_cleanup_sim_hot_swap (MMIfaceModem *self) +{ + mm_broadband_modem_sim_hot_swap_ports_context_reset (MM_BROADBAND_MODEM (self)); +} + +/*****************************************************************************/ +/* 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; +} |