/* vim: set et ts=8 sw=8: */ /* * Copyright 2014 Red Hat, Inc. * Copyright 2015 Ankit (Verma) * * Geoclue 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. * * Geoclue 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. * * You should have received a copy of the GNU General Public License along * with Geoclue; if not, write to the Free Software Foundation, Inc., * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * Authors: Zeeshan Ali (Khattak) * Ankit (Verma) */ #include #include #include "gclue-nmea-source.h" #include "gclue-location.h" #include "config.h" #include "gclue-enum-types.h" #include #include #include #include #include typedef struct AvahiServiceInfo AvahiServiceInfo; struct _GClueNMEASourcePrivate { GSocketConnection *connection; GSocketClient *client; GCancellable *cancellable; AvahiClient *avahi_client; AvahiServiceInfo *active_service; /* List of all services but only the most accurate one is used. */ GList *all_services; }; G_DEFINE_TYPE_WITH_CODE (GClueNMEASource, gclue_nmea_source, GCLUE_TYPE_LOCATION_SOURCE, G_ADD_PRIVATE (GClueNMEASource)) static gboolean gclue_nmea_source_start (GClueLocationSource *source); static gboolean gclue_nmea_source_stop (GClueLocationSource *source); static void connect_to_service (GClueNMEASource *source); static void disconnect_from_service (GClueNMEASource *source); struct AvahiServiceInfo { char *identifier; char *host_name; guint16 port; GClueAccuracyLevel accuracy; guint64 timestamp; }; static void avahi_service_free (gpointer data) { AvahiServiceInfo *service = (AvahiServiceInfo *) data; g_free (service->identifier); g_free (service->host_name); g_slice_free(AvahiServiceInfo, service); } static AvahiServiceInfo * avahi_service_new (const char *identifier, const char *host_name, guint16 port, GClueAccuracyLevel accuracy) { GTimeVal tv; AvahiServiceInfo *service = g_slice_new0 (AvahiServiceInfo); service->identifier = g_strdup (identifier); service->host_name = g_strdup (host_name); service->port = port; service->accuracy = accuracy; g_get_current_time (&tv); service->timestamp = tv.tv_sec; return service; } static gint compare_avahi_service_by_identifier (gconstpointer a, gconstpointer b) { AvahiServiceInfo *first, *second; first = (AvahiServiceInfo *) a; second = (AvahiServiceInfo *) b; return g_strcmp0 (first->identifier, second->identifier); } static gint compare_avahi_service_by_accuracy_n_time (gconstpointer a, gconstpointer b) { AvahiServiceInfo *first, *second; gint diff; first = (AvahiServiceInfo *) a; second = (AvahiServiceInfo *) b; diff = second->accuracy - first->accuracy; if (diff == 0) return first->timestamp - second->timestamp; return diff; } static gboolean reconnection_required (GClueNMEASource *source) { GClueNMEASourcePrivate *priv = source->priv; /* Basically, reconnection is required if either * * 1. service in use went down. * 2. a more accurate service than one currently in use, is now * available. */ return (priv->active_service != NULL && (priv->all_services == NULL || priv->active_service != priv->all_services->data)); } static void reconnect_service (GClueNMEASource *source) { if (!reconnection_required (source)) return; disconnect_from_service (source); connect_to_service (source); } static void refresh_accuracy_level (GClueNMEASource *source) { GClueAccuracyLevel new, existing; existing = gclue_location_source_get_available_accuracy_level (GCLUE_LOCATION_SOURCE (source)); if (source->priv->all_services != NULL) { AvahiServiceInfo *service; service = (AvahiServiceInfo *) source->priv->all_services->data; new = service->accuracy; } else { new = GCLUE_ACCURACY_LEVEL_NONE; } if (new != existing) { g_debug ("Available accuracy level from %s: %u", G_OBJECT_TYPE_NAME (source), new); g_object_set (G_OBJECT (source), "available-accuracy-level", new, NULL); } } static void add_new_service (GClueNMEASource *source, const char *name, const char *host_name, uint16_t port, AvahiStringList *txt) { GClueAccuracyLevel accuracy = GCLUE_ACCURACY_LEVEL_NONE; AvahiServiceInfo *service; AvahiStringList *node; guint n_services; char *key, *value; GEnumClass *enum_class; GEnumValue *enum_value; node = avahi_string_list_find (txt, "accuracy"); if (node == NULL) { g_warning ("No `accuracy` key inside TXT record"); accuracy = GCLUE_ACCURACY_LEVEL_EXACT; goto CREATE_SERVICE; } avahi_string_list_get_pair (node, &key, &value, NULL); if (value == NULL) { g_warning ("There is no value for `accuracy` inside TXT " "record"); accuracy = GCLUE_ACCURACY_LEVEL_EXACT; goto CREATE_SERVICE; } enum_class = g_type_class_ref (GCLUE_TYPE_ACCURACY_LEVEL); enum_value = g_enum_get_value_by_nick (enum_class, value); g_type_class_unref (enum_class); if (enum_value == NULL) { g_warning ("Invalid `accuracy` value `%s` inside TXT records.", value); accuracy = GCLUE_ACCURACY_LEVEL_EXACT; goto CREATE_SERVICE; } accuracy = enum_value->value; CREATE_SERVICE: service = avahi_service_new (name, host_name, port, accuracy); source->priv->all_services = g_list_insert_sorted (source->priv->all_services, service, compare_avahi_service_by_accuracy_n_time); refresh_accuracy_level (source); reconnect_service (source); n_services = g_list_length (source->priv->all_services); g_debug ("No. of _nmea-0183._tcp services %u", n_services); } static void remove_service (GClueNMEASource *source, AvahiServiceInfo *service) { guint n_services = 0; avahi_service_free (service); source->priv->all_services = g_list_remove (source->priv->all_services, service); n_services = g_list_length (source->priv->all_services); g_debug ("No. of _nmea-0183._tcp services %u", n_services); refresh_accuracy_level (source); reconnect_service (source); } static void remove_service_by_name (GClueNMEASource *source, const char *name) { AvahiServiceInfo *service; GList *item; /* only `name` is required here */ service = avahi_service_new (name, NULL, 0, GCLUE_ACCURACY_LEVEL_NONE); item = g_list_find_custom (source->priv->all_services, service, compare_avahi_service_by_identifier); avahi_service_free (service); if (item == NULL) return; remove_service (source, item->data); } static void resolve_callback (AvahiServiceResolver *service_resolver, AvahiIfIndex interface G_GNUC_UNUSED, AvahiProtocol protocol G_GNUC_UNUSED, AvahiResolverEvent event, const char *name, const char *type, const char *domain, const char *host_name, const AvahiAddress *address, uint16_t port, AvahiStringList *txt, AvahiLookupResultFlags flags, void *user_data) { const char *errorstr; /* FIXME: check with Avahi devs whether this is really needed. */ g_return_if_fail (service_resolver != NULL); switch (event) { case AVAHI_RESOLVER_FAILURE: { AvahiClient *avahi_client = avahi_service_resolver_get_client (service_resolver); errorstr = avahi_strerror (avahi_client_errno (avahi_client)); g_warning ("(Resolver) Failed to resolve service '%s' " "of type '%s' in domain '%s': %s", name, type, domain, errorstr); break; } case AVAHI_RESOLVER_FOUND: g_debug ("Service %s:%u resolved", host_name, port); add_new_service (GCLUE_NMEA_SOURCE (user_data), name, host_name, port, txt); break; } avahi_service_resolver_free (service_resolver); } static void client_callback (AvahiClient *avahi_client, AvahiClientState state, void *user_data) { GClueNMEASourcePrivate *priv = GCLUE_NMEA_SOURCE (user_data)->priv; g_return_if_fail (avahi_client != NULL); priv->avahi_client = avahi_client; if (state == AVAHI_CLIENT_FAILURE) { const char *errorstr = avahi_strerror (avahi_client_errno (avahi_client)); g_warning ("Avahi client failure: %s", errorstr); } } static void browse_callback (AvahiServiceBrowser *service_browser, AvahiIfIndex interface, AvahiProtocol protocol, AvahiBrowserEvent event, const char *name, const char *type, const char *domain, AvahiLookupResultFlags flags G_GNUC_UNUSED, void *user_data) { GClueNMEASourcePrivate *priv = GCLUE_NMEA_SOURCE (user_data)->priv; const char *errorstr; /* FIXME: check with Avahi devs whether this is really needed. */ g_return_if_fail (service_browser != NULL); switch (event) { case AVAHI_BROWSER_FAILURE: errorstr = avahi_strerror (avahi_client_errno (avahi_service_browser_get_client (service_browser))); g_warning ("Avahi service browser Error %s", errorstr); return; case AVAHI_BROWSER_NEW: { AvahiServiceResolver *service_resolver; g_debug ("Service '%s' of type '%s' found in domain '%s'", name, type, domain); service_resolver = avahi_service_resolver_new (priv->avahi_client, interface, protocol, name, type, domain, AVAHI_PROTO_UNSPEC, 0, resolve_callback, user_data); if (service_resolver == NULL) { errorstr = avahi_strerror (avahi_client_errno (priv->avahi_client)); g_warning ("Failed to resolve service '%s': %s", name, errorstr); } break; } case AVAHI_BROWSER_REMOVE: g_debug ("Service '%s' of type '%s' in domain '%s' removed " "from the list of available NMEA services", name, type, domain); remove_service_by_name (GCLUE_NMEA_SOURCE (user_data), name); break; case AVAHI_BROWSER_ALL_FOR_NOW: case AVAHI_BROWSER_CACHE_EXHAUSTED: g_debug ("Avahi Service Browser's %s event occurred", event == AVAHI_BROWSER_CACHE_EXHAUSTED ? "CACHE_EXHAUSTED" : "ALL_FOR_NOW"); break; } } static void on_read_gga_sentence (GObject *object, GAsyncResult *result, gpointer user_data) { GClueNMEASource *source = GCLUE_NMEA_SOURCE (user_data); GDataInputStream *data_input_stream = G_DATA_INPUT_STREAM (object); GError *error = NULL; GClueLocation *location; gsize data_size = 0 ; char *message; message = g_data_input_stream_read_line_finish (data_input_stream, result, &data_size, &error); if (message == NULL) { if (error != NULL) { if (error->code == G_IO_ERROR_CLOSED) g_debug ("Socket closed."); else if (error->code != G_IO_ERROR_CANCELLED) g_warning ("Error when receiving message: %s", error->message); g_error_free (error); } else { g_debug ("Nothing to read"); } g_object_unref (data_input_stream); if (source->priv->active_service != NULL) /* In case service did not advertise it exiting * or we failed to receive it's notification. */ remove_service (source, source->priv->active_service); return; } g_debug ("Network source sent: \"%s\"", message); if (!g_str_has_prefix (message, "$GAGGA") && /* Galieo */ !g_str_has_prefix (message, "$GBGGA") && /* BeiDou */ !g_str_has_prefix (message, "$BDGGA") && /* BeiDou */ !g_str_has_prefix (message, "$GLGGA") && /* GLONASS */ !g_str_has_prefix (message, "$GNGGA") && /* GNSS (combined) */ !g_str_has_prefix (message, "$GPGGA") && /* GPS, SBAS, QZSS */ !g_str_has_prefix (message, "$QZGGA")) { /* QZSS */ g_debug ("Ignoring non-GGA sentence from NMEA source"); goto READ_NEXT_LINE; } location = gclue_location_create_from_gga (message, &error); if (error != NULL) { g_warning ("Error: %s", error->message); g_clear_error (&error); } else { gclue_location_source_set_location (GCLUE_LOCATION_SOURCE (source), location); } READ_NEXT_LINE: g_data_input_stream_read_line_async (data_input_stream, G_PRIORITY_DEFAULT, source->priv->cancellable, on_read_gga_sentence, source); } static void on_connection_to_location_server (GObject *object, GAsyncResult *result, gpointer user_data) { GClueNMEASource *source = GCLUE_NMEA_SOURCE (user_data); GSocketClient *client = G_SOCKET_CLIENT (object); GError *error = NULL; GDataInputStream *data_input_stream; GInputStream *input_stream; source->priv->connection = g_socket_client_connect_to_host_finish (client, result, &error); if (error != NULL) { if (error->code != G_IO_ERROR_CANCELLED) g_warning ("Failed to connect to NMEA service: %s", error->message); g_clear_error (&error); return; } input_stream = g_io_stream_get_input_stream (G_IO_STREAM (source->priv->connection)); data_input_stream = g_data_input_stream_new (input_stream); g_data_input_stream_read_line_async (data_input_stream, G_PRIORITY_DEFAULT, source->priv->cancellable, on_read_gga_sentence, source); } static void connect_to_service (GClueNMEASource *source) { GClueNMEASourcePrivate *priv = source->priv; if (priv->all_services == NULL) return; priv->client = g_socket_client_new (); g_cancellable_reset (priv->cancellable); /* The service with the highest accuracy will be stored in the beginning * of the list. */ priv->active_service = (AvahiServiceInfo *) priv->all_services->data; g_socket_client_connect_to_host_async (priv->client, priv->active_service->host_name, priv->active_service->port, priv->cancellable, on_connection_to_location_server, source); } static void disconnect_from_service (GClueNMEASource *source) { GClueNMEASourcePrivate *priv = source->priv; g_cancellable_cancel (priv->cancellable); if (priv->connection != NULL) { GError *error = NULL; g_io_stream_close (G_IO_STREAM (priv->connection), NULL, &error); if (error != NULL) g_warning ("Error in closing socket connection: %s", error->message); } g_clear_object (&priv->connection); g_clear_object (&priv->client); priv->active_service = NULL; } static void gclue_nmea_source_finalize (GObject *gnmea) { GClueNMEASourcePrivate *priv = GCLUE_NMEA_SOURCE (gnmea)->priv; G_OBJECT_CLASS (gclue_nmea_source_parent_class)->finalize (gnmea); g_clear_object (&priv->connection); g_clear_object (&priv->client); g_clear_object (&priv->cancellable); if (priv->avahi_client) avahi_client_free (priv->avahi_client); g_list_free_full (priv->all_services, avahi_service_free); } static void gclue_nmea_source_class_init (GClueNMEASourceClass *klass) { GClueLocationSourceClass *source_class = GCLUE_LOCATION_SOURCE_CLASS (klass); GObjectClass *gnmea_class = G_OBJECT_CLASS (klass); gnmea_class->finalize = gclue_nmea_source_finalize; source_class->start = gclue_nmea_source_start; source_class->stop = gclue_nmea_source_stop; } static void gclue_nmea_source_init (GClueNMEASource *source) { GClueNMEASourcePrivate *priv; AvahiServiceBrowser *service_browser; const AvahiPoll *poll_api; AvahiGLibPoll *glib_poll; int error; source->priv = G_TYPE_INSTANCE_GET_PRIVATE ((source), GCLUE_TYPE_NMEA_SOURCE, GClueNMEASourcePrivate); priv = source->priv; glib_poll = avahi_glib_poll_new (NULL, G_PRIORITY_DEFAULT); poll_api = avahi_glib_poll_get (glib_poll); priv->cancellable = g_cancellable_new (); avahi_client_new (poll_api, 0, client_callback, source, &error); if (priv->avahi_client == NULL) { g_warning ("Failed to connect to avahi service: %s", avahi_strerror (error)); return; } service_browser = avahi_service_browser_new (priv->avahi_client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, "_nmea-0183._tcp", NULL, 0, browse_callback, source); if (service_browser == NULL) { const char *errorstr; error = avahi_client_errno (priv->avahi_client); errorstr = avahi_strerror (error); g_warning ("Failed to browse avahi services: %s", errorstr); } } /** * gclue_nmea_source_get_singleton: * * Get the #GClueNMEASource singleton. * * Returns: (transfer full): a new ref to #GClueNMEASource. Use g_object_unref() * when done. **/ GClueNMEASource * gclue_nmea_source_get_singleton (void) { static GClueNMEASource *source = NULL; if (source == NULL) { source = g_object_new (GCLUE_TYPE_NMEA_SOURCE, NULL); g_object_add_weak_pointer (G_OBJECT (source), (gpointer) &source); } else g_object_ref (source); return source; } static gboolean gclue_nmea_source_start (GClueLocationSource *source) { GClueLocationSourceClass *base_class; g_return_val_if_fail (GCLUE_IS_NMEA_SOURCE (source), FALSE); base_class = GCLUE_LOCATION_SOURCE_CLASS (gclue_nmea_source_parent_class); if (!base_class->start (source)) return FALSE; connect_to_service (GCLUE_NMEA_SOURCE (source)); return TRUE; } static gboolean gclue_nmea_source_stop (GClueLocationSource *source) { GClueLocationSourceClass *base_class; g_return_val_if_fail (GCLUE_IS_NMEA_SOURCE (source), FALSE); base_class = GCLUE_LOCATION_SOURCE_CLASS (gclue_nmea_source_parent_class); if (!base_class->stop (source)) return FALSE; disconnect_from_service (GCLUE_NMEA_SOURCE (source)); return TRUE; }