Skip to content

Commit

Permalink
Add NMEA message event stream and channel
Browse files Browse the repository at this point in the history
  • Loading branch information
Wackymax committed Apr 11, 2022
1 parent c80a586 commit 8cb59d7
Show file tree
Hide file tree
Showing 9 changed files with 301 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public class GeolocatorPlugin implements FlutterPlugin, ActivityAware {
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d(TAG, "Geolocator foreground service connected");
if (service instanceof GeolocatorLocationService.LocalBinder) {
initialize(((GeolocatorLocationService.LocalBinder) service).getLocationService());
initialize(((GeolocatorLocationService.LocalBinder) service).getLocationService());
}
}

Expand All @@ -51,6 +51,7 @@ public void onServiceDisconnected(ComponentName name) {
}
}
};
@Nullable private NmeaStreamHandlerImpl nmeaStreamHandlerImpl;
@Nullable private LocationServiceHandlerImpl locationServiceHandler;

@SuppressWarnings("deprecation")
Expand Down Expand Up @@ -108,6 +109,10 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBindin
streamHandler.startListening(
flutterPluginBinding.getApplicationContext(), flutterPluginBinding.getBinaryMessenger());

nmeaStreamHandlerImpl = new NmeaStreamHandlerImpl();
nmeaStreamHandlerImpl.startListening(
flutterPluginBinding.getApplicationContext(), flutterPluginBinding.getBinaryMessenger());

