/* * Copyright (C) 2021 Dylan Van Assche * * SPDX-License-Identifier: GPL-3.0-or-later */ #include "gnss.h" #include "manager.h" #include "at.h" #include #include #include #define BUFFER_SIZE 256 #define UPLOAD_DELAY_US 10000 #define UPLOAD_TIMEOUT_S 10 #define RESCHEDULE_IN_SECS 30 static void gnss_step(struct EG25Manager *manager); gboolean gnss_upload_assistance_data(struct EG25Manager *manager) { if (!manager->gnss_assistance_enabled) { g_message("GNSS assistance is disabled!"); return FALSE; } if (manager->gnss_assistance_step < EG25_GNSS_STEP_LAST) { g_warning("GNSS assistance data upload already in process (%d/%d)", manager->gnss_assistance_step, EG25_GNSS_STEP_LAST); return FALSE; } /* ModemManager's Location is only available after unlocking */ if(!manager->mm_location) { g_message ("Rescheduling upload since Location interface is not available, in %ds", RESCHEDULE_IN_SECS); manager->gnss_assistance_step = EG25_GNSS_STEP_LAST; return TRUE; } manager->gnss_assistance_step = EG25_GNSS_STEP_FIRST; gnss_step(manager); return FALSE; } void gnss_init(struct EG25Manager *manager, toml_table_t *config) { toml_datum_t enabled; toml_datum_t url; toml_datum_t file; g_autoptr (GError) error = NULL; /* * GNSS assistance is an optional feature, you can disable it * if you want in the configuration file. * In case the configuration is missing, we assume GNSS assistance * to be disabled. */ enabled = toml_bool_in(config, "enabled"); manager->gnss_assistance_enabled = FALSE; if (enabled.ok) manager->gnss_assistance_enabled = enabled.u.b; if (!manager->gnss_assistance_enabled) { g_message("GNSS assistance is disabled!"); return; } url = toml_string_in(config, "url"); if (url.ok) manager->gnss_assistance_url = url.u.s; else g_error("GNSS assistance server URL is missing from config file"); file = toml_string_in(config, "file"); if (file.ok) manager->gnss_assistance_file = file.u.s; else g_error("GNSS assistance file name is missing from config file"); /* Create temporary file to store assistance data */ manager->gnss_assistance_fd = g_file_open_tmp(NULL, NULL, &error); if (error != NULL) g_error ("Unable to create temporary file: %s", error->message); /* Initialize state and schedule upload */ manager->gnss_assistance_step = EG25_GNSS_STEP_LAST; g_timeout_add_seconds(RESCHEDULE_IN_SECS, G_SOURCE_FUNC(gnss_upload_assistance_data), manager); } void gnss_destroy(struct EG25Manager *manager) { close(manager->gnss_assistance_fd); } /******************************************************************************/ #ifdef HAVE_MMGLIB static void disable_mm_gnss(struct EG25Manager *manager) { MMModemLocationSource sources; gboolean signals_location; g_autoptr (GError) error = NULL; sources = mm_modem_location_get_enabled(manager->mm_location); signals_location = mm_modem_location_signals_location(manager->mm_location); manager->gnss_sources = EG25_GNSS_SOURCE_NONE; /* Save GNSS engine state */ if (sources & MM_MODEM_LOCATION_SOURCE_GPS_NMEA) manager->gnss_sources |= EG25_GNSS_SOURCE_NMEA; else manager->gnss_sources &= ~EG25_GNSS_SOURCE_NMEA; if (sources & MM_MODEM_LOCATION_SOURCE_GPS_RAW) manager->gnss_sources |= EG25_GNSS_SOURCE_RAW; else manager->gnss_sources &= ~EG25_GNSS_SOURCE_RAW; if (sources & MM_MODEM_LOCATION_SOURCE_GPS_UNMANAGED) manager->gnss_sources |= EG25_GNSS_SOURCE_UNMANAGED; else manager->gnss_sources &= ~EG25_GNSS_SOURCE_UNMANAGED; /* Disable GNSS engine */ sources &= ~MM_MODEM_LOCATION_SOURCE_GPS_RAW; sources &= ~MM_MODEM_LOCATION_SOURCE_GPS_NMEA; sources &= ~MM_MODEM_LOCATION_SOURCE_GPS_UNMANAGED; mm_modem_location_setup_sync(manager->mm_location, sources, signals_location, NULL, &error); if (error != NULL) { g_warning("Unable to disable GNSS engine through ModemManager: %s", error->message); } } #endif static void disable_at_gnss_cb(struct EG25Manager *manager, const char *response) { /* Clear QGPSEND AT command and process next */ at_next_command(manager); /* Go to the next step */ manager->gnss_assistance_step++; gnss_step(manager); } static void state_at_gnss_cb(struct EG25Manager *manager, const char *response) { struct AtCommand *at_cmd = manager->at_cmds ? g_list_nth_data(manager->at_cmds, 0) : NULL; if (!at_cmd) return; /* Parse GNSS engine status and disable it if needed */ if (strstr(response, "QGPS: 1")) { manager->gnss_sources |= EG25_GNSS_SOURCE_QGPS; g_free(at_cmd->value); g_free(at_cmd->cmd); at_cmd->expected = NULL; at_cmd->subcmd = NULL; at_cmd->value = NULL; at_cmd->cmd = g_strdup("QGPSEND"); at_cmd->callback = disable_at_gnss_cb; at_send_command(manager); } /* QGPS is already disabled, move on to next step */ else { at_next_command(manager); manager->gnss_sources &= ~EG25_GNSS_SOURCE_QGPS; manager->gnss_assistance_step++; gnss_step(manager); } } static void state_at_gnss(struct EG25Manager *manager) { /* Asynchronously send AT command to query status of GNSS engine */ at_append_command(manager, "QGPS?", NULL, NULL, NULL, state_at_gnss_cb); at_send_command(manager); } /******************************************************************************/ static void fetch_assistance_data(struct EG25Manager *manager) { CURLcode response; curl_off_t downloaded; CURL *curl = NULL; g_autofree gchar *url = NULL; FILE *tmp_file = NULL; gchar errbuf[CURL_ERROR_SIZE]; errbuf[0] = 0; /* Fetch assistance data with curl */ tmp_file = fdopen(manager->gnss_assistance_fd, "wb+"); if (tmp_file == NULL) { g_critical("Unable to open file to save assistance data: %s", g_strerror(errno)); goto bail; } lseek(manager->gnss_assistance_fd, 0, SEEK_SET); ftruncate(manager->gnss_assistance_fd, 0); url = g_strconcat(manager->gnss_assistance_url, "/", manager->gnss_assistance_file, NULL); curl = curl_easy_init(); if (!curl) { g_critical("Unable to initialize curl"); goto bail; } curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NULL); curl_easy_setopt(curl, CURLOPT_WRITEDATA, tmp_file); curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); response = curl_easy_perform(curl); if (response != CURLE_OK) { g_warning("Unable to fetch GNSS assistance data from %s: %s", url, strlen(errbuf) ? errbuf : curl_easy_strerror(response)); goto bail; } response = curl_easy_getinfo(curl, CURLINFO_SIZE_DOWNLOAD_T, &downloaded); if (response) { g_critical("Unable to get number of downloaded bytes from curl"); goto bail; } else if (downloaded <= 0) { g_warning("Downloaded empty assistance data file"); goto bail; } g_message("Fetching GNSS assistance data from %s was successfull", url); fflush(tmp_file); curl_easy_cleanup(curl); g_free(url); /* Go to the next step */ manager->gnss_assistance_step++; gnss_step(manager); return; bail: if (curl != NULL) curl_easy_cleanup(curl); manager->gnss_assistance_step = EG25_GNSS_STEP_LAST; } /******************************************************************************/ static void init_assistance_data_upload_ready(struct EG25Manager *manager, const char *response) { /* Search for 'CONNECT' in response to start upload */ if (strstr(response, "CONNECT")) { g_message("Modem ready for GNSS assistance data upload"); manager->gnss_assistance_step++; gnss_step(manager); } } static void init_assistance_data_upload_start(struct EG25Manager *manager, const char *response) { gchar value[BUFFER_SIZE]; off_t size; /* Process AT response */ at_process_result(manager, response); /* Get file size in bytes */ size = lseek(manager->gnss_assistance_fd, 0, SEEK_END); if (size == -1) { g_critical("gnss: unable to read size of xtra data file: %s", g_strerror(errno)); manager->gnss_assistance_step = EG25_GNSS_STEP_LAST; return; } lseek(manager->gnss_assistance_fd, 0, SEEK_SET); /* Start upload */ g_snprintf(value, BUFFER_SIZE, "\"RAM:%s\",%ld,%d", manager->gnss_assistance_file, size, UPLOAD_TIMEOUT_S); g_message("Initiate GNSS assistance data upload: %s", value); at_append_command(manager, "QFUPL", NULL, value, NULL, init_assistance_data_upload_ready); at_send_command(manager); } static void init_assistance_data_upload(struct EG25Manager *manager) { /* * Delete all previous GNSS assistance data files in RAM * and start uploading the latest one to RAM. */ at_append_command(manager, "QFDEL", NULL, "\"RAM:*\"\r\n", NULL, init_assistance_data_upload_start); at_send_command(manager); } static void upload_assistance_data(struct EG25Manager *manager) { gint error; glong written_total = 0; gint ret; struct stat sb; if (fstat(manager->gnss_assistance_fd, &sb) != 0) { g_critical("gnss: unable to stat xtra data file: %s", g_strerror(errno)); /* Make sure the upload times out and the modem goes back to AT command mode */ sleep(UPLOAD_TIMEOUT_S + 1); manager->gnss_assistance_step = EG25_GNSS_STEP_LAST; return; } do { errno = 0; /* Copy downloaded XTRA assistance data to the modem over serial */ ret = sendfile(manager->at_fd, manager->gnss_assistance_fd, &written_total, sb.st_size); error = errno; usleep(UPLOAD_DELAY_US); } while ((!error && written_total < sb.st_size) || (ret == -1 && error == EAGAIN)); /* Clear QFUPL AT command and process next */ at_next_command(manager); /* Go to the next step if successful */ if (!error) { g_message("Successfully uploaded %ld bytes to the modem", written_total); manager->gnss_assistance_step++; gnss_step(manager); } else { g_critical("Unable to upload xtra data: %s", g_strerror(error)); manager->gnss_assistance_step = EG25_GNSS_STEP_LAST; } } static void finish_assistance_data_upload_cb(struct EG25Manager *manager, const char *response) { /* Process response */ at_process_result(manager, response); g_message("GNSS assistance data upload finished"); /* Go to the next step */ manager->gnss_assistance_step++; gnss_step(manager); } static void finish_assistance_data_upload(struct EG25Manager *manager) { gchar value[BUFFER_SIZE]; GDateTime *datetime; gchar *timestring; /* Configure GNSS assistance clock to current system time (UTC) */ datetime = g_date_time_new_now_utc(); timestring = g_date_time_format(datetime, "0,\"%Y/%m/%d,%H:%M:%S\"\r\n"); g_message("Setting GNSS assistance UTC clock to: %s", timestring); at_append_command(manager, "QGPSXTRATIME", NULL, timestring, NULL, at_process_result); /* Configure GNSS engine to use uploaded GNSS assistance data */ g_snprintf(value, BUFFER_SIZE, "\"RAM:%s\"\r\n", manager->gnss_assistance_file); g_message("Setting GNSS assistance file to: %s", value); at_append_command(manager, "QGPSXTRADATA", NULL, value, NULL, finish_assistance_data_upload_cb); at_send_command(manager); } /******************************************************************************/ #ifdef HAVE_MMGLIB static void enable_mm_gnss(struct EG25Manager *manager) { g_autoptr (GError) error = NULL; MMModemLocationSource sources = mm_modem_location_get_enabled(manager->mm_location); gboolean signal_location = mm_modem_location_signals_location(manager->mm_location); if (manager->gnss_sources & EG25_GNSS_SOURCE_UNMANAGED) sources |= MM_MODEM_LOCATION_SOURCE_GPS_UNMANAGED; if (manager->gnss_sources & EG25_GNSS_SOURCE_NMEA) sources |= MM_MODEM_LOCATION_SOURCE_GPS_NMEA; if (manager->gnss_sources & EG25_GNSS_SOURCE_RAW) sources |= MM_MODEM_LOCATION_SOURCE_GPS_RAW; mm_modem_location_setup_sync(manager->mm_location, sources, signal_location, NULL, &error); if (error != NULL) g_warning("Unable to enable GNSS engine through ModemManager: %s", error->message); } #endif static void enable_at_gnss_cb(struct EG25Manager *manager, const char *response) { manager->gnss_assistance_step++; gnss_step(manager); } static void enable_at_gnss(struct EG25Manager *manager) { if (manager->gnss_sources & EG25_GNSS_SOURCE_QGPS) { at_append_command(manager, "QGPS", NULL, "1", NULL, enable_at_gnss_cb); at_send_command(manager); return; } manager->gnss_assistance_step++; gnss_step(manager); } /******************************************************************************/ void gnss_step(struct EG25Manager *manager) { switch(manager->gnss_assistance_step) { case EG25_GNSS_STEP_FIRST: manager->gnss_assistance_step++; g_message("GNSS assistance upload started..."); /* fall-through */ #ifdef HAVE_MMGLIB case EG25_GNSS_STEP_MM_GNSS_DISABLE: g_message("GNSS assistance upload step (%d/%d): " "disabling GNSS engine through ModemManager", manager->gnss_assistance_step, EG25_GNSS_STEP_LAST); disable_mm_gnss(manager); manager->gnss_assistance_step++; /* fall-through */ #endif case EG25_GNSS_STEP_AT_GNSS_DISABLE: g_message("GNSS assistance upload step (%d/%d): " "disabling GNSS engine through AT+QGPS", manager->gnss_assistance_step, EG25_GNSS_STEP_LAST); state_at_gnss(manager); break; case EG25_GNSS_STEP_FETCH_ASSISTANCE_DATA: g_message("GNSS assistance upload step (%d/%d): " "fetching assistance data", manager->gnss_assistance_step, EG25_GNSS_STEP_LAST); fetch_assistance_data(manager); break; case EG25_GNSS_STEP_INIT_UPLOAD: g_message("GNSS assistance upload step (%d/%d): initiating upload", manager->gnss_assistance_step, EG25_GNSS_STEP_LAST); init_assistance_data_upload(manager); break; case EG25_GNSS_STEP_UPLOAD: g_message("GNSS assistance upload step (%d/%d): " "uploading assistance data", manager->gnss_assistance_step, EG25_GNSS_STEP_LAST); upload_assistance_data(manager); break; case EG25_GNSS_STEP_FINISH_UPLOAD: g_message("GNSS assistance upload step (%d/%d): finishing upload", manager->gnss_assistance_step, EG25_GNSS_STEP_LAST); finish_assistance_data_upload(manager); break; #ifdef HAVE_MMGLIB case EG25_GNSS_STEP_MM_GNSS_ENABLE: g_message("GNSS assistance upload step (%d/%d): " "re-enabling GNSS through ModemManager", manager->gnss_assistance_step, EG25_GNSS_STEP_LAST); enable_mm_gnss(manager); manager->gnss_assistance_step++; /* fall-through */ #endif case EG25_GNSS_STEP_AT_QGPS_ENABLE: g_message("GNSS assistance upload step (%d/%d): " "re-enabling GNSS through AT+QGPS", manager->gnss_assistance_step, EG25_GNSS_STEP_LAST); enable_at_gnss(manager); break; case EG25_GNSS_STEP_LAST: g_message("GNSS assistance upload step (%d/%d): finished", manager->gnss_assistance_step, EG25_GNSS_STEP_LAST); break; } }