Commit 9fbdcddb authored by Nicolas Richard Walter Boeckh's avatar Nicolas Richard Walter Boeckh 💬
Browse files

Proximal points and ui

parent 802a2f34
import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/cupertino.dart';
import 'package:logair_application/ui/routes/home.dart';
/// This controller class is integrated to the [HomeView] to enable out of scope control of select Widgets therein.
class HomeController {
factory HomeController() => _singleton;
HomeController._internal();
static final HomeController _singleton = new HomeController._internal();
/// The controller for the [CarouselSlider].
PageController _pageController;
set carouselController(PageController carouselController) => this._pageController = carouselController;
get carouselController => this._pageController;
/// Force the [CarouselSlider] to navigate to another page.
void gotoPage(int page) => (this._pageController != null) ? this._pageController.jumpToPage(page) : {};
}
\ No newline at end of file
import 'dart:async';
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:flutter_map/plugin_api.dart';
import 'package:latlong/latlong.dart';
import 'package:logair_application/handlers/data_handler.dart';
import 'package:logair_application/logic/handlers/bluetooth_le_handler.dart';
import 'package:logair_application/logic/handlers/data_handler.dart';
import 'package:logair_application/logic/handlers/network_handler.dart';
import 'package:logair_application/logic/data_header.dart';
import 'package:logair_application/logic/data_packet.dart';
import 'package:logair_application/handlers/position_handler.dart';
import 'package:logair_application/logic/handlers/position_handler.dart';
/// The [MapDisplayController] is a [Singleton] that handles the mapping of information.
class MapDisplayController {
......@@ -17,22 +23,22 @@ class MapDisplayController {
LatLng _lastPos;
/// The [FlutterMap]'s zoom level, used to keep centering stable.
/// TODO Modify zoom action buttons.
double _zoom;
/// The [FlutterMap]s [MapController], kept unique in order to avoid incessant reloading of resources.
MapController _controller;
/// [List] of [Polyline] representing the evolution of the PM2.5 value.
/// TODO Discuss [Polyline] efficiency vs. 3px [Marker], especially considering navigation.
List<Polyline> _polylines = [];
/// [Marker] representing the user's current [Position].
Marker _marker;
HashMap<String, List<CircleMarker>> _pmMarkers = HashMap.from({'PM1': <CircleMarker>[], 'PM2_5': <CircleMarker>[], 'PM4': <CircleMarker>[], 'PM10': <CircleMarker>[]});
/// [List] of [Color]s to be smudged as a gradient and cherry picked based on a value (lerped).
/// @see [_colorizeValue]
List<Color> _colors = [ Color.fromARGB(0, 0, 0xcc, 0), Color.fromARGB(0, 0, 0xcc, 0), Color.fromARGB(0, 0xff, 0xff, 0), Color.fromARGB(0, 0xeb, 0x8a, 0x14), Color.fromARGB(0, 0xff, 0, 0), Color.fromARGB(0, 0xa1, 0x06, 0x49), Color.fromARGB(0, 0x7e, 0, 0x23) ];
List<Color> _colors = [ Color.fromARGB(0xff, 0, 0xcc, 0), Color.fromARGB(0xff, 0, 0xcc, 0), Color.fromARGB(0xff, 0xff, 0xff, 0), Color.fromARGB(0xff, 0xeb, 0x8a, 0x14), Color.fromARGB(0xff, 0xff, 0, 0), Color.fromARGB(0xff, 0xa1, 0x06, 0x49), Color.fromARGB(0xff, 0x7e, 0, 0x23) ];
/// [List] of stops for the lerping to occur.
/// @see [_colorizeValue]
......@@ -40,6 +46,24 @@ class MapDisplayController {
/// [Singleton]() instantiation factory.
factory MapDisplayController() => _singleton;
/// @Getter for [_polylines].
List<Polyline> polylines() => this._polylines;
/// @Getter for [_marker].
Marker marker() => this._marker != null ? this._marker : _buildLocationMarker(LatLng(0, 0));
/// Object used to calculate the real-life distance of two [Position]s
final Distance _distance = new Distance();
// TODO Faster to rebuild than to store ?
List<CircleMarker> pmMarkers() => this._pmMarkers[_pmKey[_pmKeyIndex]];
List<String> _pmKey = ["PM1", "PM2_5", "PM4", "PM10"];
int _pmKeyIndex = 1;
void setPMKeyIndex(int value) { print('PM Index $value gives ${_pmKey[value]}'); this._pmKeyIndex = value; }
/// This function is executed on first initialization of the [MapDisplayController]
/// TODO: Throttle to 1 in 5 updates for efficiency ?
......@@ -50,6 +74,9 @@ class MapDisplayController {
/// This function is hooked to the [PositionHandler]s progress, ie. when a new [Position] is acquired (once per second), it is broadcasted across the app and can be listened to.
PositionHandler().getCurrentOrLastPosition().listen((posChanged) {
if (posChanged == null)
return;
/// If a [BluetoothDevice] is connected, then the [DataHandler] will have a [DataPacket] available.
DataPacket latest = DataHandler().getLatestData();
......@@ -68,38 +95,40 @@ class MapDisplayController {
this._controller.move(this._currentPos, this._zoom);
/// Move the [Marker] to the current [Position].
this._marker = this._buildMarker(this._currentPos);
if (this._lastPos != null && this._currentPos != null && this._distance.as(LengthUnit.Meter, this._lastPos, this._currentPos) >= 2) {
// TODO Remove "|| true" to allow PM dependent gradient colors.
if (latest == null || true) {
if (this._polylines.length == 0)
this._polylines.add(
Polyline(
points: [this._lastPos, this._currentPos],
color: Colors.blue
)
);
else if (this._polylines.last.color == Colors.blue)
this._polylines.last.points.add(this._currentPos);
} else {
this._marker = this._buildLocationMarker(this._currentPos);
if ((this._lastPos != null && this._currentPos != null)) {// && this._distance.as(LengthUnit.Meter, this._lastPos, this._currentPos) >= 2)) {
if (this._polylines.length == 0) {
this._polylines.add(
Polyline(
points: [this._lastPos, this._currentPos],
color: _colorizeValue(latest.pm2_5())
color: Colors.blue
)
);
} else
this._polylines.last.points.add(this._currentPos);
if (latest != null) {
if (latest.pm1() != null) this._pmMarkers['PM1'].add(_buildPMMarker(this._currentPos, latest.pm1()));
if (latest.pm2_5() != null) this._pmMarkers['PM2_5'].add(_buildPMMarker(this._currentPos, latest.pm2_5()));
if (latest.pm4() != null) this._pmMarkers['PM4'].add(_buildPMMarker(this._currentPos, latest.pm4()));
if (latest.pm10() != null) this._pmMarkers['PM10'].add(_buildPMMarker(this._currentPos, latest.pm10()));
}
}
});
// TODO Set user defined duration
// TODO Set user defined radius
new Timer.periodic(Duration(minutes: 1), (Timer t) {
if (this._controller != null && this._controller.bounds != null)
NetworkHandler().getProximityPoints(DataHeader().deviceID, this._controller.bounds);
});
}
/// Internal static [Singleton]() reference.
static final MapDisplayController _singleton = new MapDisplayController._internal();
/// Object used to calculate the real-life distance of two [Position]s
final Distance _distance = new Distance();
/// @Getter for [_currentPos]
LatLng currentPosition() => _currentPos != null ? _currentPos : LatLng(0, 0);
......@@ -135,12 +164,6 @@ class MapDisplayController {
}
}
/// @Getter for [_polylines].
List<Polyline> polylines() => this._polylines;
/// @Getter for [_marker].
Marker marker() => this._marker != null ? this._marker : _buildMarker(LatLng(0, 0));
/// Gets the lerped [Color] value at a for a given PM value.
Color _colorizeValue(double pmValue) {
for (int stop = 0; stop < _stops.length - 1; stop++) {
......@@ -157,7 +180,7 @@ class MapDisplayController {
}
/// Wrapper function to build a [Marker], with default values.
Marker _buildMarker(LatLng position) => Marker(
Marker _buildLocationMarker(LatLng position) => Marker(
width: 20.0,
height: 20.0,
point: position,
......@@ -166,4 +189,14 @@ class MapDisplayController {
),
);
/// TODO MAKE PM Value be switchable.
CircleMarker _buildPMMarker(LatLng position, double pmValue) {
return CircleMarker(
radius: 8,
point: position,
color: _colorizeValue(pmValue).withAlpha(0xff),
borderStrokeWidth: 1,
borderColor: Colors.blue[300]
);
}
}
\ No newline at end of file
......@@ -13,7 +13,7 @@ class DataHeader {
static final DataHeader _singleton = new DataHeader._internal();
/// Data contained within the Header packet
List<String> _headerData;
bool _headerSet;
......@@ -23,8 +23,11 @@ class DataHeader {
this._headerSet = true;
}
/// Retrieve the deviceID from the header
String get deviceID => (this._headerData != null && this._headerData.length > 0) ? this._headerData[0] : '';
/// @Getter for _headerSet;
bool isHeaderSet() => _headerSet;
bool get headerSet => this._headerSet;
bool isEqual(List<int> that) => ListEquality().equals(_headerData, that);
......
import 'package:geolocator/geolocator.dart';
import 'package:logair_application/handlers/position_handler.dart';
import 'package:logair_application/logic/handlers/position_handler.dart';
import 'package:logair_application/utils/utils.dart';
/// Represents 1 second of data streamed from LogAir's devices,
/// and contains methods for:
......@@ -40,18 +41,6 @@ class DataPacket {
double pm2_5() => _pm2_5;
double pm4() => _pm4;
double pm10() => _pm10;
double parseDoubleWrapper(String data) {
double result = double.tryParse(data);
if (result == null) {
int result2 = int.tryParse(data);
if (result2 == null)
return null;
else
result = result2.toDouble();
}
return result;
}
DataPacket(this._data) {
_timestamp = DateTime.now().millisecondsSinceEpoch;
......
......@@ -3,59 +3,109 @@ import 'dart:io';
import 'package:flutter_blue/flutter_blue.dart';
import 'package:kotlin_flavor/scope_functions.dart';
import 'package:logair_application/enums/bluetooth_connection_status.dart';
import 'package:logair_application/handlers/data_handler.dart';
import 'package:logair_application/services/wake_service.dart';
import 'package:logair_application/logic/handlers/preference_handler.dart';
import 'package:logair_application/utils/enums/bluetooth_connection_status.dart';
import 'package:logair_application/logic/handlers/data_handler.dart';
/// Serves as a wrapper class around [FlutterBlue], and helps integrate it into the application with extensions
class BTLEHandler {
/// Static values used by the chip.
/// TODO Optionize (ongoing see [PreferencesHandler]) ?
static final String bluetoothLeCc254xServiceUUID = "0000ffe0-0000-1000-8000-00805f9b34fb";
static final String bluetoothLeCc254xReadUUID = "0000ffe1-0000-1000-8000-00805f9b34fb";
/// [Singleton]() instantiation factory.
factory BTLEHandler() => _singleton;
/// Internal static [Singleton]() constructor.
BTLEHandler._internal() {
fblueInstance.setLogLevel(LogLevel.info);
this._fblueInstance = FlutterBlue.instance;
//this._fblueInstance.setLogLevel(LogLevel.);
}
/// Internal static [Singleton]() reference.
static final BTLEHandler _singleton = new BTLEHandler._internal();
static bool wakeServiceActive = false;
/// The referenced [BluetoothCharacteristic].
/// Should be singular, unless we plan to broadcast over channels.
BluetoothCharacteristic _characteristic;
static Duration duration = new Duration(seconds: 1);
/// The static [FlutterBlue] instance that this handler should reference.
/// Enable avoiding extraneous GC, and referencing errors.
FlutterBlue _fblueInstance;
static final String bluetoothLeCc254xServiceUUID = "0000ffe0-0000-1000-8000-00805f9b34fb";
static final String bluetoothLeCc254xReadUUID = "0000ffe1-0000-1000-8000-00805f9b34fb";
/// @Getter for [_fblueInstance]
FlutterBlue get fblueInstance => this._fblueInstance;
Timer t;
/// [BluetoothConnectionStatus] that will serve as the baseline to display status to the user to.
BluetoothConnectionStatus _bluetoothConnectionStatus = BluetoothConnectionStatus.BTSTATUS_NOT_STREAMING;
FlutterBlue fblueInstance = FlutterBlue.instance;
/// @Getter to get this [BluetoothDevice]s MAC address.
String get address => this._device?.let((it) => it.id.toString());
BluetoothConnectionStatus _bluetoothConnectionStatus = BluetoothConnectionStatus.BTSTATUS_NOT_STREAMING;
/// The [BluetoothDevice] that this handler references.
BluetoothDevice _device;
/// Method that sets the [BluetoothDevice] to be listened to by [FlutterBlue].
void setDevice(BluetoothDevice device) => device?.let((it) => this._device = it);
/// [Stream] that tracks when the app is connected to the [BluetoothDevice]
/// [Stream] that notifes when the app is connected to the [BluetoothDevice].
Stream<bool> isDeviceConnected() async* {
while (true) {
await Future.delayed(Duration(milliseconds: 750));
yield (this._device != null);
await Future.delayed(Duration(milliseconds: 750));
}
}
/// [Stream] that notifes the [BluetoothConnectionStatus] every 2 seconds.
Stream<BluetoothConnectionStatus> getConnectionStatus() async* {
while (true) {
yield _bluetoothConnectionStatus;
await Future.delayed(Duration(seconds: 2));
}
}
Future<bool> setupCharacteristics() async {
/// Makes the phone connect to the [BluetoothDevice] and initiate [BluetoothService] discovery.
Future<void> connect() async {
/// If the [BluetoothDevice] is connected to this handler.
if (this._device != null)
await this._asyncConnectionProcess();
}
/// Makes the smartphone disconnect from the [BluetoothDevice].
Future<dynamic> disconnect() => this._device?.let((it) async {
/// Remove notifications and references to the [BluetoothDevice] or [BluetoothCharacteristic]s.
if (_characteristic != null)
await _characteristic.setNotifyValue(false);
_characteristic = null;
_device = null;
_bluetoothConnectionStatus = BluetoothConnectionStatus.BTSTATUS_NOT_STREAMING;
return it.disconnect();
});
/// Wrapper function that tries to make the handler connect to the desired [BluetoothCharacteristic], if it exists.
Future<bool> _setupCharacteristics() async {
List<BluetoothService> services = [];
List<BluetoothCharacteristic> characteristics = [];
/// Termination status variable.
bool success = false;
if (this._device != null) {
/// Get the first round of [BluetoothService]s and apply filter to desired Service UUID.
services = await this._device.services.first;
services = services.where((s) => s.uuid.toString() == bluetoothLeCc254xServiceUUID).toList();
/// If the [BluetoothService] exists.
if (services.length > 0) {
/// Get the associated [BluetoothCharacteristic]s and apply filter to desired Characteristic UUID.
characteristics = services[0].characteristics;
characteristics = characteristics.where((c) => c.uuid.toString() == bluetoothLeCc254xReadUUID).toList();
/// If the [BluetoothCharacteristic] exists.
if (characteristics.length > 0) {
/// Set the [BluetoothCharacteristic] to notify new data.
if (!characteristics[0].isNotifying)
await characteristics[0].setNotifyValue(true);
_characteristic = characteristics[0];
......@@ -63,94 +113,48 @@ class BTLEHandler {
}
}
}
return success;
}
Future<bool> setupListener() async {
/// Wrapper function that enable data handoff to [DataHandler];
Future<bool> _setupListener() async {
bool success = false;
/// Guard against unwanted trigger.
if (_characteristic != null) {
_bluetoothConnectionStatus = BluetoothConnectionStatus.BTSTATUS_STREAMING;
/// Apply a pipe transform to the values that are notified by the [BluetoothCharacteristic].
_characteristic.value.transform(
new StreamTransformer.fromHandlers(
handleData: (List<int> list, EventSink<List<int>> sink) {
if (list.isNotEmpty)
sink.add(list);
else
sink.add([]);
}
)
).listen((data) => DataHandler().addData(data));
handleData: (List<int> list, EventSink<List<int>> sink) =>
sink.add((list.isNotEmpty) ? list : []))
).listen((data) =>
/// Handoff the data to the [DataHandler].
DataHandler().addData(data)
);
print("RETURNS");
return true;
success = true;
} else {
_bluetoothConnectionStatus = BluetoothConnectionStatus.BTSTATUS_STREAMING;
return false;
}
}
Stream<BluetoothConnectionStatus> getConnectionStatus() async* {
while (true) {
yield _bluetoothConnectionStatus;
await Future.delayed(Duration(seconds: 2));
_bluetoothConnectionStatus = BluetoothConnectionStatus.BTSTATUS_NOT_STREAMING;
}
return success;
}
/// Makes the phone connect to the [BluetoothDevice] and initiate [BluetoothService] discovery.
Future<void> connect() => this._device?.let(
(it) {
asyncConnection();
return;
}
);
asyncConnection() async {
/// Wrapper function that aggregates all of the steps to connect to a [BluetoothDevice] and stream its data.
Future<void> _asyncConnectionProcess() async {
await this._device.connect();
if (Platform.isAndroid) {
/// Android can negotiate greater Message Transmission Units.
/// 517 is the maximum defined in the BLE specification, the request will assign the largest available MTU instead.
_bluetoothConnectionStatus = BluetoothConnectionStatus.BTSTATUS_SETTING_MTU;
await this._device.requestMtu(517);
}
_bluetoothConnectionStatus = BluetoothConnectionStatus.BTSTATUS_DISCOVERING_SERVICES;
/// @bug{Paul Demarco has been notified} Delay to let MTU request sink in, otherwise hindering [BluetoothService] discovery.
await Future.delayed(Duration(seconds: 1));
await this._device.discoverServices();
this._device.services.map((event) => print);
_bluetoothConnectionStatus = BluetoothConnectionStatus.BTSTATUS_GETTING_CHARACTERISTICS;
await setupCharacteristics();
await setupListener();
await _setupCharacteristics();
await _setupListener();
}
Future<dynamic> disconnect() => this._device?.let((it) async {
if (_characteristic != null)
await _characteristic.setNotifyValue(false);
_characteristic = null;
_device = null;
_bluetoothConnectionStatus = BluetoothConnectionStatus.BTSTATUS_NOT_STREAMING;
return it.disconnect();
});
String get address => this._device?.let((it) => it.id.toString());
BluetoothDevice _device;
setDevice(BluetoothDevice device) => device?.let((it) => this._device = it);
bool switchState() {
if (wakeServiceActive)
_end();
else
_start();
return wakeServiceActive;
}
void _start() {
WakeService().modifyWakeService(true).then((value) => print('_device will stay awake $value'));
wakeServiceActive = true;
t = new Timer.periodic(duration, (Timer t) => print("${new DateTime.now()}"));
}
void _end() {
WakeService().modifyWakeService(false).then((value) => print('_device will stay awake $value'));
wakeServiceActive = false;
t.cancel();
}
}
\ No newline at end of file
import 'dart:async';
import 'package:logair_application/services/wake_service.dart';
/// Handler that wraps the [WakeService]
/// TODO flutter_blue seems to do so already, needs testing and iOS solution
class BTWakeHandler {
factory BTWakeHandler() => _singleton;
BTWakeHandler._internal();
static final BTWakeHandler _singleton = BTWakeHandler._internal();
/// TODO Used for debugging, remove
Timer _t;
/// TODO Used for debugging, remove
static Duration _duration = new Duration(seconds: 1);
static bool _wakeServiceActive = false;
bool switchState() {
if (_wakeServiceActive)
_end();
else
_start();
return _wakeServiceActive;
}
void _start() {
WakeService().modifyWakeService(true).then((value) => print('_device will stay awake $value'));
_wakeServiceActive = true;
_t = new Timer.periodic(_duration, (Timer t) => print("${new DateTime.now()}"));
}
void _end() {
WakeService().modifyWakeService(false).then((value) => print('_device will stay awake $value'));
_wakeServiceActive = false;
_t.cancel();
}
}
\ No newline at end of file
import 'package:logair_application/handlers/bluetooth_le_handler.dart';
import 'package:logair_application/logic/handlers/bluetooth_le_handler.dart';
import 'package:logair_application/logic/data_header.dart';
import 'package:logair_application/logic/data_packet.dart';
......@@ -21,50 +21,64 @@ class DataHandler {
void addData(List<int> data) {
_data.addAll(data);
/// Get a start index for the data, either '{' (DataHeader) or '[' (DataPacket)
int index = _data.indexWhere((e) => e == 91 || e == 123);
if (index == -1)
return;
/// Remove noise if exists
_data.removeRange(0, index);
/// Get the termination index '$'
int termIndex = _data.indexWhere((e) => e == 36);
if (termIndex == -1)
return;
/// If both exist, then extract the packet.
List<int> packet = _data.sublist(index, termIndex + 1);
_data.removeRange(0, termIndex);
/// If a packet is ingrained within this packet (possibly because of BLE transmission issues),
/// this will attempt to fix it.
if (packet.sublist(1).contains((e) => e == 91 || e == 123)) {
print('True ${packet.sublist(1).indexWhere((e) => e == 91 || e == 123)}');
int startIndex = packet.sublist(1).indexWhere((e) => e == 91 || e == 123);
packet.removeRange(0, startIndex);
}
/// If it's a [DataHeader] instance.
if (packet[0] == 123) {
print('HEADER ${String.fromCharCodes(packet)}');
DataHeader().setHeader(packet);
/// Or if it is a [DataPacket] instance.
} else if (packet[0] == 91) {
_sortedData.add(DataPacket(packet));
print('PACKET ${String.fromCharCodes(packet)}');
}
}
/// TODO Used for debugging, later
void printLatest() {
print(_sortedData.last);
}
/// Get the entirety of the [DataPacket]s.
/// TODO Caching
List<DataPacket> getData() => _sortedData;
/// Get a subrange of [DataPacket]s.
List<DataPacket> getDataRange(int range) => (range > _sortedData.length) ? _sortedData : _sortedData.sublist(0, range);
/// Remove the entirety of the acquired data.
void clearData() {
_sortedData.removeWhere((element) => true);
}