From 81101b014755e98029aeb37593365fb52e4cd46b Mon Sep 17 00:00:00 2001 From: Andrey Skvortsov Date: Tue, 7 Oct 2025 01:18:49 +0300 Subject: [PATCH 1/5] shared-qmi: handle inject time requests from the modem To be able to have quick position fix GNSS engine needs to know - predicted orbits data, - current time, - approximate position. Modem sends indications with requests, when it needs corresponding information. Predicted orbits can be injected using 'mmcli -m any --location-inject-assistance-data='. But to accelerate time to first fix after cold start extra information is needed like time and coarse position. This commit injects UTC time. UTC time inject requests are received, when GPS session is initiated using QMI_LOC_START. Tested on Quectel EG25-G. Signed-off-by: Andrey Skvortsov --- src/mm-shared-qmi.c | 199 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 193 insertions(+), 6 deletions(-) diff --git a/src/mm-shared-qmi.c b/src/mm-shared-qmi.c index 055829b5..8a18c382 100644 --- a/src/mm-shared-qmi.c +++ b/src/mm-shared-qmi.c @@ -91,6 +91,8 @@ typedef struct { guint32 loc_assistance_data_max_file_size; guint32 loc_assistance_data_max_part_size; + gulong loc_assistance_inject_time_req_indication_id; + /* Carrier config helpers */ gboolean config_active_default; GArray *config_list; @@ -119,6 +121,8 @@ private_free (Private *priv) g_object_unref (priv->pds_client); if (priv->loc_location_nmea_indication_id) g_signal_handler_disconnect (priv->loc_client, priv->loc_location_nmea_indication_id); + if (priv->loc_assistance_inject_time_req_indication_id) + g_signal_handler_disconnect (priv->loc_client, priv->loc_assistance_inject_time_req_indication_id); if (priv->loc_client) g_object_unref (priv->loc_client); if (priv->uim_slot_status_indication_id) @@ -5670,6 +5674,7 @@ loc_start_ready (QmiClientLoc *client, { QmiMessageLocRegisterEventsInput *input; QmiMessageLocStartOutput *output; + guint64 event_mask; GError *error = NULL; output = qmi_client_loc_start_finish (client, res, &error); @@ -5691,8 +5696,10 @@ loc_start_ready (QmiClientLoc *client, qmi_message_loc_start_output_unref (output); input = qmi_message_loc_register_events_input_new (); + event_mask = QMI_LOC_EVENT_REGISTRATION_FLAG_NMEA | + QMI_LOC_EVENT_REGISTRATION_FLAG_INJECT_TIME_REQUEST; qmi_message_loc_register_events_input_set_event_registration_mask ( - input, QMI_LOC_EVENT_REGISTRATION_FLAG_NMEA, NULL); + input, event_mask, NULL); qmi_client_loc_register_events (client, input, 10, @@ -6714,6 +6721,172 @@ loc_location_get_predicted_orbits_data_source_indication_timed_out (GTask *task) return G_SOURCE_REMOVE; } +/*****************************************************************************/ +/* Location: Inject UTC time */ + +typedef struct { + guint timeout_id; + gulong indication_id; + QmiClientLoc *client; +} InjectTimeContext; + +static void +inject_time_context_cleanup_action (InjectTimeContext *ctx) +{ + if (ctx->timeout_id) { + g_source_remove (ctx->timeout_id); + ctx->timeout_id = 0; + } + if (ctx->indication_id) { + if (ctx->client) + g_signal_handler_disconnect (ctx->client, ctx->indication_id); + ctx->indication_id = 0; + } +} + +static void +inject_time_context_free (InjectTimeContext *ctx) +{ + inject_time_context_cleanup_action (ctx); + g_clear_object (&ctx->client); + g_slice_free (InjectTimeContext, ctx); +} + +static void +inject_time_finish_task (GTask *_task, + GError *error) +{ + InjectTimeContext *ctx; + g_autoptr(GTask) task = _task; + + ctx = g_task_get_task_data (task); + inject_time_context_cleanup_action (ctx); + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, TRUE); +} + +static gboolean +loc_location_inject_time_indication_timed_out (GTask *task) +{ + inject_time_finish_task (task, g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_ABORTED, + "Operation timed out")); + return G_SOURCE_REMOVE; +} + +static void +loc_location_inject_time_indication_cb (QmiClientLoc *client, + QmiIndicationLocInjectUtcTimeOutput *output, + GTask *task) +{ + QmiLocIndicationStatus status; + GError *error = NULL; + + if (!qmi_indication_loc_inject_utc_time_output_get_indication_status (output, &status, &error)) + g_prefix_error (&error, "QMI operation failed: "); + else + mm_error_from_qmi_loc_indication_status (status, &error); + inject_time_finish_task (task, error); +} + +static void +loc_location_inject_time_ready (QmiClientLoc *client, + GAsyncResult *res, + GTask *task) +{ + g_autoptr(QmiMessageLocInjectUtcTimeOutput) output = NULL; + GError *error = NULL; + + output = qmi_client_loc_inject_utc_time_finish (client, res, &error); + if (!output) { + g_prefix_error (&error, "QMI operation failed: "); + inject_time_finish_task (task, error); + return; + } + + if (!qmi_message_loc_inject_utc_time_output_get_result (output, &error)) { + inject_time_finish_task (task, error); + return; + } +} + +static void +loc_location_inject_time_req_indication_cb (QmiClientLoc *client, + QmiIndicationLocInjectTimeRequestOutput *output, + MMSharedQmi *self) +{ + g_autoptr(QmiMessageLocInjectUtcTimeInput) input = NULL; + guint64 utc_ms = 0; + GTask *task; + InjectTimeContext *ctx; + + task = g_task_new (self, NULL, NULL, NULL); + ctx = g_slice_new0 (InjectTimeContext); + g_task_set_task_data (task, ctx, (GDestroyNotify)inject_time_context_free); + + ctx->client = g_object_ref (client); + + input = qmi_message_loc_inject_utc_time_input_new (); + + utc_ms = g_get_real_time () / 1000; + qmi_message_loc_inject_utc_time_input_set_utc_time (input, utc_ms, NULL); + qmi_message_loc_inject_utc_time_input_set_time_uncertainty (input, 1000, NULL); + qmi_message_loc_inject_utc_time_input_set_time_source (input, QMI_LOC_INJECTED_TIME_SOURCE_UNKNOWN, NULL); + + ctx->timeout_id = g_timeout_add_seconds (10, + (GSourceFunc) loc_location_inject_time_indication_timed_out, + task); + + ctx->indication_id = g_signal_connect (client, + "inject-utc-time", + G_CALLBACK (loc_location_inject_time_indication_cb), + task); + + qmi_client_loc_inject_utc_time ( + QMI_CLIENT_LOC (ctx->client), + input, + 10, + NULL, /* cancellable */ + (GAsyncReadyCallback)loc_location_inject_time_ready, + task); +} + +static void +loc_register_event_inject_req_ready (QmiClientLoc *client, + GAsyncResult *res, + GTask *task) +{ + g_autoptr(QmiMessageLocRegisterEventsOutput) output = NULL; + g_autoptr(GError) error = NULL; + MMSharedQmi *self; + Private *priv; + + output = qmi_client_loc_register_events_finish (client, res, &error); + if (!output) { + mm_obj_warn (client, "QMI operation failed: %s", error->message); + goto out; + } + + if (!qmi_message_loc_register_events_output_get_result (output, &error)) { + mm_obj_warn (client, "Couldn't not register tracking events: %s", error->message); + goto out; + } + + self = g_task_get_source_object (task); + priv = get_private (self); + + g_assert (!priv->loc_assistance_inject_time_req_indication_id); + priv->loc_assistance_inject_time_req_indication_id = + g_signal_connect (client, + "inject-time-request", + G_CALLBACK (loc_location_inject_time_req_indication_cb), + self); +out: + g_task_return_int (task, MM_MODEM_LOCATION_ASSISTANCE_DATA_TYPE_XTRA); + g_object_unref (task); +} + static void loc_location_get_predicted_orbits_data_source_indication_cb (QmiClientLoc *client, QmiIndicationLocGetPredictedOrbitsDataSourceOutput *output, @@ -6772,13 +6945,27 @@ loc_location_get_predicted_orbits_data_source_indication_cb (QmiClientLoc } out: - if (error) + if (error) { g_task_return_error (task, error); - else if (!supported) + g_object_unref (task); + } else if (!supported) { g_task_return_int (task, MM_MODEM_LOCATION_ASSISTANCE_DATA_TYPE_NONE); - else - g_task_return_int (task, MM_MODEM_LOCATION_ASSISTANCE_DATA_TYPE_XTRA); - g_object_unref (task); + g_object_unref (task); + } else { + g_autoptr(QmiMessageLocRegisterEventsInput) input = NULL; + guint64 event_mask; + + event_mask = QMI_LOC_EVENT_REGISTRATION_FLAG_INJECT_TIME_REQUEST; + input = qmi_message_loc_register_events_input_new (); + qmi_message_loc_register_events_input_set_event_registration_mask ( + input, event_mask, NULL); + qmi_client_loc_register_events (client, + input, + 10, + NULL, + (GAsyncReadyCallback) loc_register_event_inject_req_ready, + task); + } } static void From 8ac3dfb9557d01e5a60b61081ec9da7b3b7911fd Mon Sep 17 00:00:00 2001 From: Andrey Skvortsov Date: Fri, 2 Jan 2026 23:09:11 +0300 Subject: [PATCH 2/5] location-cache: add class to store cached location This class allows to preserve cached position across ModemManager restarts. Cached location can be used to improve time to first fix after device start by injecting coarse position into location engine. Since there wasn't any fix yet, position in ModemManager is unknown. To fix this cached location is saved on disk (/var/lib/ModemManager/location.ini) when location session is stopped or ModemManager is stopped and loaded on the next start from the file. install_emptydir is introduced in meson 0.60. Distributions has to create directory to store cached location in packaging, because ModemManager runs under systemd isolation with limited rights and it can't create this directory by itself. Signed-off-by: Andrey Skvortsov --- meson.build | 4 +- src/meson.build | 11 ++ src/mm-location-cache.c | 241 ++++++++++++++++++++++++++++++++++++++++ src/mm-location-cache.h | 57 ++++++++++ 4 files changed, 312 insertions(+), 1 deletion(-) create mode 100644 src/mm-location-cache.c create mode 100644 src/mm-location-cache.h diff --git a/meson.build b/meson.build index f81e5974..71b3c3c1 100644 --- a/meson.build +++ b/meson.build @@ -10,7 +10,7 @@ project( 'c_std=gnu89', 'warning_level=2', ], - meson_version: '>= 0.53.0', + meson_version: '>= 0.60.0', ) meson_version = meson.version() @@ -31,11 +31,13 @@ mm_includedir = get_option('includedir') mm_libdir = get_option('libdir') mm_sbindir = get_option('sbindir') mm_sysconfdir = get_option('sysconfdir') +mm_sharedstatedir = get_option('sharedstatedir') mm_pkgdatadir = mm_datadir / mm_name mm_pkgincludedir = mm_includedir / mm_name mm_pkglibdir = mm_libdir / mm_name mm_pkgsysconfdir = mm_sysconfdir / mm_name +mm_pkgsharedstatedir = mm_sharedstatedir / mm_name mm_glib_name = 'libmm-glib' mm_glib_pkgincludedir = mm_includedir / mm_glib_name diff --git a/src/meson.build b/src/meson.build index 04d199d1..78d8794d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -46,6 +46,7 @@ sources = files( 'mm-cbm-part.c', 'mm-charsets.c', 'mm-error-helpers.c', + 'mm-location-cache.c', 'mm-log.c', 'mm-log-object.c', 'mm-modem-helpers.c', @@ -80,11 +81,16 @@ if enable_systemd_journal private_deps += libsystemd_dep endif +c_args = [ + '-DPKGSTATEDIR="@0@"'.format(mm_pkgsharedstatedir), +] + libhelpers = static_library( 'helpers', sources: sources + enums_sources, include_directories: incs, dependencies: deps + private_deps, + c_args: c_args, ) libhelpers_dep = declare_dependency( @@ -506,6 +512,11 @@ install_data( install_dir: udev_rulesdir, ) +# empty directory for cached position +install_emptydir( + mm_pkgsharedstatedir, +) + if enable_tests subdir('tests') endif diff --git a/src/mm-location-cache.c b/src/mm-location-cache.c new file mode 100644 index 00000000..e27921ff --- /dev/null +++ b/src/mm-location-cache.c @@ -0,0 +1,241 @@ +/* -*- 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) 2026 Andrey Skvortsov + */ + + +#include +#include + +#define _LIBMM_INSIDE_MM +#include + +#include "mm-location-cache.h" +#include "mm-log-object.h" + +#if !defined PKGSTATEDIR +# error PKGSTATEDIR is not defined +#endif + +#define LOCATION_STATE_FILE "location.ini" +#define LOCATION_LAST_POS_GROUP "last_position" +#define LOCATION_LAST_POS_LATITUDE_KEY "latitude" +#define LOCATION_LAST_POS_LONGITUDE_KEY "longitude" + +/*****************************************************************************/ + +static void log_object_iface_init (MMLogObjectInterface *iface); + +G_DEFINE_TYPE_EXTENDED (MMLocationCache, mm_location_cache, G_TYPE_OBJECT, 0, + G_IMPLEMENT_INTERFACE (MM_TYPE_LOG_OBJECT, log_object_iface_init)) + +struct _MMLocationCachePrivate { + MMLocationGpsRaw *gps_raw; + gdouble last_latitude; + gdouble last_longitude; + gchar* filename; +}; + +/*****************************************************************************/ + +static gchar * +log_object_build_id (MMLogObject *_self) +{ + return g_strdup ("location-cache"); +} + +gboolean +mm_location_cache_load_from_file (MMLocationCache *self, + const gchar *file, + GError **error) +{ + g_autoptr(GKeyFile) key_file = g_key_file_new (); + gdouble lat; + gdouble lon; + + self->priv->last_latitude = MM_LOCATION_LATITUDE_UNKNOWN; + self->priv->last_longitude = MM_LOCATION_LONGITUDE_UNKNOWN; + + if (!g_key_file_load_from_file (key_file, file, G_KEY_FILE_NONE, error)) { + g_prefix_error (error, "Error loading cached location from %s: ", file); + return FALSE; + } + + lat = g_key_file_get_double (key_file, + LOCATION_LAST_POS_GROUP, + LOCATION_LAST_POS_LATITUDE_KEY, + error); + if (*error) { + g_prefix_error (error, "Error finding latitude in file: "); + return FALSE; + } + + lon = g_key_file_get_double (key_file, + LOCATION_LAST_POS_GROUP, + LOCATION_LAST_POS_LONGITUDE_KEY, + error); + if (*error) { + g_prefix_error (error, "Error finding longitude in file: "); + return FALSE; + } + + self->priv->last_latitude = lat; + self->priv->last_longitude = lon; + return TRUE; +} + +gboolean +mm_location_cache_load (MMLocationCache *self, + GError **error) +{ + return mm_location_cache_load_from_file (self, self->priv->filename, error); +} + +gboolean +mm_location_cache_save_to_file (MMLocationCache *self, + const gchar *file, + GError **error) +{ + g_autoptr(GKeyFile) key_file = g_key_file_new (); + + if ((self->priv->last_latitude == MM_LOCATION_LATITUDE_UNKNOWN) || + (self->priv->last_longitude == MM_LOCATION_LONGITUDE_UNKNOWN)) { + return TRUE; + } + + key_file = g_key_file_new (); + g_key_file_set_double (key_file, + LOCATION_LAST_POS_GROUP, + LOCATION_LAST_POS_LATITUDE_KEY, + self->priv->last_latitude); + g_key_file_set_double (key_file, + LOCATION_LAST_POS_GROUP, + LOCATION_LAST_POS_LONGITUDE_KEY, + self->priv->last_longitude); + + if (!g_key_file_save_to_file (key_file, file, error)) { + g_prefix_error (error, "Error saving cached location to %s", file); + return FALSE; + } + + return TRUE; +} + +gboolean +mm_location_cache_save (MMLocationCache *self, + GError **error) +{ + return mm_location_cache_save_to_file (self, self->priv->filename, error); +} + +void +mm_location_cache_set_filename (MMLocationCache *self, + const gchar *file) +{ + g_free (self->priv->filename); + self->priv->filename = g_strdup (file); +} + +void +mm_location_cache_update_from_lat_lon (MMLocationCache *self, + const double lat, + const double lon) +{ + if (lat != MM_LOCATION_LATITUDE_UNKNOWN && + lon != MM_LOCATION_LONGITUDE_UNKNOWN) { + self->priv->last_latitude = lat; + self->priv->last_longitude = lon; + } +} + +void +mm_location_cache_update_from_nmea (MMLocationCache *self, + const gchar *trace) +{ + gdouble lat; + gdouble lon; + + g_assert (self->priv->gps_raw != NULL); + if (mm_location_gps_raw_add_trace (self->priv->gps_raw, trace)) { + lat = mm_location_gps_raw_get_latitude (self->priv->gps_raw); + lon = mm_location_gps_raw_get_longitude (self->priv->gps_raw); + mm_location_cache_update_from_lat_lon (self, lat, lon); + } +} + +void +mm_location_cache_get_lat_lon (MMLocationCache *self, + double *lat, + double *lon) +{ + *lat = self->priv->last_latitude; + *lon = self->priv->last_longitude; +} + +MMLocationCache * +mm_location_cache_new (void) +{ + return MM_LOCATION_CACHE (g_object_new (MM_TYPE_LOCATION_CACHE, NULL)); +} + +static void +mm_location_cache_init (MMLocationCache *self) +{ + self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, MM_TYPE_LOCATION_CACHE, MMLocationCachePrivate); + + self->priv->last_longitude = MM_LOCATION_LONGITUDE_UNKNOWN; + self->priv->last_latitude = MM_LOCATION_LATITUDE_UNKNOWN; + self->priv->gps_raw = mm_location_gps_raw_new (); + + self->priv->filename = g_build_path (G_DIR_SEPARATOR_S, PKGSTATEDIR, LOCATION_STATE_FILE, NULL); +} + +static void +finalize (GObject *object) +{ + MMLocationCache *self = MM_LOCATION_CACHE (object); + + g_object_unref (self->priv->gps_raw); + g_free (self->priv->filename); + + G_OBJECT_CLASS (mm_location_cache_parent_class)->finalize (object); +} + +static void +dispose (GObject *object) +{ + g_autoptr(GError) error = NULL; + MMLocationCache *self = MM_LOCATION_CACHE (object); + + if (!mm_location_cache_save (self, &error)) + mm_obj_warn (self, "%s", error->message); + + G_OBJECT_CLASS (mm_location_cache_parent_class)->dispose (object); +} + +static void +log_object_iface_init (MMLogObjectInterface *iface) +{ + iface->build_id = log_object_build_id; +} + +static void +mm_location_cache_class_init (MMLocationCacheClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + g_type_class_add_private (object_class, sizeof (MMLocationCachePrivate)); + + object_class->finalize = finalize; + object_class->dispose = dispose; +} diff --git a/src/mm-location-cache.h b/src/mm-location-cache.h new file mode 100644 index 00000000..6c8ed291 --- /dev/null +++ b/src/mm-location-cache.h @@ -0,0 +1,57 @@ +/* -*- 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) 2026 Andrey Skvortsov + */ + +#ifndef MM_LOCATION_CACHE_H +#define MM_LOCATION_CACHE_H + +#include +#include + +#define MM_TYPE_LOCATION_CACHE (mm_location_cache_get_type ()) +#define MM_LOCATION_CACHE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), MM_TYPE_LOCATION_CACHE, MMLocationCache)) +#define MM_LOCATION_CACHE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), MM_TYPE_LOCATION_CACHE, MMLocationCacheClass)) +#define MM_IS_LOCATION_CACHE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MM_TYPE_LOCATION_CACHE)) +#define MM_IS_LOCATION_CACHE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), MM_TYPE_LOCATION_CACHE)) +#define MM_LOCATION_CACHE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), MM_TYPE_LOCATION_CACHE, MMLocationCacheClass)) + +typedef struct _MMLocationCache MMLocationCache; +typedef struct _MMLocationCacheClass MMLocationCacheClass; +typedef struct _MMLocationCachePrivate MMLocationCachePrivate; + +struct _MMLocationCache { + GObject parent; + MMLocationCachePrivate *priv; +}; + +struct _MMLocationCacheClass { + GObjectClass parent; +}; + +GType mm_location_cache_get_type (void); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (MMLocationCache, g_object_unref) + +MMLocationCache *mm_location_cache_new (void); + +gboolean mm_location_cache_load_from_file (MMLocationCache *self, const gchar *file, GError **error); +gboolean mm_location_cache_load (MMLocationCache *self, GError **error); +gboolean mm_location_cache_save_to_file (MMLocationCache *self, const gchar *file, GError **error); +gboolean mm_location_cache_save (MMLocationCache *self, GError **error); +void mm_location_cache_set_filename (MMLocationCache *self, const gchar *file); +void mm_location_cache_update_from_lat_lon (MMLocationCache *self, const double lat, const double lon); +void mm_location_cache_update_from_nmea (MMLocationCache *self, const gchar *nmea); +void mm_location_cache_get_lat_lon (MMLocationCache *self, double *lat, double *lon); + +#endif /* MM_LOCATION_CACHE_H */ + From a108a0fe7cf8315a414a313e15f47f9404380b69 Mon Sep 17 00:00:00 2001 From: Andrey Skvortsov Date: Fri, 14 Nov 2025 14:43:41 +0300 Subject: [PATCH 3/5] build: Fix meson deprecation warnings following warnings are fixed: Dependency udev found: YES 258 (cached) ../meson.build:202: WARNING: Project targets '>= 0.60.0' but uses feature deprecated since '0.56.0': dependency.get_pkgconfig_variable. use dependency.get_variable(pkgconfig : ...) instead Dependency systemd found: YES 258 (cached) ../meson.build:212: WARNING: Project targets '>= 0.60.0' but uses feature deprecated since '0.56.0': dependency.get_pkgconfig_variable. use dependency.get_variable(pkgconfig : ...) instead Dependency libsystemd found: YES 258 (cached) Dependency polkit-gobject-1 found: YES 126 (cached) ../meson.build:245: WARNING: Project targets '>= 0.60.0' but uses feature deprecated since '0.56.0': dependency.get_pkgconfig_variable. use dependency.get_variable(pkgconfig : ...) instead Dependency mbim-glib found: YES 1.32.0 (cached) Dependency qmi-glib found: YES 1.37.1 (cached) ../meson.build:302: WARNING: Project targets '>= 0.60.0' but uses feature deprecated since '0.56.0': dependency.get_pkgconfig_variable. use dependency.get_variable(pkgconfig : ...) instead Signed-off-by: Andrey Skvortsov --- meson.build | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/meson.build b/meson.build index 71b3c3c1..0198a6d1 100644 --- a/meson.build +++ b/meson.build @@ -170,21 +170,21 @@ glib_deps = declare_dependency( # DBus system directory dbus_dep = dependency('dbus-1') -dbus_interfaces_dir = dbus_dep.get_pkgconfig_variable('interfaces_dir', define_variable: ['datadir', mm_datadir]) -dbus_system_bus_services_dir = dbus_dep.get_pkgconfig_variable('system_bus_services_dir', define_variable: ['datadir', mm_datadir]) +dbus_interfaces_dir = dbus_dep.get_variable(pkgconfig: 'interfaces_dir', pkgconfig_define: ['datadir', mm_datadir]) +dbus_system_bus_services_dir = dbus_dep.get_variable(pkgconfig: 'system_bus_services_dir', pkgconfig_define: ['datadir', mm_datadir]) dbus_policy_dir = get_option('dbus_policy_dir') if dbus_policy_dir == '' - dbus_policy_dir = dbus_dep.get_pkgconfig_variable('sysconfdir', define_variable: ['sysconfdir', mm_sysconfdir]) / 'dbus-1/system.d' + dbus_policy_dir = dbus_dep.get_variable(pkgconfig: 'sysconfdir', pkgconfig_define: ['sysconfdir', mm_sysconfdir]) / 'dbus-1/system.d' endif enable_bash_completion = get_option('bash_completion') if enable_bash_completion bash_completion_dep = dependency('bash-completion') - bash_completion_completionsdir = bash_completion_dep.get_pkgconfig_variable( - 'completionsdir', + bash_completion_completionsdir = bash_completion_dep.get_variable( + pkgconfig: 'completionsdir', # bash-completion 2.10 changed the substitutions - define_variable: bash_completion_dep.version().version_compare('>= 2.10') ? ['datadir', mm_datadir] : ['prefix', mm_prefix], + pkgconfig_define: bash_completion_dep.version().version_compare('>= 2.10') ? ['datadir', mm_datadir] : ['prefix', mm_prefix], ) endif @@ -199,7 +199,7 @@ config_h.set('WITH_UDEV', enable_udev) udev_udevdir = get_option('udevdir') if udev_udevdir == '' assert(enable_udev, 'udevdir must be explicitly given if udev support is disabled') - udev_udevdir = dependency('udev').get_pkgconfig_variable('udevdir') + udev_udevdir = dependency('udev').get_variable(pkgconfig: 'udevdir') endif udev_rulesdir = udev_udevdir / 'rules.d' @@ -209,7 +209,7 @@ install_systemdunitdir = (systemd_systemdsystemunitdir != 'no') if install_systemdunitdir and systemd_systemdsystemunitdir == '' systemd_dep = dependency('systemd', not_found_message: 'systemd required but not found, please provide a valid systemd user unit dir or disable it') - systemd_systemdsystemunitdir = systemd_dep.get_pkgconfig_variable('systemdsystemunitdir', define_variable: ['root_prefix', mm_prefix]) + systemd_systemdsystemunitdir = systemd_dep.get_variable(pkgconfig: 'systemdsystemunitdir', pkgconfig_define: ['root_prefix', mm_prefix]) endif # Suspend/resume support @@ -242,7 +242,7 @@ enable_polkit = (polkit_opt != 'no') if enable_polkit polkit_gobject_dep = dependency('polkit-gobject-1', version: '>= 0.97', not_found_message: 'PolicyKit development headers are required') - polkit_gobject_policydir = polkit_gobject_dep.get_pkgconfig_variable('policydir', define_variable: ['prefix', mm_prefix]) + polkit_gobject_policydir = polkit_gobject_dep.get_variable(pkgconfig: 'policydir', pkgconfig_define: ['prefix', mm_prefix]) policy_conf = {'MM_DEFAULT_USER_POLICY': (polkit_opt == 'permissive' ? 'yes' : 'auth_self_keep')} endif @@ -299,7 +299,7 @@ if enable_qrtr if qmi_glib_dep.type_name() == 'internal' qmi_qrtr_supported = qmi_glib_proj.get_variable('enable_qrtr') else - qmi_qrtr_supported = qmi_glib_dep.get_pkgconfig_variable('qmi_qrtr_supported') + qmi_qrtr_supported = qmi_glib_dep.get_variable(pkgconfig: 'qmi_qrtr_supported') endif assert(qmi_qrtr_supported.to_int().is_odd(), 'Couldn\'t find QRTR support in qmi-glib.') qrtr_glib_dep = dependency('qrtr-glib', version: '>= 1.0.0') From 8b6116fe6ce3bcbac6dafa3cdf8144c7d51ed5c0 Mon Sep 17 00:00:00 2001 From: Andrey Skvortsov Date: Wed, 5 Nov 2025 23:24:20 +0300 Subject: [PATCH 4/5] shared-qmi: handle inject position requests from the modem To be able to have quick position fix GNSS engine needs to know - predicted orbits data, - current time, - approximate position. Modem sends indications with requests, when it needs corresponding information. Predicted orbits can be injected using 'mmcli -m any --location-inject-assistance-data='. But to accelerate time to first fix after cold start extra information is needed like time and coarse position. Inject position requests are received, when GPS session is initiated using QMI_LOC_START. In inject position request modem sends information about its last known position. If last known position is zero, when this is a cold start and inject position is needed to improve TTFF. Last position from the modem is cached in ModemManager and is injected next time, when modem requests it. For example, if modem is restarted. When timestamp on injected position is too old, than position is ignored and location engine does cold start. Therefore current time is used for injected position. When injected position is far away the actual one (for example, after flight), after some time location engine falls back to cold start. Tested on Quectel EG25-G. Signed-off-by: Andrey Skvortsov --- src/mm-shared-qmi.c | 201 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 196 insertions(+), 5 deletions(-) diff --git a/src/mm-shared-qmi.c b/src/mm-shared-qmi.c index 8a18c382..d93698a6 100644 --- a/src/mm-shared-qmi.c +++ b/src/mm-shared-qmi.c @@ -30,6 +30,7 @@ #include "mm-iface-modem.h" #include "mm-iface-modem-3gpp.h" #include "mm-iface-modem-location.h" +#include "mm-location-cache.h" #include "mm-sim-qmi.h" #include "mm-shared-qmi.h" #include "mm-modem-helpers-qmi.h" @@ -92,6 +93,7 @@ typedef struct { guint32 loc_assistance_data_max_part_size; gulong loc_assistance_inject_time_req_indication_id; + gulong loc_assistance_inject_position_req_indication_id; /* Carrier config helpers */ gboolean config_active_default; @@ -104,6 +106,9 @@ typedef struct { gulong uim_slot_status_indication_id; gulong uim_refresh_indication_id; guint uim_refresh_start_timeout_id; + + /* Cached last location from nmea */ + MMLocationCache *location_cache; } Private; static void @@ -123,6 +128,8 @@ private_free (Private *priv) g_signal_handler_disconnect (priv->loc_client, priv->loc_location_nmea_indication_id); if (priv->loc_assistance_inject_time_req_indication_id) g_signal_handler_disconnect (priv->loc_client, priv->loc_assistance_inject_time_req_indication_id); + if (priv->loc_assistance_inject_position_req_indication_id) + g_signal_handler_disconnect (priv->loc_client, priv->loc_assistance_inject_position_req_indication_id); if (priv->loc_client) g_object_unref (priv->loc_client); if (priv->uim_slot_status_indication_id) @@ -135,6 +142,7 @@ private_free (Private *priv) g_object_unref (priv->uim_client); if (priv->uim_refresh_start_timeout_id) g_source_remove (priv->uim_refresh_start_timeout_id); + g_clear_object (&priv->location_cache); if (priv->feature_nas_ssp_acquisition_order_preference_array) g_array_unref (priv->feature_nas_ssp_acquisition_order_preference_array); g_strfreev (priv->loc_assistance_data_servers); @@ -144,7 +152,8 @@ private_free (Private *priv) static Private * get_private (MMSharedQmi *self) { - Private *priv; + g_autoptr(GError) error = NULL; + Private *priv; if (G_UNLIKELY (!private_quark)) private_quark = g_quark_from_static_string (PRIVATE_TAG); @@ -159,6 +168,10 @@ get_private (MMSharedQmi *self) priv->feature_nas_ssp_acquisition_order_preference = FEATURE_UNKNOWN; priv->config_active_i = -1; + priv->location_cache = mm_location_cache_new (); + if (!mm_location_cache_load (priv->location_cache, &error)) + mm_obj_warn (priv->location_cache, "%s", error->message); + /* Setup parent class' MMIfaceModemLocation */ g_assert (MM_SHARED_QMI_GET_INTERFACE (self)->peek_parent_location_interface); priv->iface_modem_location_parent = MM_SHARED_QMI_GET_INTERFACE (self)->peek_parent_location_interface (self); @@ -5160,13 +5173,17 @@ stop_gps_engine (MMSharedQmi *self, GAsyncReadyCallback callback, gpointer user_data) { - GTask *task; - Private *priv; + GTask *task; + Private *priv; + g_autoptr(GError) error = NULL; priv = get_private (self); task = g_task_new (self, NULL, callback, user_data); + if (!mm_location_cache_save (priv->location_cache, &error)) + mm_obj_warn (self, "%s", error->message); + if (priv->pds_client) { QmiMessagePdsSetGpsServiceStateInput *input; @@ -5213,7 +5230,9 @@ pds_location_event_report_indication_cb (QmiClientPds *clie { QmiPdsPositionSessionStatus session_status; const gchar *nmea; + Private *priv; + priv = get_private (MM_SHARED_QMI (self)); if (qmi_indication_pds_event_report_output_get_position_session_status ( output, &session_status, @@ -5228,6 +5247,7 @@ pds_location_event_report_indication_cb (QmiClientPds *clie NULL)) { mm_obj_dbg (self, "[NMEA] %s", nmea); mm_iface_modem_location_gps_update (MM_IFACE_MODEM_LOCATION (self), nmea); + mm_location_cache_update_from_nmea (priv->location_cache, nmea); } } @@ -5237,13 +5257,16 @@ loc_location_nmea_indication_cb (QmiClientLoc *client, MMSharedQmi *self) { const gchar *nmea = NULL; + Private *priv; + priv = get_private (MM_SHARED_QMI (self)); qmi_indication_loc_nmea_output_get_nmea_string (output, &nmea, NULL); if (!nmea) return; mm_obj_dbg (self, "[NMEA] %s", nmea); mm_iface_modem_location_gps_update (MM_IFACE_MODEM_LOCATION (self), nmea); + mm_location_cache_update_from_nmea (priv->location_cache, nmea); } /*****************************************************************************/ @@ -5697,7 +5720,8 @@ loc_start_ready (QmiClientLoc *client, input = qmi_message_loc_register_events_input_new (); event_mask = QMI_LOC_EVENT_REGISTRATION_FLAG_NMEA | - QMI_LOC_EVENT_REGISTRATION_FLAG_INJECT_TIME_REQUEST; + QMI_LOC_EVENT_REGISTRATION_FLAG_INJECT_TIME_REQUEST | + QMI_LOC_EVENT_REGISTRATION_FLAG_INJECT_POSITION_REQUEST; qmi_message_loc_register_events_input_set_event_registration_mask ( input, event_mask, NULL); qmi_client_loc_register_events (client, @@ -6852,6 +6876,164 @@ loc_location_inject_time_req_indication_cb (QmiClientLoc task); } +/*****************************************************************************/ +/* Location: Inject position */ + +typedef struct { + guint timeout_id; + gulong indication_id; + QmiClientLoc *client; +} InjectPositionContext; + +static void +inject_position_context_cleanup_action (InjectPositionContext *ctx) +{ + if (ctx->timeout_id) { + g_source_remove (ctx->timeout_id); + ctx->timeout_id = 0; + } + if (ctx->indication_id) { + if (ctx->client) + g_signal_handler_disconnect (ctx->client, ctx->indication_id); + ctx->indication_id = 0; + } +} + +static void +inject_position_context_free (InjectPositionContext *ctx) +{ + inject_position_context_cleanup_action (ctx); + g_clear_object (&ctx->client); + g_slice_free (InjectPositionContext, ctx); +} + +static void +inject_position_finish_task (GTask *_task, + GError *error) +{ + InjectPositionContext *ctx; + g_autoptr(GTask) task = _task; + + ctx = g_task_get_task_data (task); + inject_position_context_cleanup_action (ctx); + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, TRUE); +} + +static gboolean +loc_location_inject_position_indication_timed_out (GTask *task) +{ + inject_position_finish_task (task, g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_ABORTED, + "Operation timed out")); + return G_SOURCE_REMOVE; +} + +static void +loc_location_inject_position_indication_cb (QmiClientLoc *client, + QmiIndicationLocInjectPositionOutput *output, + GTask *task) +{ + QmiLocIndicationStatus status; + GError *error = NULL; + + if (!qmi_indication_loc_inject_position_output_get_indication_status (output, &status, &error)) + g_prefix_error (&error, "QMI operation failed: "); + else + mm_error_from_qmi_loc_indication_status (status, &error); + inject_position_finish_task (task, error); +} + +static void +loc_location_inject_position_ready (QmiClientLoc *client, + GAsyncResult *res, + GTask *task) +{ + g_autoptr(QmiMessageLocInjectPositionOutput) output = NULL; + GError *error = NULL; + + output = qmi_client_loc_inject_position_finish (client, res, &error); + if (!output) { + g_prefix_error (&error, "QMI operation failed: "); + inject_position_finish_task (task, error); + return; + } + + if (!qmi_message_loc_inject_position_output_get_result (output, &error)) { + inject_position_finish_task (task, error); + return; + } +} + +static void +loc_location_inject_position_req_indication_cb (QmiClientLoc *client, + QmiIndicationLocInjectPositionRequestOutput *output, + MMSharedQmi *self) +{ + g_autoptr(QmiMessageLocInjectPositionInput) input = NULL; + InjectPositionContext *ctx; + Private *priv; + GTask *task; + gdouble lat; + gdouble lon; + guint64 utc_ms = 0; + gdouble last_lat; + gdouble last_lon; + guint64 timestamp; + + qmi_indication_loc_inject_position_request_output_get_latitude (output, &lat, NULL); + qmi_indication_loc_inject_position_request_output_get_longitude (output, &lon, NULL); + qmi_indication_loc_inject_position_request_output_get_utc_timestamp (output, ×tamp, NULL); + + /* check whether modem already has position */ + if ((lat != 0.0) && (lon != 0.0) && (timestamp != 0)) + return; + + priv = get_private (MM_SHARED_QMI (self)); + mm_location_cache_get_lat_lon (priv->location_cache, &last_lat, &last_lon); + if ((last_lat == MM_LOCATION_LATITUDE_UNKNOWN) || + (last_lon == MM_LOCATION_LONGITUDE_UNKNOWN)) + return; + + task = g_task_new (self, NULL, NULL, NULL); + ctx = g_slice_new0 (InjectPositionContext); + g_task_set_task_data (task, ctx, (GDestroyNotify)inject_position_context_free); + + ctx->client = g_object_ref (client); + + input = qmi_message_loc_inject_position_input_new (); + + qmi_message_loc_inject_position_input_set_latitude (input, last_lat, NULL); + qmi_message_loc_inject_position_input_set_longitude (input, last_lon, NULL); + qmi_message_loc_inject_position_input_set_position_source (input, QMI_LOC_POSITION_SOURCE_OTHER, NULL); + + // it's not recommended to advertise accuracy better than 1000 meters to the modem + qmi_message_loc_inject_position_input_set_horizontal_uncertainty_circular (input, 1000, NULL); + qmi_message_loc_inject_position_input_set_horizontal_confidence (input, 68, NULL); + + utc_ms = g_get_real_time () / 1000; + qmi_message_loc_inject_position_input_set_utc_timestamp (input, utc_ms, NULL); + + ctx->timeout_id = g_timeout_add_seconds (10, + (GSourceFunc) loc_location_inject_position_indication_timed_out, + task); + + ctx->indication_id = g_signal_connect (client, + "inject-position", + G_CALLBACK (loc_location_inject_position_indication_cb), + task); + + qmi_client_loc_inject_position ( + QMI_CLIENT_LOC (ctx->client), + input, + 10, + NULL, /* cancellable */ + (GAsyncReadyCallback)loc_location_inject_position_ready, + task); +} + + static void loc_register_event_inject_req_ready (QmiClientLoc *client, GAsyncResult *res, @@ -6882,6 +7064,14 @@ loc_register_event_inject_req_ready (QmiClientLoc *client, "inject-time-request", G_CALLBACK (loc_location_inject_time_req_indication_cb), self); + + g_assert (!priv->loc_assistance_inject_position_req_indication_id); + priv->loc_assistance_inject_position_req_indication_id = + g_signal_connect (client, + "inject-position-request", + G_CALLBACK (loc_location_inject_position_req_indication_cb), + self); + out: g_task_return_int (task, MM_MODEM_LOCATION_ASSISTANCE_DATA_TYPE_XTRA); g_object_unref (task); @@ -6955,7 +7145,8 @@ out: g_autoptr(QmiMessageLocRegisterEventsInput) input = NULL; guint64 event_mask; - event_mask = QMI_LOC_EVENT_REGISTRATION_FLAG_INJECT_TIME_REQUEST; + event_mask = QMI_LOC_EVENT_REGISTRATION_FLAG_INJECT_TIME_REQUEST | + QMI_LOC_EVENT_REGISTRATION_FLAG_INJECT_POSITION_REQUEST; input = qmi_message_loc_register_events_input_new (); qmi_message_loc_register_events_input_set_event_registration_mask ( input, event_mask, NULL); From c0f0c02ac14dc565867163e92a16d44a40b7ea3f Mon Sep 17 00:00:00 2001 From: Andrey Skvortsov Date: Tue, 6 Jan 2026 00:32:59 +0300 Subject: [PATCH 5/5] tests: add tests for location-cache only GGA NMEA sentences are supported by current implementation of MMLocationGpsRaw. RMC and GLL messages are added, but currently test are negative. Signed-off-by: Andrey Skvortsov --- src/tests/meson.build | 1 + src/tests/test-location-cache.c | 453 ++++++++++++++++++++++++++++++++ 2 files changed, 454 insertions(+) create mode 100644 src/tests/test-location-cache.c diff --git a/src/tests/meson.build b/src/tests/meson.build index 18ece913..3804fe63 100644 --- a/src/tests/meson.build +++ b/src/tests/meson.build @@ -10,6 +10,7 @@ test_units = { 'charsets': libhelpers_dep, 'error-helpers': libhelpers_dep, 'kernel-device-helpers': libkerneldevice_dep, + 'location-cache': libhelpers_dep, 'modem-helpers': libhelpers_dep, 'port-scheduler': libport_dep, 'sms-part-3gpp': libhelpers_dep, diff --git a/src/tests/test-location-cache.c b/src/tests/test-location-cache.c new file mode 100644 index 00000000..fcffca0f --- /dev/null +++ b/src/tests/test-location-cache.c @@ -0,0 +1,453 @@ +/*****************************************************************************/ +/* -*- 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) 2026 Andrey Skvortsov + */ + +#include + +#include +#include +#include + +#define _LIBMM_INSIDE_MM +#include +#include "mm-log-test.h" +#include "mm-location-cache.h" + +/*****************************************************************************/ + +static const gdouble reset_lat = 0.1; +static const gdouble reset_lon = 0.2; + +typedef struct { + const gchar *file_content; + const gdouble expect_lat; + const gdouble expect_lon; + const gboolean expect_success; +} LoadTestcase; + +static const LoadTestcase load_testcases[] = { + { + .file_content = "[last_position]\n" + "latitude=48.137\n" + "longitude=11.575\n", + .expect_lat = 48.137, + .expect_lon = 11.575, + .expect_success = TRUE, + }, + { + .file_content = "[last_position]\n" + "longitude=11.575\n" + "latitude=48.137\n", + .expect_lat = 48.137, + .expect_lon = 11.575, + .expect_success = TRUE, + }, + { + .file_content = "[last_position]\n" + "latitude=-27.142\n" + "longitude=-109.314\n", + .expect_lat = -27.142, + .expect_lon = -109.314, + .expect_success = TRUE, + }, + { + .file_content = "[last_position]\n" + "latitude=48.137\n", + .expect_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .expect_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_success = FALSE, + }, + { + .file_content = "[last_position]\n" + "longitude=11.575\n", + .expect_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .expect_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_success = FALSE, + }, + { + .file_content = "[last_position]\n", + .expect_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .expect_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_success = FALSE, + }, + { + .file_content = "To be, or not to be, that is the question...\n", + .expect_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .expect_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_success = FALSE, + }, +}; + +static void +test_location_cache_load (void) +{ + g_autoptr(MMLocationCache) cache = NULL; + gdouble get_lat; + gdouble get_lon; + g_autofree gchar *filename = NULL; + g_autoptr(GFile) loc = NULL; + g_autoptr(GIOStream) stream = NULL; + g_autoptr(GError) err = NULL; + gulong i; + gboolean ret; + + cache = mm_location_cache_new (); + g_assert_nonnull (cache); + + loc = g_file_new_tmp (NULL, (GFileIOStream **)&stream, &err); + filename = g_file_get_path (loc); + g_assert_no_error (err); + g_assert_nonnull (filename); + + mm_location_cache_set_filename (cache, filename); + + for (i = 0; i < G_N_ELEMENTS (load_testcases); i++) { + g_autoptr(GError) error = NULL; + LoadTestcase test = load_testcases[i]; + + g_debug ("load test %ld", i); + g_file_replace_contents (loc, + test.file_content, + strlen (test.file_content), + NULL, /* etag */ + FALSE, /* make backup */ + G_FILE_CREATE_NONE, /* flags */ + NULL, /* new etag */ + NULL, /* cancellable */ + &error); + g_assert_no_error (error); + + /* reset coordinates */ + mm_location_cache_update_from_lat_lon (cache, reset_lat, reset_lon); + ret = mm_location_cache_load_from_file (cache, filename, &error); + g_assert (ret == test.expect_success); + g_assert (test.expect_success ? error == NULL : error != NULL); + + mm_location_cache_get_lat_lon (cache, &get_lat, &get_lon); + g_assert_cmpfloat_with_epsilon (test.expect_lat, get_lat, 0.01); + g_assert_cmpfloat_with_epsilon (test.expect_lon, get_lon, 0.01); + } + + /* try to load missing file */ + g_file_delete (loc, NULL, NULL); + mm_location_cache_update_from_lat_lon (cache, reset_lat, reset_lon); + ret = mm_location_cache_load_from_file (cache, filename, &err); + g_assert (err != NULL); + + mm_location_cache_get_lat_lon (cache, &get_lat, &get_lon); + g_assert_cmpfloat_with_epsilon (MM_LOCATION_LATITUDE_UNKNOWN, get_lat, 0.01); + g_assert_cmpfloat_with_epsilon (MM_LOCATION_LONGITUDE_UNKNOWN, get_lon, 0.01); +} + +/*****************************************************************************/ + +typedef struct { + const gchar *nmea; + const gdouble set_lat; + const gdouble set_lon; + const gdouble expect_lat; + const gdouble expect_lon; +} UpdateTestcase; + +static const UpdateTestcase update_testcases[] = { + { + .nmea = NULL, + .set_lat = 48.137, + .set_lon = 11.575, + .expect_lat = 48.137, + .expect_lon = 11.575, + }, + { + .nmea = NULL, + .set_lat = 48.137, + .set_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_lat = reset_lat, + .expect_lon = reset_lon, + }, + { + .nmea = NULL, + .set_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .set_lon = 11.575, + .expect_lat = reset_lat, + .expect_lon = reset_lon, + }, + /* only GGA NMEA sentences are supported by current implementation */ + { + .nmea = "$GNGLL,4808.2200,N,01134.5000,E,012345.000,A,A*40", + .set_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .set_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_lat = reset_lat, + .expect_lon = reset_lon, + }, + { + .nmea = "$GNRMC,012345.000,A,4808.2200,N,01134.5000,E,1.0,300.5,100817,3.6,E,A,V*6B", + .set_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .set_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_lat = reset_lat, + .expect_lon = reset_lon, + }, + { + .nmea = "$GNGGA,012345.000,4808.2200,N,01134.5000,E,1,12,1.1,-14.1,M,32.0,M,,*5E", + .set_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .set_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_lat = 48.137, + .expect_lon = 11.575, + }, + { + .nmea = NULL, + .set_lat = -27.142, + .set_lon = -109.314, + .expect_lat = -27.142, + .expect_lon = -109.314, + }, + { + .nmea = "$GNGLL,2708.5200,S,10918.8400,W,012345.000,A,A*4E", + .set_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .set_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_lat = reset_lat, + .expect_lon = reset_lon, + }, + { + .nmea = "$GNRMC,012345.000,A,2708.5200,S,10918.8400,W,1.0,300.5,100817,3.6,E,A,V*65", + .set_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .set_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_lat = reset_lat, + .expect_lon = reset_lon, + }, + { + .nmea = "$GNGGA,012345.000,2708.5200,S,10918.8400,W,1,12,1.1,-14.1,M,32.0,M,,*50", + .set_lat = MM_LOCATION_LATITUDE_UNKNOWN, + .set_lon = MM_LOCATION_LONGITUDE_UNKNOWN, + .expect_lat = -27.142, + .expect_lon = -109.314, + }, +}; + +static void +test_location_cache_update (void) +{ + g_autoptr(MMLocationCache) cache = NULL; + gulong i; + + cache = mm_location_cache_new (); + g_assert_nonnull (cache); + + for (i = 0; i < G_N_ELEMENTS (update_testcases); i++) { + gdouble get_lat = 0; + gdouble get_lon = 0; + UpdateTestcase test = update_testcases[i]; + + g_debug ("update test %ld", i); + + /* reset coordinates */ + mm_location_cache_update_from_lat_lon (cache, reset_lat, reset_lon); + + if (test.nmea) + mm_location_cache_update_from_nmea (cache, test.nmea); + else + mm_location_cache_update_from_lat_lon (cache, test.set_lat, test.set_lon); + mm_location_cache_get_lat_lon (cache, &get_lat, &get_lon); + g_assert_cmpfloat_with_epsilon (test.expect_lat, get_lat, 0.01); + g_assert_cmpfloat_with_epsilon (test.expect_lon, get_lon, 0.01); + } +} + +/*****************************************************************************/ + +static gchar * +get_temp_filename (void) +{ + g_autoptr(GError) error = NULL; + gchar *filename; + gint fd; + + fd = g_file_open_tmp (NULL, &filename, &error); + g_assert_no_error (error); + g_assert_nonnull (filename); + + g_close (fd, &error); + g_assert_no_error (error); + + g_unlink (filename); + + return filename; +} + +static void +test_location_cache_save (void) +{ + g_autoptr(MMLocationCache) cache = NULL; + g_autoptr(GError) error = NULL; + g_autofree gchar *filename = NULL; + g_autofree gchar *path = NULL; + gboolean ret; + + cache = mm_location_cache_new (); + g_assert_nonnull (cache); + + filename = get_temp_filename (); + + // write empty cache + g_unlink (filename); + ret = mm_location_cache_save_to_file (cache, filename, &error); + g_assert_true (ret); + g_assert_no_error (error); + + ret = g_file_test (filename, G_FILE_TEST_EXISTS); + g_assert_false (ret); + + // write non-empty cache + g_unlink (filename); + mm_location_cache_update_from_lat_lon (cache, reset_lat, reset_lon); + ret = mm_location_cache_save_to_file (cache, filename, &error); + g_assert_true (ret); + g_assert_no_error (error); + + ret = g_file_test (filename, G_FILE_TEST_EXISTS); + g_assert_true (ret); + g_unlink (filename); + + // write to non-existing path + path = g_build_path (G_DIR_SEPARATOR_S, "missing", "X", "Y", "Z", "location.ini", NULL); + ret = mm_location_cache_save_to_file (cache, path, &error); + g_assert_false (ret); + g_assert (error != NULL); +} + +/*****************************************************************************/ + +typedef struct { + const gdouble set_lat; + const gdouble set_lon; +} SaveLoadTestcase; + +static const SaveLoadTestcase save_load_testcases[] = { + { + .set_lat = 48.137, + .set_lon = 11.575, + }, + { + .set_lat = -27.142, + .set_lon = -109.314, + }, +}; + +static void +test_location_cache_save_load (void) +{ + g_autoptr(MMLocationCache) cache = NULL; + g_autoptr(GError) error = NULL; + g_autofree gchar *filename = NULL; + gboolean ret; + gulong i; + + cache = mm_location_cache_new (); + g_assert_nonnull (cache); + + filename = get_temp_filename (); + mm_location_cache_set_filename (cache, filename); + + for (i = 0; i < G_N_ELEMENTS (save_load_testcases); i++) { + gdouble get_lat = 0; + gdouble get_lon = 0; + SaveLoadTestcase test = save_load_testcases[i]; + + g_debug ("save/load test %ld", i); + + mm_location_cache_update_from_lat_lon (cache, test.set_lat, test.set_lon); + g_unlink (filename); + ret = mm_location_cache_save (cache, &error); + g_assert_true (ret); + g_assert_no_error (error); + + ret = g_file_test (filename, G_FILE_TEST_EXISTS); + g_assert_true (ret); + + mm_location_cache_update_from_lat_lon (cache, reset_lat, reset_lon); + ret = mm_location_cache_load (cache, &error); + g_assert_true (ret); + g_assert_no_error (error); + + mm_location_cache_get_lat_lon (cache, &get_lat, &get_lon); + g_assert_cmpfloat_with_epsilon (test.set_lat, get_lat, 0.01); + g_assert_cmpfloat_with_epsilon (test.set_lon, get_lon, 0.01); + } + + g_unlink (filename); +} + +/*****************************************************************************/ + +static void +test_location_cache_create_dispose (void) +{ + MMLocationCache *cache = NULL; + g_autoptr(GError) error = NULL; + g_autofree gchar *filename = NULL; + gboolean ret; + gdouble get_lat = 0; + gdouble get_lon = 0; + + cache = mm_location_cache_new (); + g_assert_nonnull (cache); + + // to check later, that MMLocationCache object is finalized + g_object_add_weak_pointer (G_OBJECT (cache), (gpointer *) &cache); + + filename = get_temp_filename (); + mm_location_cache_set_filename (cache, filename); + mm_location_cache_update_from_lat_lon (cache, reset_lat, reset_lon); + + // drop the last reference and save cached location + g_object_unref (cache); + g_assert_null (cache); + + // check whether file is created + ret = g_file_test (filename, G_FILE_TEST_EXISTS); + g_assert_true (ret); + + // check file content + cache = mm_location_cache_new (); + g_assert_nonnull (cache); + + mm_location_cache_set_filename (cache, filename); + ret = mm_location_cache_load (cache, &error); + g_assert_true (ret); + g_assert_no_error (error); + + mm_location_cache_get_lat_lon (cache, &get_lat, &get_lon); + g_assert_cmpfloat_with_epsilon (reset_lat, get_lat, 0.01); + g_assert_cmpfloat_with_epsilon (reset_lon, get_lon, 0.01); + + g_object_unref (cache); + g_unlink (filename); +} + +/*****************************************************************************/ + +int main (int argc, char **argv) +{ + setlocale (LC_ALL, ""); + + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/MM/location-cache/update", test_location_cache_update); + g_test_add_func ("/MM/location-cache/load", test_location_cache_load); + g_test_add_func ("/MM/location-cache/save", test_location_cache_save); + g_test_add_func ("/MM/location-cache/save_load", test_location_cache_save_load); + g_test_add_func ("/MM/location-cache/create_dispose", test_location_cache_create_dispose); + + return g_test_run (); +}