locationServiceHandler = new LocationServiceHandlerImpl();
locationServiceHandler.setContext(flutterPluginBinding.getApplicationContext());
locationServiceHandler.startListening(
Expand Down Expand Up @@ -216,6 +221,10 @@ private void dispose() {
streamHandler.setForegroundLocationService(null);
streamHandler = null;
}

if (nmeaStreamHandlerImpl != null) {
nmeaStreamHandlerImpl.stopListening();
}
if (locationServiceHandler != null) {
locationServiceHandler.setContext(null);
locationServiceHandler.stopListening();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.baseflow.geolocator;

import android.content.Context;
import android.util.Log;

import androidx.annotation.Nullable;

import com.baseflow.geolocator.location.NmeaClient;
import com.baseflow.geolocator.location.NmeaMapper;

import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.EventChannel;

class NmeaStreamHandlerImpl implements EventChannel.StreamHandler {
private static final String TAG = "FlutterGeolocator";

@Nullable private EventChannel channel;
@Nullable private Context context;
@Nullable private NmeaClient nmeaClient;

public NmeaStreamHandlerImpl() {}

/**
* Registers this instance as event stream handler on the given {@code messenger}.
*
* <p>Stops any previously started and unstopped calls.
*
* <p>This should be cleaned with {@link #stopListening} once the messenger is disposed of.
*/
void startListening(Context context, BinaryMessenger messenger) {
if (channel != null) {
Log.w(TAG, "Setting a event call handler before the last was disposed.");
stopListening();
}

channel = new EventChannel(messenger, "flutter.baseflow.com/geolocator_nmea_updates_android");
channel.setStreamHandler(this);
this.context = context;
this.nmeaClient = new NmeaClient(this.context);
}

/**
* Clears this instance from listening to method calls.
*
* <p>Does nothing if {@link #startListening} hasn't been called, or if we're already stopped.
*/
void stopListening() {
if (channel == null) {
Log.d(TAG, "Tried to stop listening when no MethodChannel had been initialized.");
return;
}

disposeListeners();
channel.setStreamHandler(null);
channel = null;
}

@Override
public void onListen(Object arguments, EventChannel.EventSink events) {

if (nmeaClient == null) {
Log.e(TAG, "NMEA Client has not started correctly");
return;
}

nmeaClient.start();
nmeaClient.setCallback((message -> events.success(NmeaMapper.toHashMap(message))));
}

@Override
public void onCancel(Object arguments) {
disposeListeners();
}

private void disposeListeners() {
Log.e(TAG, "Geolocator position updates stopped");
if (nmeaClient != null) {
nmeaClient.setCallback(null);
nmeaClient.stop();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.baseflow.geolocator.location;

public interface NMEACallback {
void onMessage(String message);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ public class NmeaClient {
public static final String NMEA_MESSAGE_EXTRA = "geolocator_nmeaMessage";
public static final String NMEA_ALTITUDE_EXTRA = "geolocator_mslAltitude";

private final Context context;
private final LocationManager locationManager;
@NonNull private final Context context;
@Nullable private final LocationManager locationManager;

@TargetApi(Build.VERSION_CODES.N)
private OnNmeaMessageListener nmeaMessageListener;
@NonNull private OnNmeaMessageListener nmeaMessageListener;

private String lastNmeaMessage;
@Nullable private NMEACallback callback;
@Nullable private String lastNmeaMessage;
private boolean listenerAdded = false;

public NmeaClient(@NonNull Context context) {
Expand All @@ -35,6 +36,9 @@ public NmeaClient(@NonNull Context context) {
if (message.startsWith("$")) {
lastNmeaMessage = message;
}
if(callback != null) {
callback.onMessage(message);
}
};
}
}
Expand All @@ -50,6 +54,13 @@ public void start() {
}
}

public void setCallback(NMEACallback callback) {
if(this.callback != null) {
throw new IllegalArgumentException("A callback has already been registered");
}
this.callback = callback;
}

public void stop() {
if (!listenerAdded) {
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.baseflow.geolocator.location;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

public class NmeaMapper {

public static Map<String, Object> toHashMap(String message) {
if (message == null) {
return null;
}

Map<String, Object> nmeaMessage = new HashMap<>();

if (message.startsWith("$")) {

nmeaMessage.put("nmeaMessage", message);
nmeaMessage.putAll(tryParseGPGAAMessage(message));
}


return nmeaMessage;
}

private static Map<String, Object> tryParseGPGAAMessage(String message) {

Map<String, Object> parsedMessage = new HashMap<>();

String[] tokens = message.split(",");
String type = tokens[0];
// Parse altitude above sea level, Detailed description of NMEA string here
// http://aprs.gids.nl/nmea/#gga
if (type.startsWith("$GPGGA") && tokens.length > 9) {

parsedMessage.put("time", tokens[1]);
parsedMessage.put("latitude", tokens[2] + ',' + tokens[3]);
parsedMessage.put("longitude", tokens[4] + ',' + tokens[5]);
parsedMessage.put("quality", Integer.parseInt(tokens[6]));
parsedMessage.put("numberOfSatellites", Integer.parseInt(tokens[7]));
parsedMessage.put("horizontalDilutionOfPrecision", Double.parseDouble(tokens[8]));
parsedMessage.put("altitude", Double.parseDouble(tokens[9]));
parsedMessage.put("heightAboveEllipsoid", Double.parseDouble(tokens[11]));
}

return parsedMessage;
}
}
1 change: 1 addition & 0 deletions geolocator_android/lib/geolocator_android.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ export 'package:geolocator_platform_interface/geolocator_platform_interface.dart
export 'src/geolocator_android.dart';
export 'src/types/android_settings.dart' show AndroidSettings;
export 'src/types/foreground_settings.dart' show ForegroundNotificationConfig;
export 'src/types/NMEAMessage.dart' show NMEAMessage;
27 changes: 27 additions & 0 deletions geolocator_android/lib/src/geolocator_android.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:geolocator_android/src/types/NMEAMessage.dart';
import 'package:geolocator_platform_interface/geolocator_platform_interface.dart';

/// An implementation of [GeolocatorPlatform] that uses method channels.
Expand All @@ -19,6 +20,11 @@ class GeolocatorAndroid extends GeolocatorPlatform {
static const _serviceStatusEventChannel =
EventChannel('flutter.baseflow.com/geolocator_service_updates_android');

/// The event channel used to receive [LocationServiceStatus] updates from the
/// native platform.
static const _nmeaEventChannel =
EventChannel('flutter.baseflow.com/geolocator_nmea_updates_android');

/// Registers this class as the default instance of [GeolocatorPlatform].
static void registerWith() {
GeolocatorPlatform.instance = GeolocatorAndroid();
Expand All @@ -32,6 +38,7 @@ class GeolocatorAndroid extends GeolocatorPlatform {

Stream<Position>? _positionStream;
Stream<ServiceStatus>? _serviceStatusStream;
Stream<NMEAMessage>? _nmeaMessagesStream;

@override
Future<LocationPermission> checkPermission() async {
Expand Down Expand Up @@ -148,6 +155,26 @@ class GeolocatorAndroid extends GeolocatorPlatform {
return _serviceStatusStream!;
}

Stream<NMEAMessage> getNmeaMessagesStream() {
if (_nmeaMessagesStream != null) {
return _nmeaMessagesStream!;
}
var nmeaMessagesStream =
_nmeaEventChannel.receiveBroadcastStream();

_nmeaMessagesStream = nmeaMessagesStream
.map((dynamic element) => NMEAMessage.fromMap(element.cast<String, dynamic>()))
.handleError((error) {
_nmeaMessagesStream = null;
if (error is PlatformException) {
error = _handlePlatformException(error);
}
throw error;
});

return _nmeaMessagesStream!;
}

@override
Stream<Position> getPositionStream({
LocationSettings? locationSettings,
Expand Down
111 changes: 111 additions & 0 deletions geolocator_android/lib/src/types/NMEAMessage.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/// Represents an NMEA message received from the platform.
/// If the NMEA message represents a GPGGA sequence then the message
/// will be parsed and the GPGGA fields will be populate as well.
class NMEAMessage {

/// Initializes a new [NMEAMessage] instance with default values.
const NMEAMessage({
required this.nmeaMessage,
this.time,
this.latitude,
this.longitude,
this.quality,
this.numberOfSatellites,
this.altitude,
this.heightAboveEllipsoid,
});

/// The raw NMEA Message
final String nmeaMessage;

/// UTC time if the NMEA Message was GPGGA Sequence
/// GPGGA formatting can be found here: http://aprs.gids.nl/nmea/#gga
final String? time;

/// Latitude if the NMEA Message was GPGGA Sequence
/// GPGGA formatting can be found here: http://aprs.gids.nl/nmea/#gga
final String? latitude;

/// Longitude if the NMEA Message was GPGGA Sequence
/// GPGGA formatting can be found here: http://aprs.gids.nl/nmea/#gga
final String? longitude;

/// Quality if the NMEA Message was GPGGA Sequence
/// Valid values can be found here: http://aprs.gids.nl/nmea/#gga
final int? quality;

/// Number of Satellites if the NMEA Message was GPGGA Sequence
final int? numberOfSatellites;

/// Altitude above mean sea level in meters if the NMEA Message was GPGGA Sequence
final double? altitude;

/// Height of geoid above QGS84 ellipsoid
/// if the NMEA Message was GPGGA Sequence
final double? heightAboveEllipsoid;

@override
bool operator ==(Object other) {
var areEqual = other is NMEAMessage &&
other.nmeaMessage == nmeaMessage &&
other.time == time &&
other.latitude == latitude &&
other.longitude == longitude &&
other.quality == quality &&
other.numberOfSatellites == numberOfSatellites &&
other.altitude == altitude &&
other.heightAboveEllipsoid == heightAboveEllipsoid;

return areEqual;
}

@override
int get hashCode =>
nmeaMessage.hashCode ^
time.hashCode ^
latitude.hashCode ^
longitude.hashCode ^
quality.hashCode ^
numberOfSatellites.hashCode ^
altitude.hashCode ^
heightAboveEllipsoid.hashCode;

@override
String toString() {
return 'NMEA Message: $nmeaMessage';
}

/// Converts the supplied [Map] to an instance of the [NMEAMessage] class.
static NMEAMessage fromMap(dynamic message) {
final Map<dynamic, dynamic> nmeaMap = message;

if (!nmeaMap.containsKey('nmeaMessage')) {
throw ArgumentError.value(nmeaMap, 'nmeaMap',
'The supplied map doesn\'t contain the mandatory key `nmeaMessage`.');
}

return NMEAMessage(
nmeaMessage: nmeaMap['nmeaMessage'],
time: nmeaMap['time'],
latitude: nmeaMap['latitude'],
longitude: nmeaMap['longitude'],
quality: nmeaMap['quality'],
numberOfSatellites: nmeaMap['numberOfSatellites'],
altitude: nmeaMap['altitude'],
heightAboveEllipsoid: nmeaMap['heightAboveEllipsoid'],
);
}

/// Converts the [NMEAMessage] instance into a [Map] instance that can be
/// serialized to JSON.
Map<String, dynamic> toJson() => {
'nmeaMessage': nmeaMessage,
'time': time,
'latitude': latitude,
'longitude': longitude,
'quality': quality,
'numberOfSatellites': numberOfSatellites,
'altitude': altitude,
'heightAboveEllipsoid': heightAboveEllipsoid,
};
}
Loading

0 comments on commit 8cb59d7

Please sign in to comment.