| /* -*- 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> |
| * Copyright (C) 2024 JUCR GmbH |
| */ |
| |
| #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-object.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_QRTR |
| #include "mm-kernel-device-qrtr.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 |
| */ |
| |
| static void log_object_iface_init (MMLogObjectInterface *iface); |
| |
| G_DEFINE_TYPE_EXTENDED (MMPortProbe, mm_port_probe, G_TYPE_OBJECT, 0, |
| G_IMPLEMENT_INTERFACE (MM_TYPE_LOG_OBJECT, log_object_iface_init)) |
| |
| enum { |
| PROP_0, |
| PROP_DEVICE, |
| PROP_PORT, |
| PROP_LAST |
| }; |
| |
| static GParamSpec *properties[PROP_LAST]; |
| |
| struct _MMPortProbePrivate { |
| /* Properties */ |
| MMDevice *device; |
| MMKernelDevice *port; |
| |
| /* From udev tags */ |
| gboolean is_ignored; |
| gboolean is_gps; |
| gboolean is_audio; |
| gboolean is_xmmrpc; |
| gboolean maybe_at; |
| gboolean maybe_qcdm; |
| gboolean maybe_qmi; |
| gboolean maybe_mbim; |
| |
| /* 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; |
| |
| /* Current probing task. Only one can be available at a time */ |
| GTask *task; |
| }; |
| |
| static const MMStringUintMap port_subsys_map[] = { |
| { "usbmisc", MM_PORT_SUBSYS_USBMISC }, |
| { "rpmsg", MM_PORT_SUBSYS_RPMSG }, |
| { "wwan", MM_PORT_SUBSYS_WWAN }, |
| }; |
| |
| /*****************************************************************************/ |
| |
| 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 }, |
| { "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 }, |
| { "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 }, |
| { "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 }, |
| { "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 }, |
| { "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 }, |
| { "AT", 3, mm_port_probe_response_processor_is_at }, |
| { "AT", 3, mm_port_probe_response_processor_is_at }, |
| { NULL } |
| }; |
| |
| typedef struct { |
| MMPortSerialAt *serial; |
| const MMPortProbeAtCommand *at_commands; |
| guint at_commands_limit; |
| } EarlyAtProbeContext; |
| |
| static void |
| early_at_probe_context_free (EarlyAtProbeContext *ctx) |
| { |
| g_clear_object (&ctx->serial); |
| g_slice_free (EarlyAtProbeContext, ctx); |
| } |
| |
| gboolean |
| mm_port_probe_run_early_at_probe_finish (MMPortProbe *self, |
| GAsyncResult *result, |
| GError **error) |
| { |
| return g_task_propagate_boolean (G_TASK (result), error); |
| } |
| |
| static void |
| early_at_probe_parse_response (MMPortSerialAt *serial, |
| GAsyncResult *res, |
| GTask *task) |
| { |
| g_autoptr(GVariant) result = NULL; |
| g_autoptr(GError) result_error = NULL; |
| g_autofree gchar *response = NULL; |
| g_autoptr(GError) command_error = NULL; |
| EarlyAtProbeContext *ctx; |
| MMPortProbe *self; |
| gboolean is_at = FALSE; |
| |
| ctx = g_task_get_task_data (task); |
| self = g_task_get_source_object (task); |
| |
| /* If already cancelled, do nothing else */ |
| if (g_task_return_error_if_cancelled (task)) { |
| g_object_unref (task); |
| return; |
| } |
| |
| response = mm_port_serial_at_command_finish (serial, res, &command_error); |
| if (!ctx->at_commands->response_processor (ctx->at_commands->command, |
| response, |
| !!ctx->at_commands[1].command, |
| command_error, |
| &result, |
| &result_error)) { |
| /* Were we told to abort the whole probing? */ |
| if (result_error) { |
| g_task_return_new_error (task, |
| 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); |
| g_object_unref (task); |
| return; |
| } |
| |
| /* Go on to next command */ |
| ctx->at_commands++; |
| ctx->at_commands_limit--; |
| if (ctx->at_commands->command && ctx->at_commands_limit > 0) { |
| /* More commands in the group? */ |
| mm_port_serial_at_command ( |
| ctx->serial, |
| ctx->at_commands->command, |
| ctx->at_commands->timeout, |
| FALSE, /* raw */ |
| FALSE, /* allow_cached */ |
| g_task_get_cancellable (task), |
| (GAsyncReadyCallback)early_at_probe_parse_response, |
| task); |
| return; |
| } |
| |
| /* No more commands in the group; end probing; not AT */ |
| } else if (result) { |
| /* If any result given, it must be a boolean */ |
| g_assert (g_variant_is_of_type (result, G_VARIANT_TYPE_BOOLEAN)); |
| is_at = g_variant_get_boolean (result); |
| } |
| |
| mm_port_probe_set_result_at (self, is_at); |
| g_task_return_boolean (task, is_at); |
| g_object_unref (task); |
| } |
| |
| gboolean |
| mm_port_probe_run_early_at_probe (MMPortProbe *self, |
| MMPortSerialAt *serial, |
| GCancellable *cancellable, |
| GAsyncReadyCallback callback, |
| gpointer user_data) |
| { |
| GTask *task; |
| EarlyAtProbeContext *ctx; |
| gint tries; |
| |
| tries = mm_kernel_device_get_global_property_as_int (mm_port_probe_peek_port (self), |
| ID_MM_TTY_AT_PROBE_TRIES); |
| if (tries == 0) { |
| /* Early probing not required */ |
| return FALSE; |
| } |
| |
| task = g_task_new (self, cancellable, callback, user_data); |
| |
| ctx = g_slice_new0 (EarlyAtProbeContext); |
| ctx->serial = g_object_ref (serial); |
| ctx->at_commands = at_probing; |
| ctx->at_commands_limit = CLAMP (tries, 1, (gint) G_N_ELEMENTS (at_probing)); |
| g_task_set_task_data (task, ctx, (GDestroyNotify) early_at_probe_context_free); |
| |
| mm_port_serial_at_command ( |
| ctx->serial, |
| ctx->at_commands->command, |
| ctx->at_commands->timeout, |
| FALSE, /* raw */ |
| FALSE, /* allow_cached */ |
| g_task_get_cancellable (task), |
| (GAsyncReadyCallback)early_at_probe_parse_response, |
| task); |
| return TRUE; |
| } |
| |
| /*****************************************************************************/ |
| |
| static void |
| mm_port_probe_clear (MMPortProbe *self) |
| { |
| /* Clears existing probe results so probing can restart from the beginning. |
| * Should only be used internally as it does not ensure `task` is NULL. |
| */ |
| self->priv->flags = 0; |
| self->priv->is_at = FALSE; |
| self->priv->is_qcdm = FALSE; |
| g_clear_pointer (&self->priv->vendor, g_free); |
| g_clear_pointer (&self->priv->product, g_free); |
| self->priv->is_icera = FALSE; |
| self->priv->is_xmm = FALSE; |
| self->priv->is_qmi = FALSE; |
| self->priv->is_mbim = FALSE; |
| } |
| |
| void |
| mm_port_probe_reset (MMPortProbe *self) |
| { |
| /* Clears existing probe results after probing is complete */ |
| g_assert (!self->priv->task); |
| mm_port_probe_clear (self); |
| } |
| |
| /*****************************************************************************/ |
| /* 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_obj_dbg (self, "port is AT-capable"); |
| |
| /* 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->is_xmmrpc = FALSE; |
| self->priv->flags |= (MM_PORT_PROBE_QCDM | MM_PORT_PROBE_QMI | MM_PORT_PROBE_MBIM); |
| } else { |
| mm_obj_dbg (self, "port is not AT-capable"); |
| 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_obj_dbg (self, "vendor probing finished"); |
| self->priv->vendor = g_utf8_casefold (at_vendor, -1); |
| self->priv->flags |= MM_PORT_PROBE_AT_VENDOR; |
| } else { |
| mm_obj_dbg (self, "couldn't probe for vendor string"); |
| 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_obj_dbg (self, "product probing finished"); |
| self->priv->product = g_utf8_casefold (at_product, -1); |
| self->priv->flags |= MM_PORT_PROBE_AT_PRODUCT; |
| } else { |
| mm_obj_dbg (self, "couldn't probe for product string"); |
| 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_obj_dbg (self, "modem is Icera-based"); |
| self->priv->is_icera = TRUE; |
| self->priv->flags |= MM_PORT_PROBE_AT_ICERA; |
| } else { |
| mm_obj_dbg (self, "modem is probably not Icera-based"); |
| 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_obj_dbg (self, "modem is XMM-based"); |
| self->priv->is_xmm = TRUE; |
| self->priv->flags |= MM_PORT_PROBE_AT_XMM; |
| } else { |
| mm_obj_dbg (self, "modem is probably not XMM-based"); |
| 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_obj_dbg (self, "port is QCDM-capable"); |
| |
| /* 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->is_xmmrpc = 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_obj_dbg (self, "port is not QCDM-capable"); |
| } |
| |
| 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_obj_dbg (self, "port is QMI-capable"); |
| |
| /* 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->is_xmmrpc = 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_obj_dbg (self, "port is not QMI-capable"); |
| } |
| |
| 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_obj_dbg (self, "port is MBIM-capable"); |
| |
| /* 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->is_xmmrpc = 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_obj_dbg (self, "port is not MBIM-capable"); |
| } |
| |
| /*****************************************************************************/ |
| |
| typedef enum { |
| PROBE_STEP_FIRST, |
| PROBE_STEP_AT_CUSTOM_INIT_OPEN_PORT, |
| PROBE_STEP_AT_CUSTOM_INIT, |
| PROBE_STEP_AT_OPEN_PORT, |
| PROBE_STEP_AT, |
| PROBE_STEP_AT_VENDOR, |
| PROBE_STEP_AT_PRODUCT, |
| PROBE_STEP_AT_ICERA, |
| PROBE_STEP_AT_XMM, |
| PROBE_STEP_AT_CLOSE_PORT, |
| PROBE_STEP_QCDM, |
| PROBE_STEP_QCDM_CLOSE_PORT, |
| PROBE_STEP_QMI, |
| PROBE_STEP_MBIM, |
| PROBE_STEP_LAST |
| } ProbeStep; |
| |
| typedef struct { |
| /* ---- Generic task context ---- */ |
| guint32 flags; |
| guint source_id; |
| GCancellable *cancellable; |
| ProbeStep step; |
| |
| /* ---- 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 */ |
| 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; |
| /* Maximum number of at_commands to be sent */ |
| guint at_commands_limit; |
| /* 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 |
| |
| /* ---- QCDM probing specific context ---- */ |
| gboolean qcdm_required; |
| } PortProbeRunContext; |
| |
| static gboolean probe_at (MMPortProbe *self); |
| static void probe_step_next (MMPortProbe *self); |
| |
| static void |
| clear_probe_serial_port (PortProbeRunContext *ctx) |
| { |
| if (ctx->serial) { |
| if (ctx->buffer_full_id) { |
| g_signal_handler_disconnect (ctx->serial, ctx->buffer_full_id); |
| ctx->buffer_full_id = 0; |
| } |
| |
| if (mm_port_serial_is_open (ctx->serial)) |
| mm_port_serial_close (ctx->serial); |
| g_clear_object (&ctx->serial); |
| } |
| } |
| |
| 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; |
| } |
| |
| clear_probe_serial_port (ctx); |
| |
| #if defined WITH_QMI |
| if (ctx->port_qmi) { |
| /* We should have closed it cleanly before */ |
| g_assert (!mm_port_qmi_is_open (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 |
| |
| g_clear_object (&ctx->at_probing_cancellable); |
| g_clear_object (&ctx->cancellable); |
| |
| g_slice_free (PortProbeRunContext, ctx); |
| } |
| |
| /***************************************************************/ |
| /* QMI & MBIM */ |
| |
| #if defined WITH_QMI |
| |
| static void |
| qmi_port_close_ready (MMPortQmi *qmi_port, |
| GAsyncResult *res, |
| MMPortProbe *self) |
| { |
| g_assert (self->priv->task); |
| |
| mm_port_qmi_close_finish (qmi_port, res, NULL); |
| |
| /* Continue with remaining probings */ |
| probe_step_next (self); |
| } |
| |
| 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_obj_dbg (self, "error checking QMI support: %s", |
| 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 (ctx->port_qmi, |
| (GAsyncReadyCallback) qmi_port_close_ready, |
| self); |
| } |
| |
| #endif /* WITH_QMI */ |
| |
| static gboolean |
| wdm_probe_qmi (MMPortProbe *self) |
| { |
| PortProbeRunContext *ctx; |
| |
| g_assert (self->priv->task); |
| ctx = g_task_get_task_data (self->priv->task); |
| ctx->source_id = 0; |
| |
| #if defined WITH_QMI |
| /* Create a port and try to open it */ |
| mm_obj_dbg (self, "probing QMI..."); |
| |
| #if defined WITH_QRTR |
| if (MM_IS_KERNEL_DEVICE_QRTR (self->priv->port)) { |
| g_autoptr(QrtrNode) node = NULL; |
| |
| node = mm_kernel_device_qrtr_get_node (MM_KERNEL_DEVICE_QRTR (self->priv->port)); |
| |
| /* Will set MM_PORT_SUBSYS_QRTR when creating the mm-port */ |
| ctx->port_qmi = mm_port_qmi_new_from_node (mm_kernel_device_get_name (self->priv->port), node); |
| } else |
| #endif /* WITH_QRTR */ |
| { |
| MMPortSubsys subsys; |
| |
| subsys = mm_string_uint_map_lookup (port_subsys_map, |
| G_N_ELEMENTS (port_subsys_map), |
| mm_kernel_device_get_subsystem (self->priv->port), |
| MM_PORT_SUBSYS_USBMISC); |
| ctx->port_qmi = mm_port_qmi_new (mm_kernel_device_get_name (self->priv->port), subsys); |
| } |
| |
| mm_port_qmi_open (ctx->port_qmi, |
| FALSE, |
| g_task_get_cancellable (self->priv->task), |
| (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); |
| probe_step_next (self); |
| #endif /* WITH_QMI */ |
| |
| return G_SOURCE_REMOVE; |
| } |
| |
| #if defined WITH_MBIM |
| |
| static void |
| mbim_port_close_ready (MMPortMbim *mbim_port, |
| GAsyncResult *res, |
| MMPortProbe *self) |
| { |
| mm_port_mbim_close_finish (mbim_port, res, NULL); |
| |
| /* Continue with remaining probings */ |
| probe_step_next (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_obj_dbg (self, "error checking MBIM support: %s", |
| 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 gboolean |
| wdm_probe_mbim (MMPortProbe *self) |
| { |
| PortProbeRunContext *ctx; |
| |
| g_assert (self->priv->task); |
| ctx = g_task_get_task_data (self->priv->task); |
| ctx->source_id = 0; |
| |
| #if defined WITH_MBIM |
| mm_obj_dbg (self, "probing MBIM..."); |
| |
| /* 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_SUBSYS_USBMISC); |
| 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 |
| g_task_get_cancellable (self->priv->task), |
| (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); |
| probe_step_next (self); |
| #endif /* WITH_MBIM */ |
| |
| 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_obj_warn (self, "unsupported flow control settings in port: %s", error->message); |
| g_error_free (error); |
| } else { |
| g_object_set (serial, |
| MM_PORT_SERIAL_FLOW_CONTROL, flow_control, |
| NULL); |
| } |
| } |
| } |
| |
| /***************************************************************/ |
| /* QCDM */ |
| |
| static void |
| probe_qcdm_parse_response (MMPortSerialQcdm *port, |
| GAsyncResult *res, |
| MMPortProbe *self) |
| { |
| QcdmResult *result; |
| gint err = QCDM_SUCCESS; |
| gboolean is_qcdm = FALSE; |
| gboolean retry = FALSE; |
| g_autoptr(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_obj_warn (self, "failed to parse QCDM version info command result: %d", 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_obj_dbg (self, "QCDM parsing error: %s", error->message); |
| } else if (g_error_matches (error, MM_CONNECTION_ERROR, MM_CONNECTION_ERROR_NO_CARRIER)) { |
| /* Special-case: the port may have been in PPP mode (if system is restarted |
| * but the modem still had power) and failed AT probing. QCDM probing |
| * sends empty HDLC frames that PPP parses and then terminates the |
| * connection with "NO CARRIER". Match this and go back to AT probing. |
| */ |
| mm_obj_dbg (self, "QCDM parsing got NO CARRIER; retrying AT probing"); |
| mm_port_probe_clear (self); |
| } else { |
| if (!g_error_matches (error, MM_SERIAL_ERROR, MM_SERIAL_ERROR_RESPONSE_TIMEOUT)) |
| mm_obj_dbg (self, "QCDM probe error: (%d) %s", error->code, error->message); |
| 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) 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); |
| |
| /* Continue with remaining probings */ |
| probe_step_next (self); |
| } |
| |
| static gboolean |
| probe_qcdm (MMPortProbe *self) |
| { |
| GError *error = NULL; |
| GByteArray *verinfo = NULL; |
| GByteArray *verinfo2; |
| gint len; |
| guint8 marker = 0x7E; |
| MMPortSubsys subsys = MM_PORT_SUBSYS_TTY; |
| 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 the plugin specifies QCDM is not required, we can right away complete the QCDM |
| * probing task. */ |
| if (!ctx->qcdm_required) { |
| mm_obj_dbg (self, "Maybe a QCDM port, but plugin does not require probing and grabbing..."); |
| /* If we had a port type hint, flag the port as QCDM capable but ignored. Otherwise, |
| * no QCDM capable and not ignored. The outcome is really the same, i.e. the port is not |
| * used any more, but the way it's reported in DBus will be different (i.e. "ignored" vs |
| "unknown" */ |
| if (self->priv->maybe_qcdm) { |
| mm_port_probe_set_result_qcdm (self, TRUE); |
| self->priv->is_ignored = TRUE; |
| } else |
| mm_port_probe_set_result_qcdm (self, FALSE); |
| |
| /* Continue with remaining probings */ |
| probe_step_next (self); |
| return G_SOURCE_REMOVE; |
| } |
| |
| mm_obj_dbg (self, "probing QCDM..."); |
| |
| /* If open, close the AT port */ |
| clear_probe_serial_port (ctx); |
| |
| if (g_str_equal (mm_kernel_device_get_subsystem (self->priv->port), "wwan")) |
| subsys = MM_PORT_SUBSYS_WWAN; |
| |
| /* Open the QCDM port */ |
| ctx->serial = MM_PORT_SERIAL (mm_port_serial_qcdm_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 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) 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 const guint8 quectel_qcdm[] = { |
| 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, |
| 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, |
| 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, |
| 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, |
| }; |
| |
| 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; |
| } |
| |
| /* Observed on a Quectel EG915Q Qualcomm-based device's DIAG port */ |
| for (i = 0; (len >= sizeof (quectel_qcdm)) && (i < len - sizeof (quectel_qcdm)); i++) { |
| if (!memcmp (&data[i], quectel_qcdm, sizeof (quectel_qcdm))) |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| probe_at_parse_response (MMPortSerialAt *port, |
| GAsyncResult *res, |
| MMPortProbe *self) |
| { |
| g_autoptr(GVariant) result = NULL; |
| g_autoptr(GError) result_error = NULL; |
| g_autofree gchar *response = NULL; |
| g_autoptr(GError) command_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_obj_dbg (self, "no need to keep on probing the port for AT support"); |
| ctx->at_result_processor (self, NULL); |
| probe_step_next (self); |
| return; |
| } |
| |
| response = mm_port_serial_at_command_finish (port, res, &command_error); |
| |
| if (!ctx->at_commands->response_processor (ctx->at_commands->command, |
| response, |
| !!ctx->at_commands[1].command, |
| 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)); |
| return; |
| } |
| |
| /* Go on to next command */ |
| ctx->at_commands++; |
| ctx->at_commands_limit--; |
| if (!ctx->at_commands->command || ctx->at_commands_limit == 0) { |
| /* Was it the last command in the group? If so, |
| * end this partial probing */ |
| ctx->at_result_processor (self, NULL); |
| probe_step_next (self); |
| return; |
| } |
| |
| /* Schedule the next command in the probing group */ |
| if (ctx->at_commands_wait_secs == 0) |
| ctx->source_id = g_idle_add ((GSourceFunc) probe_at, self); |
| else { |
| mm_obj_dbg (self, "re-scheduling next command in probing group in %u seconds...", |
| ctx->at_commands_wait_secs); |
| ctx->source_id = g_timeout_add_seconds (ctx->at_commands_wait_secs, (GSourceFunc) probe_at, self); |
| } |
| return; |
| } |
| |
| /* Run result processor. |
| * Note that custom init commands are allowed to not return anything */ |
| ctx->at_result_processor (self, result); |
| probe_step_next (self); |
| } |
| |
| static gboolean |
| 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_obj_dbg (self, "no need to launch probing for AT support"); |
| ctx->at_result_processor (self, NULL); |
| probe_step_next (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)probe_at_parse_response, |
| self); |
| return G_SOURCE_REMOVE; |
| } |
| |
| 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; |
| } |
| |
| /* Continue with remaining probings */ |
| probe_step_next (self); |
| } |
| |
| /***************************************************************/ |
| |
| static void |
| serial_flash_ready (MMPortSerial *port, |
| GAsyncResult *res, |
| MMPortProbe *self) |
| { |
| mm_port_serial_flash_finish (port, res, NULL); |
| |
| /* Continue with remaining probings */ |
| probe_step_next (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_obj_dbg (self, "serial buffer full"); |
| /* 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; |
| |
| subsys = mm_string_uint_map_lookup (port_subsys_map, |
| G_N_ELEMENTS (port_subsys_map), |
| mm_kernel_device_get_subsystem (self->priv->port), |
| MM_PORT_SUBSYS_TTY); |
| |
| 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, |
| mm_serial_parser_v1_remove_echo, |
| 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; |
| } |
| |
| #define PROBE_FLAGS_AT_MASK (MM_PORT_PROBE_AT | \ |
| MM_PORT_PROBE_AT_VENDOR | \ |
| MM_PORT_PROBE_AT_PRODUCT | \ |
| MM_PORT_PROBE_AT_ICERA | \ |
| MM_PORT_PROBE_AT_XMM) |
| |
| #define AT_PROBING_DEFAULT_TRIES 6 |
| |
| static void |
| probe_step (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; |
| |
| /* Cleanup from previous iterations */ |
| ctx->at_result_processor = NULL; |
| ctx->at_commands = NULL; |
| ctx->at_commands_wait_secs = 0; |
| ctx->at_commands_limit = G_MAXUINT; /* run all given AT probes */ |
| |
| switch (ctx->step) { |
| case PROBE_STEP_FIRST: |
| mm_obj_msg (self, "probe step: start"); |
| ctx->step++; |
| /* Fall through */ |
| |
| case PROBE_STEP_AT_CUSTOM_INIT_OPEN_PORT: |
| if ((ctx->flags & MM_PORT_PROBE_AT) && (ctx->at_custom_init && ctx->at_custom_init_finish)) { |
| mm_obj_msg (self, "probe step: AT custom init open port"); |
| ctx->source_id = g_idle_add ((GSourceFunc) serial_open_at, self); |
| return; |
| } |
| ctx->step++; |
| /* Fall through */ |
| |
| case PROBE_STEP_AT_CUSTOM_INIT: |
| /* 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->flags & MM_PORT_PROBE_AT) && (ctx->at_custom_init && ctx->at_custom_init_finish)) { |
| mm_obj_msg (self, "probe step: AT custom init run"); |
| g_assert (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; |
| } |
| ctx->step++; |
| /* Fall through */ |
| |
| case PROBE_STEP_AT_OPEN_PORT: |
| /* If the port has AT probes, but at least one of the AT probes hasn't |
| * completed yet, open the serial port. |
| */ |
| if ((ctx->flags & PROBE_FLAGS_AT_MASK) && |
| ((ctx->flags & PROBE_FLAGS_AT_MASK) != (self->priv->flags & PROBE_FLAGS_AT_MASK))) { |
| mm_obj_msg (self, "probe step: AT open port"); |
| /* We might end up back here after later probe types fail, so make |
| * sure we have a usable AT port. |
| */ |
| if (ctx->serial && !MM_IS_PORT_SERIAL_AT (ctx->serial)) |
| clear_probe_serial_port (ctx); |
| ctx->source_id = g_idle_add ((GSourceFunc) serial_open_at, self); |
| return; |
| } |
| ctx->step++; |
| /* Fall through */ |
| |
| case PROBE_STEP_AT: |
| if ((ctx->flags & MM_PORT_PROBE_AT) && !(self->priv->flags & MM_PORT_PROBE_AT)) { |
| mm_obj_msg (self, "probe step: AT"); |
| /* Prepare AT probing */ |
| if (ctx->at_custom_probe) |
| ctx->at_commands = ctx->at_custom_probe; |
| else { |
| gint at_probe_tries; |
| |
| /* NOTE: update ID_MM_TTY_AT_PROBE_TRIES documentation when changing min/max/default */ |
| at_probe_tries = mm_kernel_device_get_property_as_int (mm_port_probe_peek_port (self), |
| ID_MM_TTY_AT_PROBE_TRIES); |
| /* If no tag, use default number of tries */ |
| if (at_probe_tries <= 0) |
| at_probe_tries = AT_PROBING_DEFAULT_TRIES; |
| ctx->at_commands_limit = MIN (at_probe_tries, (gint) G_N_ELEMENTS (at_probing)); |
| ctx->at_commands = at_probing; |
| } |
| ctx->at_result_processor = probe_at_result_processor; |
| ctx->source_id = g_idle_add ((GSourceFunc) probe_at, self); |
| return; |
| } |
| ctx->step++; |
| /* Fall through */ |
| |
| case PROBE_STEP_AT_VENDOR: |
| /* Vendor requested and not already probed? */ |
| if ((ctx->flags & MM_PORT_PROBE_AT_VENDOR) && !(self->priv->flags & MM_PORT_PROBE_AT_VENDOR)) { |
| mm_obj_msg (self, "probe step: AT vendor"); |
| ctx->at_result_processor = probe_at_vendor_result_processor; |
| ctx->at_commands = vendor_probing; |
| ctx->source_id = g_idle_add ((GSourceFunc) probe_at, self); |
| return; |
| } |
| ctx->step++; |
| /* Fall through */ |
| |
| case PROBE_STEP_AT_PRODUCT: |
| /* Product requested and not already probed? */ |
| if ((ctx->flags & MM_PORT_PROBE_AT_PRODUCT) && !(self->priv->flags & MM_PORT_PROBE_AT_PRODUCT)) { |
| mm_obj_msg (self, "probe step: AT product"); |
| ctx->at_result_processor = probe_at_product_result_processor; |
| ctx->at_commands = product_probing; |
| ctx->source_id = g_idle_add ((GSourceFunc) probe_at, self); |
| return; |
| } |
| ctx->step++; |
| /* Fall through */ |
| |
| case PROBE_STEP_AT_ICERA: |
| /* Icera support check requested and not already done? */ |
| if ((ctx->flags & MM_PORT_PROBE_AT_ICERA) && !(self->priv->flags & MM_PORT_PROBE_AT_ICERA)) { |
| mm_obj_msg (self, "probe step: Icera"); |
| ctx->at_result_processor = 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; |
| ctx->source_id = g_idle_add ((GSourceFunc) probe_at, self); |
| return; |
| } |
| ctx->step++; |
| /* Fall through */ |
| |
| case PROBE_STEP_AT_XMM: |
| /* XMM support check requested and not already done? */ |
| if ((ctx->flags & MM_PORT_PROBE_AT_XMM) && !(self->priv->flags & MM_PORT_PROBE_AT_XMM)) { |
| mm_obj_msg (self, "probe step: XMM"); |
| /* Prepare AT product probing */ |
| ctx->at_result_processor = probe_at_xmm_result_processor; |
| ctx->at_commands = xmm_probing; |
| ctx->source_id = g_idle_add ((GSourceFunc) probe_at, self); |
| return; |
| } |
| ctx->step++; |
| /* Fall through */ |
| |
| case PROBE_STEP_AT_CLOSE_PORT: |
| if (ctx->serial) { |
| mm_obj_msg (self, "probe step: AT close port"); |
| clear_probe_serial_port (ctx); |
| } |
| ctx->step++; |
| /* Fall through */ |
| |
| case PROBE_STEP_QCDM: |
| /* QCDM requested and not already probed? */ |
| if ((ctx->flags & MM_PORT_PROBE_QCDM) && !(self->priv->flags & MM_PORT_PROBE_QCDM)) { |
| mm_obj_msg (self, "probe step: QCDM"); |
| ctx->source_id = g_idle_add ((GSourceFunc) probe_qcdm, self); |
| return; |
| } |
| ctx->step++; |
| /* Fall through */ |
| |
| case PROBE_STEP_QCDM_CLOSE_PORT: |
| if (ctx->serial) { |
| mm_obj_msg (self, "probe step: QCDM close port"); |
| clear_probe_serial_port (ctx); |
| } |
| ctx->step++; |
| /* Fall through */ |
| |
| case PROBE_STEP_QMI: |
| /* QMI probing needed? */ |
| if ((ctx->flags & MM_PORT_PROBE_QMI) && !(self->priv->flags & MM_PORT_PROBE_QMI)) { |
| mm_obj_msg (self, "probe step: QMI"); |
| ctx->source_id = g_idle_add ((GSourceFunc) wdm_probe_qmi, self); |
| return; |
| } |
| ctx->step++; |
| /* Fall through */ |
| |
| case PROBE_STEP_MBIM: |
| /* MBIM probing needed */ |
| if ((ctx->flags & MM_PORT_PROBE_MBIM) && !(self->priv->flags & MM_PORT_PROBE_MBIM)) { |
| mm_obj_msg (self, "probe step: MBIM"); |
| ctx->source_id = g_idle_add ((GSourceFunc) wdm_probe_mbim, self); |
| return; |
| } |
| ctx->step++; |
| /* Fall through */ |
| |
| case PROBE_STEP_LAST: |
| /* All done! */ |
| mm_obj_msg (self, "probe step: done"); |
| port_probe_task_return_boolean (self, TRUE); |
| return; |
| |
| default: |
| g_assert_not_reached (); |
| } |
| } |
| |
| static void |
| probe_step_next (MMPortProbe *self) |
| { |
| PortProbeRunContext *ctx; |
| |
| g_assert (self->priv->task); |
| ctx = g_task_get_task_data (self->priv->task); |
| |
| ctx->step++; |
| probe_step (self); |
| } |
| |
| 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_obj_dbg (self, "requested to cancel all AT probing"); |
| 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, |
| gboolean qcdm_required, |
| 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->step = PROBE_STEP_FIRST; |
| 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->qcdm_required = qcdm_required; |
| 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_obj_dbg (self, "port probing finished: skipping for ignored port"); |
| port_probe_task_return_boolean (self, TRUE); |
| return; |
| } |
| |
| /* If this is a port flagged as a GPS port, don't do any other probing */ |
| if (self->priv->is_gps) { |
| mm_obj_dbg (self, "GPS port detected"); |
| mm_port_probe_set_result_at (self, FALSE); |
| mm_port_probe_set_result_qcdm (self, FALSE); |
| mm_port_probe_set_result_qmi (self, FALSE); |
| mm_port_probe_set_result_mbim (self, FALSE); |
| } |
| |
| /* If this is a port flagged as an audio port, don't do any other probing */ |
| if (self->priv->is_audio) { |
| mm_obj_dbg (self, "audio port detected"); |
| mm_port_probe_set_result_at (self, FALSE); |
| mm_port_probe_set_result_qcdm (self, FALSE); |
| mm_port_probe_set_result_qmi (self, FALSE); |
| mm_port_probe_set_result_mbim (self, FALSE); |
| } |
| |
| /* If this is a port flagged as an XMMRPC port, don't do any other probing */ |
| if (self->priv->is_xmmrpc) { |
| mm_obj_dbg (self, "XMMRPC port detected"); |
| mm_port_probe_set_result_at (self, FALSE); |
| mm_port_probe_set_result_qcdm (self, FALSE); |
| mm_port_probe_set_result_qmi (self, FALSE); |
| mm_port_probe_set_result_mbim (self, FALSE); |
| } |
| |
| /* If this is a port flagged as being an AT port, don't do any other probing */ |
| if (self->priv->maybe_at) { |
| mm_obj_dbg (self, "no QCDM/QMI/MBIM probing in possible AT port"); |
| mm_port_probe_set_result_qcdm (self, FALSE); |
| mm_port_probe_set_result_qmi (self, FALSE); |
| mm_port_probe_set_result_mbim (self, FALSE); |
| } |
| |
| /* If this is a port flagged as being a QCDM port, don't do any other probing */ |
| if (self->priv->maybe_qcdm) { |
| mm_obj_dbg (self, "no AT/QMI/MBIM probing in possible QCDM port"); |
| mm_port_probe_set_result_at (self, FALSE); |
| mm_port_probe_set_result_qmi (self, FALSE); |
| mm_port_probe_set_result_mbim (self, FALSE); |
| } |
| |
| /* If this is a port flagged as being a QMI port, don't do any other probing */ |
| if (self->priv->maybe_qmi) { |
| mm_obj_dbg (self, "no AT/QCDM/MBIM probing in possible QMI port"); |
| mm_port_probe_set_result_at (self, FALSE); |
| mm_port_probe_set_result_qcdm (self, FALSE); |
| mm_port_probe_set_result_mbim (self, FALSE); |
| } |
| |
| /* If this is a port flagged as being a MBIM port, don't do any other probing */ |
| if (self->priv->maybe_mbim) { |
| mm_obj_dbg (self, "no AT/QCDM/QMI probing in possible MBIM port"); |
| mm_port_probe_set_result_at (self, FALSE); |
| mm_port_probe_set_result_qcdm (self, FALSE); |
| mm_port_probe_set_result_qmi (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_obj_dbg (self, "port probing finished: no more probings needed"); |
| 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_obj_dbg (self, "launching port probing: '%s'", probe_list_str); |
| g_free (probe_list_str); |
| |
| if (ctx->flags & PROBE_FLAGS_AT_MASK) { |
| 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); |
| } |
| |
| probe_step (self); |
| } |
| |
| 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) |
| { |
| g_return_val_if_fail (MM_IS_PORT_PROBE (self), FALSE); |
| |
| return (self->priv->flags & MM_PORT_PROBE_QCDM ? |
| self->priv->is_qcdm : |
| FALSE); |
| } |
| |
| gboolean |
| mm_port_probe_is_qmi (MMPortProbe *self) |
| { |
| g_return_val_if_fail (MM_IS_PORT_PROBE (self), FALSE); |
| |
| return (self->priv->flags & MM_PORT_PROBE_QMI ? |
| self->priv->is_qmi : |
| FALSE); |
| } |
| |
| 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) |
| { |
| g_return_val_if_fail (MM_IS_PORT_PROBE (self), FALSE); |
| |
| return (self->priv->flags & MM_PORT_PROBE_MBIM ? |
| self->priv->is_mbim : |
| FALSE); |
| } |
| |
| 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; |
| } |
| |
| gboolean |
| mm_port_probe_is_xmmrpc (MMPortProbe *self) |
| { |
| g_return_val_if_fail (MM_IS_PORT_PROBE (self), FALSE); |
| |
| return self->priv->is_xmmrpc; |
| } |
| |
| gboolean |
| mm_port_probe_list_has_xmmrpc_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_xmmrpc (probe)) |
| return TRUE; |
| } |
| |
| return FALSE; |
| } |
| |
| MMPortGroup |
| mm_port_probe_get_port_group (MMPortProbe *self) |
| { |
| g_return_val_if_fail (MM_IS_PORT_PROBE (self), MM_PORT_GROUP_UNKNOWN); |
| |
| if (self->priv->is_ignored) |
| return MM_PORT_GROUP_IGNORED; |
| |
| return MM_PORT_GROUP_USED; |
| } |
| |
| 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 defined WITH_QMI |
| if (self->priv->flags & MM_PORT_PROBE_QMI && |
| self->priv->is_qmi) |
| return MM_PORT_TYPE_QMI; |
| #endif |
| |
| #if defined WITH_MBIM |
| if (self->priv->flags & MM_PORT_PROBE_MBIM && |
| 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; |
| |
| if (self->priv->is_xmmrpc) |
| return MM_PORT_TYPE_XMMRPC; |
| |
| 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) |
| { |
| g_return_val_if_fail (MM_IS_PORT_PROBE (self), FALSE); |
| |
| return (self->priv->flags & MM_PORT_PROBE_AT_VENDOR ? |
| self->priv->vendor : |
| NULL); |
| } |
| |
| const gchar * |
| mm_port_probe_get_product (MMPortProbe *self) |
| { |
| g_return_val_if_fail (MM_IS_PORT_PROBE (self), FALSE); |
| |
| 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); |
| |
| 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); |
| |
| 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; |
| } |
| |
| 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); |
| } |
| |
| static void |
| initialize_port_type_hints (MMPortProbe *self) |
| { |
| g_autoptr(GString) udev_tags = NULL; |
| guint n_udev_hints = 0; |
| gboolean auto_maybe_qmi = FALSE; |
| gboolean auto_maybe_mbim = FALSE; |
| gboolean auto_is_xmmrpc = FALSE; |
| gboolean auto_maybe_at = FALSE; |
| gboolean auto_maybe_qcdm = FALSE; |
| gboolean auto_ignored = FALSE; |
| |
| #define ADD_HINT_FROM_UDEV_TAG(TAG, FIELD) do { \ |
| if (!self->priv->FIELD && \ |
| mm_kernel_device_get_property_as_boolean (self->priv->port, TAG)) { \ |
| mm_obj_dbg (self, "port type hint detected in udev tag: %s", TAG); \ |
| self->priv->FIELD = TRUE; \ |
| n_udev_hints++; \ |
| if (!udev_tags) \ |
| udev_tags = g_string_new (TAG); \ |
| else \ |
| g_string_append_printf (udev_tags, ", %s", TAG); \ |
| } \ |
| } while (0) |
| |
| /* Process udev-configured port type hints */ |
| ADD_HINT_FROM_UDEV_TAG (ID_MM_PORT_TYPE_GPS, is_gps); |
| ADD_HINT_FROM_UDEV_TAG (ID_MM_PORT_TYPE_AUDIO, is_audio); |
| ADD_HINT_FROM_UDEV_TAG (ID_MM_PORT_TYPE_XMMRPC, is_xmmrpc); |
| ADD_HINT_FROM_UDEV_TAG (ID_MM_PORT_TYPE_AT_PRIMARY, maybe_at); |
| ADD_HINT_FROM_UDEV_TAG (ID_MM_PORT_TYPE_AT_SECONDARY, maybe_at); |
| ADD_HINT_FROM_UDEV_TAG (ID_MM_PORT_TYPE_AT_PPP, maybe_at); |
| ADD_HINT_FROM_UDEV_TAG (ID_MM_PORT_TYPE_QCDM, maybe_qcdm); |
| ADD_HINT_FROM_UDEV_TAG (ID_MM_PORT_TYPE_QMI, maybe_qmi); |
| ADD_HINT_FROM_UDEV_TAG (ID_MM_PORT_TYPE_MBIM, maybe_mbim); |
| |
| /* Warn if more than one given at the same time */ |
| if (n_udev_hints > 1) |
| mm_obj_warn (self, "multiple incompatible port type hints configured via udev: %s", udev_tags->str); |
| |
| /* Process automatic port type hints, and warn if the hint doesn't match the |
| * one provided via udev. The udev-provided hints are always preferred. */ |
| if (!g_strcmp0 (mm_kernel_device_get_subsystem (self->priv->port), "usbmisc")) { |
| const gchar *driver; |
| |
| driver = mm_kernel_device_get_driver (self->priv->port); |
| if (!g_strcmp0 (driver, "qmi_wwan")) { |
| mm_obj_dbg (self, "port may be QMI based on the driver in use"); |
| auto_maybe_qmi = TRUE; |
| } else if (!g_strcmp0 (driver, "cdc_mbim")) { |
| mm_obj_dbg (self, "port may be MBIM based on the driver in use"); |
| auto_maybe_mbim = TRUE; |
| } else { |
| mm_obj_dbg (self, "port may be AT based on the driver in use: %s", driver); |
| auto_maybe_at = TRUE; |
| } |
| } else if (!g_strcmp0 (mm_kernel_device_get_subsystem (self->priv->port), "wwan")) { |
| /* Linux >= 5.14 has at 'type' attribute specifying the type of port */ |
| if (mm_kernel_device_has_attribute (self->priv->port, "type")) { |
| const gchar *type; |
| |
| type = mm_kernel_device_get_attribute (self->priv->port, "type"); |
| if (!g_strcmp0 (type, "AT")) { |
| mm_obj_dbg (self, "port may be AT based on the wwan type attribute"); |
| auto_maybe_at = TRUE; |
| } else if (!g_strcmp0 (type, "MBIM")) { |
| mm_obj_dbg (self, "port may be MBIM based on the wwan type attribute"); |
| auto_maybe_mbim = TRUE; |
| } else if (!g_strcmp0 (type, "XMMRPC")) { |
| mm_obj_dbg (self, "port is XMMRPC based on the wwan type attribute"); |
| auto_is_xmmrpc = TRUE; |
| } else if (!g_strcmp0 (type, "QMI")) { |
| mm_obj_dbg (self, "port may be QMI based on the wwan type attribute"); |
| auto_maybe_qmi = TRUE; |
| } else if (!g_strcmp0 (type, "QCDM")) { |
| mm_obj_dbg (self, "port may be QCDM based on the wwan type attribute"); |
| auto_maybe_qcdm = TRUE; |
| } else if (!g_strcmp0 (type, "FIREHOSE")) { |
| mm_obj_dbg (self, "port may be FIREHOSE based on the wwan type attribute"); |
| auto_ignored = TRUE; |
| } else if (!g_strcmp0 (type, "FASTBOOT")) { |
| mm_obj_dbg (self, "port may be FASTBOOT based on the wwan type attribute"); |
| auto_ignored = TRUE; |
| } |
| } |
| /* Linux 5.13 does not have 'type' attribute yet, match kernel name instead */ |
| else { |
| const gchar *name; |
| |
| name = mm_kernel_device_get_name (self->priv->port); |
| if (g_str_has_suffix (name, "AT")) { |
| mm_obj_dbg (self, "port may be AT based on the wwan device name"); |
| auto_maybe_at = TRUE; |
| } else if (g_str_has_suffix (name, "MBIM")) { |
| mm_obj_dbg (self, "port may be MBIM based on the wwan device name"); |
| auto_maybe_mbim = TRUE; |
| } else if (g_str_has_suffix (name, "QMI")) { |
| mm_obj_dbg (self, "port may be QMI based on the wwan device name"); |
| auto_maybe_qmi = TRUE; |
| } else if (g_str_has_suffix (name, "QCDM")) { |
| mm_obj_dbg (self, "port may be QCDM based on the wwan device name"); |
| auto_maybe_qcdm = TRUE; |
| } else if (g_str_has_suffix (name, "FIREHOSE")) { |
| mm_obj_dbg (self, "port may be FIREHOSE based on the wwan device name"); |
| auto_ignored = TRUE; |
| } |
| } |
| } |
| |
| g_assert ((auto_maybe_qmi + |
| auto_maybe_mbim + |
| auto_is_xmmrpc + |
| auto_maybe_at + |
| auto_maybe_qcdm + |
| auto_ignored) <= 1); |
| |
| #define PROCESS_AUTO_HINTS(TYPE, FIELD) do { \ |
| if (auto_##FIELD) { \ |
| if (n_udev_hints > 0 && !self->priv->FIELD) \ |
| mm_obj_warn (self, "overriding type in possible " TYPE " port with udev tag: %s", udev_tags->str); \ |
| else \ |
| self->priv->FIELD = TRUE; \ |
| } \ |
| } while (0) |
| |
| PROCESS_AUTO_HINTS ("QMI", maybe_qmi); |
| PROCESS_AUTO_HINTS ("MBIM", maybe_mbim); |
| PROCESS_AUTO_HINTS ("XMMRPC", is_xmmrpc); |
| PROCESS_AUTO_HINTS ("AT", maybe_at); |
| PROCESS_AUTO_HINTS ("QCDM", maybe_qcdm); |
| |
| #undef PROCESS_AUTO_HINTS |
| |
| mm_obj_dbg (self, "port type hints loaded: AT %s, QMI %s, MBIM %s, QCDM %s, XMMRPC %s, AUDIO %s, GPS %s", |
| self->priv->maybe_at ? "yes" : "no", |
| self->priv->maybe_qmi ? "yes" : "no", |
| self->priv->maybe_mbim ? "yes" : "no", |
| self->priv->maybe_qcdm ? "yes" : "no", |
| self->priv->is_xmmrpc ? "yes" : "no", |
| self->priv->is_audio ? "yes" : "no", |
| self->priv->is_gps ? "yes" : "no"); |
| |
| /* Regardless of the type, the port may be ignored */ |
| if (mm_kernel_device_get_property_as_boolean (self->priv->port, ID_MM_PORT_IGNORE)) { |
| mm_obj_dbg (self, "port is ignored via udev tag"); |
| self->priv->is_ignored = TRUE; |
| } else if (auto_ignored) { |
| mm_obj_dbg (self, "port is ignored via automatic rules"); |
| self->priv->is_ignored = TRUE; |
| } |
| } |
| |
| /*****************************************************************************/ |
| |
| static gchar * |
| log_object_build_id (MMLogObject *_self) |
| { |
| MMPortProbe *self; |
| |
| self = MM_PORT_PROBE (_self); |
| return g_strdup_printf ("%s/probe", mm_kernel_device_get_name (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); |
| initialize_port_type_hints (self); |
| 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 |
| log_object_iface_init (MMLogObjectInterface *iface) |
| { |
| iface->build_id = log_object_build_id; |
| } |
| |
| 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]); |
| } |