2023-09-21 00:12:01 +02:00
|
|
|
import 'dart:async';
|
|
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
|
|
import 'dart:ffi';
|
|
|
|
import 'package:flutter_map/flutter_map.dart';
|
|
|
|
import 'package:latlong2/latlong.dart';
|
|
|
|
|
|
|
|
import 'package:ju_rc_app/lib/boxes.dart';
|
|
|
|
import 'package:ju_rc_app/lib/sensors.dart';
|
|
|
|
import 'package:ju_rc_app/lib/serial.dart';
|
|
|
|
|
|
|
|
// c-structs
|
|
|
|
base class GnssLocData extends Struct {
|
|
|
|
@Uint8()
|
|
|
|
external int type;
|
|
|
|
@Uint8()
|
|
|
|
external int command;
|
|
|
|
|
|
|
|
@Uint8()
|
|
|
|
external int isValid;
|
|
|
|
|
|
|
|
@Double()
|
|
|
|
external double lat;
|
|
|
|
@Double()
|
|
|
|
external double lng;
|
|
|
|
}
|
|
|
|
|
|
|
|
base class SpeedLocData extends Struct {
|
|
|
|
@Uint8()
|
|
|
|
external int type;
|
|
|
|
@Uint8()
|
|
|
|
external int command;
|
|
|
|
|
|
|
|
@Uint8()
|
|
|
|
external int isValid;
|
|
|
|
|
|
|
|
@Double()
|
|
|
|
external double speed;
|
|
|
|
}
|
|
|
|
|
|
|
|
base class CourseLocData extends Struct {
|
|
|
|
@Uint8()
|
|
|
|
external int type;
|
|
|
|
@Uint8()
|
|
|
|
external int command;
|
|
|
|
|
|
|
|
@Uint8()
|
|
|
|
external int isValid;
|
|
|
|
|
|
|
|
@Double()
|
|
|
|
external double course;
|
|
|
|
}
|
|
|
|
|
|
|
|
base class AltLocData extends Struct {
|
|
|
|
@Uint8()
|
|
|
|
external int type;
|
|
|
|
@Uint8()
|
|
|
|
external int command;
|
|
|
|
|
|
|
|
@Uint8()
|
|
|
|
external int isValid;
|
|
|
|
|
|
|
|
@Double()
|
|
|
|
external double alt;
|
|
|
|
}
|
|
|
|
|
|
|
|
base class SatLocData extends Struct {
|
|
|
|
@Uint8()
|
|
|
|
external int type;
|
|
|
|
@Uint8()
|
|
|
|
external int command;
|
|
|
|
|
|
|
|
@Uint8()
|
|
|
|
external int isValid;
|
|
|
|
|
|
|
|
@Uint32()
|
|
|
|
external int n;
|
|
|
|
}
|
|
|
|
|
|
|
|
base class HdopLocData extends Struct {
|
|
|
|
@Uint8()
|
|
|
|
external int type;
|
|
|
|
@Uint8()
|
|
|
|
external int command;
|
|
|
|
|
|
|
|
@Uint8()
|
|
|
|
external int isValid;
|
|
|
|
|
|
|
|
@Double()
|
|
|
|
external double hdop;
|
|
|
|
}
|
|
|
|
|
|
|
|
// c-structs end
|
|
|
|
|
|
|
|
class Maps extends JuBox {
|
|
|
|
const Maps({super.key});
|
|
|
|
|
|
|
|
@override
|
|
|
|
State<StatefulWidget> createState() => _MapsState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _MapsState extends State<Maps> with TickerProviderStateMixin {
|
|
|
|
USerial serial = getSerial();
|
|
|
|
late Timer timer;
|
|
|
|
|
|
|
|
double _lat = 51.1667, _long = 10.4500;
|
|
|
|
int _state = 0;
|
|
|
|
bool _wasVaild = false;
|
|
|
|
final _mapController = MapController();
|
2023-09-21 16:05:34 +02:00
|
|
|
bool available = true;
|
2023-09-21 00:12:01 +02:00
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
super.initState();
|
|
|
|
SensorReader.listen(serialListen);
|
|
|
|
timer = Timer.periodic(const Duration(seconds: 1), (_) async {
|
2023-09-21 16:05:34 +02:00
|
|
|
if (available) serial.sprintln("<rfSystemSensor>2300", system: true);
|
2023-09-21 00:12:01 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
SensorReader.removeListen(serialListen);
|
|
|
|
timer.cancel();
|
|
|
|
super.dispose();
|
|
|
|
}
|
|
|
|
|
|
|
|
void serialListen(int type, int command, Pointer<ArrayCStruct> ptr) {
|
2023-09-21 16:05:34 +02:00
|
|
|
if (type == 0x23 && command == 0xff) {
|
|
|
|
setState(() {
|
|
|
|
available = false;
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
2023-09-21 00:12:01 +02:00
|
|
|
if (type != 0x23 || command != 0x00) return;
|
|
|
|
var loc = ptr as Pointer<GnssLocData>;
|
|
|
|
setState(() {
|
|
|
|
_lat = loc.ref.lat;
|
|
|
|
_long = loc.ref.lng;
|
|
|
|
_state = loc.ref.isValid;
|
|
|
|
//_mapController.move(LatLng(_lat, _long), _wasVaild ? 17 : 6);
|
|
|
|
_animatedMapMove(
|
|
|
|
_mapController, this, LatLng(_lat, _long), _wasVaild ? 17 : 6);
|
|
|
|
if (_state & 1 > 0) _wasVaild = true;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2023-09-21 16:05:34 +02:00
|
|
|
Widget build(BuildContext context) => !available
|
|
|
|
? GestureDetector(
|
|
|
|
onTap: () {
|
|
|
|
if (!available) {
|
|
|
|
available = true;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
child: const Center(
|
|
|
|
child: Column(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
|
children: [
|
|
|
|
Text("GNSS is not available!"),
|
|
|
|
Icon(Icons.block)
|
|
|
|
])))
|
|
|
|
: FlutterMap(
|
|
|
|
options: MapOptions(
|
|
|
|
center: LatLng(_lat, _long),
|
|
|
|
zoom: _wasVaild ? 17 : 6,
|
|
|
|
maxZoom: _wasVaild ? 17 : 6,
|
|
|
|
minZoom: _wasVaild ? 17 : 6,
|
|
|
|
interactiveFlags: InteractiveFlag.none,
|
|
|
|
onTap: (TapPosition pos, LatLng ll) {
|
|
|
|
Navigator.push(
|
|
|
|
context,
|
|
|
|
MaterialPageRoute(
|
|
|
|
builder: (context) => const SerialDetailPage()),
|
|
|
|
);
|
|
|
|
}),
|
|
|
|
mapController: _mapController,
|
|
|
|
children: [
|
|
|
|
TileLayer(
|
|
|
|
urlTemplate: 'https://tile.openstreetmap.de/{z}/{x}/{y}.png',
|
|
|
|
userAgentPackageName: 'de.jusax.ju_rc_app',
|
|
|
|
),
|
|
|
|
MarkerLayer(
|
|
|
|
markers: [
|
|
|
|
Marker(
|
|
|
|
point: LatLng(_lat, _long),
|
|
|
|
width: 100,
|
|
|
|
height: 100,
|
|
|
|
builder: (context) => Icon(
|
|
|
|
switch (_state) {
|
|
|
|
3 => Icons.location_on,
|
|
|
|
2 => Icons.location_off,
|
|
|
|
1 => Icons.location_on_outlined,
|
|
|
|
int() => Icons.location_off_outlined,
|
|
|
|
},
|
|
|
|
color: Theme.of(context).colorScheme.primary,
|
|
|
|
),
|
2023-09-21 00:12:01 +02:00
|
|
|
),
|
2023-09-21 16:05:34 +02:00
|
|
|
],
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
2023-09-21 00:12:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
class SerialDetailPage extends StatefulWidget {
|
|
|
|
const SerialDetailPage({super.key});
|
|
|
|
|
|
|
|
@override
|
|
|
|
State<StatefulWidget> createState() => _SerialDetailPageState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _SerialDetailPageState extends State<SerialDetailPage>
|
|
|
|
with TickerProviderStateMixin {
|
|
|
|
USerial serial = getSerial();
|
|
|
|
late Timer timer;
|
|
|
|
final _mapController = MapController();
|
|
|
|
|
|
|
|
double _lat = 51.1667, _long = 10.4500;
|
|
|
|
int _state = 0;
|
|
|
|
|
|
|
|
double _mps = -1, _course = -1, _alt = -1, _hdop = -1;
|
|
|
|
bool _mpsState = false,
|
|
|
|
_courseState = false,
|
|
|
|
_altState = false,
|
|
|
|
_satNState = false,
|
|
|
|
_hdopState = false;
|
|
|
|
int _satN = -1;
|
|
|
|
bool _follow = true;
|
2023-09-21 16:05:34 +02:00
|
|
|
bool available = true;
|
2023-09-21 00:12:01 +02:00
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
super.initState();
|
|
|
|
timer = Timer.periodic(const Duration(seconds: 2), (_) async {
|
2023-09-21 16:05:34 +02:00
|
|
|
if(!available) return;
|
2023-09-21 00:12:01 +02:00
|
|
|
serial.sprintln("<rfSystemSensor>2303", system: true);
|
|
|
|
sleep(const Duration(milliseconds: 200));
|
|
|
|
serial.sprintln("<rfSystemSensor>2304", system: true);
|
|
|
|
sleep(const Duration(milliseconds: 200));
|
|
|
|
serial.sprintln("<rfSystemSensor>2305", system: true);
|
|
|
|
sleep(const Duration(milliseconds: 200));
|
|
|
|
serial.sprintln("<rfSystemSensor>2306", system: true);
|
|
|
|
sleep(const Duration(milliseconds: 200));
|
|
|
|
serial.sprintln("<rfSystemSensor>2307", system: true);
|
|
|
|
});
|
|
|
|
SensorReader.listen(serialListen);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
SensorReader.removeListen(serialListen);
|
|
|
|
timer.cancel();
|
|
|
|
super.dispose();
|
|
|
|
}
|
|
|
|
|
|
|
|
void serialListen(int type, int command, Pointer<ArrayCStruct> ptr) {
|
|
|
|
if (type != 0x23) return;
|
2023-09-21 16:05:34 +02:00
|
|
|
command = 0xff;
|
2023-09-21 00:12:01 +02:00
|
|
|
switch (command) {
|
|
|
|
case 0:
|
|
|
|
{
|
|
|
|
var loc = ptr as Pointer<GnssLocData>;
|
|
|
|
setState(() {
|
|
|
|
_lat = loc.ref.lat;
|
|
|
|
_long = loc.ref.lng;
|
|
|
|
_state = loc.ref.isValid;
|
|
|
|
if (_follow) {
|
|
|
|
_animatedMapMove(_mapController, this, LatLng(_lat, _long),
|
|
|
|
_mapController.zoom,
|
|
|
|
currentZoom: true);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 3:
|
|
|
|
{
|
|
|
|
var loc = ptr as Pointer<SpeedLocData>;
|
|
|
|
setState(() {
|
|
|
|
_mps = loc.ref.speed;
|
|
|
|
_mpsState = loc.ref.isValid == 3;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 4:
|
|
|
|
{
|
|
|
|
var loc = ptr as Pointer<CourseLocData>;
|
|
|
|
setState(() {
|
|
|
|
_course = loc.ref.course;
|
|
|
|
_courseState = loc.ref.isValid == 3;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 5:
|
|
|
|
{
|
|
|
|
var loc = ptr as Pointer<AltLocData>;
|
|
|
|
setState(() {
|
|
|
|
_alt = loc.ref.alt;
|
|
|
|
_altState = loc.ref.isValid == 3;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 6:
|
|
|
|
{
|
|
|
|
var loc = ptr as Pointer<SatLocData>;
|
|
|
|
setState(() {
|
|
|
|
_satN = loc.ref.n;
|
|
|
|
_satNState = loc.ref.isValid == 3;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 7:
|
|
|
|
{
|
|
|
|
var loc = ptr as Pointer<HdopLocData>;
|
|
|
|
setState(() {
|
|
|
|
_hdop = loc.ref.hdop;
|
|
|
|
_hdopState = loc.ref.isValid == 3;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 0xff:
|
2023-09-21 16:05:34 +02:00
|
|
|
{
|
|
|
|
setState(() {
|
|
|
|
available = false;
|
|
|
|
});
|
|
|
|
}
|
2023-09-21 00:12:01 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
int shade() {
|
|
|
|
return MediaQuery.of(context).platformBrightness == Brightness.light
|
|
|
|
? 100
|
|
|
|
: 800;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) => Scaffold(
|
|
|
|
appBar: AppBar(
|
|
|
|
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
|
|
|
title: const Text(
|
|
|
|
"Maps",
|
|
|
|
style: TextStyle(
|
|
|
|
fontSize: 16,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
toolbarHeight: 40,
|
|
|
|
),
|
|
|
|
floatingActionButton: FloatingActionButton(
|
|
|
|
onPressed: () {
|
|
|
|
setState(() {
|
|
|
|
if (!_follow) _mapController.move(LatLng(_lat, _long), 17);
|
|
|
|
_follow = !_follow;
|
|
|
|
});
|
|
|
|
},
|
|
|
|
child: Icon(_follow ? Icons.near_me : Icons.near_me_outlined),
|
|
|
|
),
|
2023-09-21 16:05:34 +02:00
|
|
|
body: !available
|
|
|
|
? const Center(
|
|
|
|
child: Column(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
|
children: [
|
|
|
|
Text("GNSS is not available!"),
|
|
|
|
Icon(Icons.block)
|
|
|
|
]))
|
|
|
|
: Column(children: [
|
|
|
|
Row(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
children: [
|
|
|
|
Text(
|
|
|
|
"Speed: ${_mps.toStringAsPrecision(3)}m/s",
|
|
|
|
style: TextStyle(
|
|
|
|
backgroundColor: _mpsState
|
|
|
|
? Colors.green[shade()]
|
|
|
|
: Colors.red[shade()]),
|
|
|
|
),
|
|
|
|
const VerticalDivider(width: 5),
|
|
|
|
Text("Course: ${_course.toStringAsFixed(0)}°",
|
|
|
|
style: TextStyle(
|
|
|
|
backgroundColor: _courseState
|
|
|
|
? Colors.green[shade()]
|
|
|
|
: Colors.red[shade()])),
|
|
|
|
const VerticalDivider(width: 5),
|
|
|
|
Text("Alt: ${_alt.toStringAsPrecision(4)}m",
|
|
|
|
style: TextStyle(
|
|
|
|
backgroundColor: _altState
|
|
|
|
? Colors.green[shade()]
|
|
|
|
: Colors.red[shade()])),
|
|
|
|
const VerticalDivider(width: 5),
|
|
|
|
Text("Sats: $_satN",
|
|
|
|
style: TextStyle(
|
|
|
|
backgroundColor: _satNState
|
|
|
|
? Colors.green[shade()]
|
|
|
|
: Colors.red[shade()])),
|
|
|
|
const VerticalDivider(width: 5),
|
|
|
|
Text("Hdop: ${_hdop.toStringAsPrecision(2)}",
|
|
|
|
style: TextStyle(
|
|
|
|
backgroundColor: _hdopState
|
|
|
|
? Colors.green[shade()]
|
|
|
|
: Colors.red[shade()])),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
Expanded(
|
|
|
|
child: FlutterMap(
|
|
|
|
mapController: _mapController,
|
|
|
|
options: MapOptions(
|
|
|
|
center: const LatLng(51.1667, 10.4500),
|
|
|
|
zoom: 6,
|
|
|
|
maxZoom: 18,
|
|
|
|
interactiveFlags: InteractiveFlag.drag |
|
|
|
|
InteractiveFlag.flingAnimation |
|
|
|
|
InteractiveFlag.pinchMove |
|
|
|
|
InteractiveFlag.pinchZoom |
|
|
|
|
InteractiveFlag.doubleTapZoom,
|
|
|
|
onPositionChanged: (MapPosition pos, bool user) {
|
|
|
|
if (user) {
|
|
|
|
setState(() {
|
|
|
|
_follow = false;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
children: [
|
|
|
|
TileLayer(
|
|
|
|
urlTemplate:
|
|
|
|
'https://tile.openstreetmap.de/{z}/{x}/{y}.png',
|
|
|
|
//urlTemplate: 'https://sgx.geodatenzentrum.de/wmts_basemapde/tile/1.0.0/de_basemapde_web_raster_farbe/default/GLOBAL_WEBMERCATOR/{z}/{y}/{x}.png',
|
|
|
|
userAgentPackageName: 'de.jusax.ju_rc_app',
|
|
|
|
),
|
|
|
|
MarkerLayer(
|
|
|
|
markers: [
|
|
|
|
Marker(
|
|
|
|
point: LatLng(_lat, _long),
|
|
|
|
width: 100,
|
|
|
|
height: 100,
|
|
|
|
builder: (context) => Transform.rotate(
|
|
|
|
angle: _course *
|
|
|
|
(pi / 180), // Convert degrees to radians
|
|
|
|
child: Icon(
|
|
|
|
switch (_state) {
|
|
|
|
3 => Icons.navigation,
|
|
|
|
2 => Icons.location_off,
|
|
|
|
1 => Icons.navigation_outlined,
|
|
|
|
int() => Icons.location_off_outlined,
|
|
|
|
},
|
|
|
|
color: Theme.of(context).colorScheme.primary,
|
|
|
|
),
|
|
|
|
)),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
],
|
|
|
|
))
|
|
|
|
]));
|
2023-09-21 00:12:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// animated Map from Map lib provider
|
|
|
|
|
|
|
|
void _animatedMapMove(MapController mapController, TickerProvider ticker,
|
|
|
|
LatLng destLocation, double destZoom,
|
|
|
|
{bool currentZoom = false}) {
|
|
|
|
const startedId = 'AnimatedMapController#MoveStarted';
|
|
|
|
const inProgressId = 'AnimatedMapController#MoveInProgress';
|
|
|
|
const finishedId = 'AnimatedMapController#MoveFinished';
|
|
|
|
// Create some tweens. These serve to split up the transition from one location to another.
|
|
|
|
// In our case, we want to split the transition be<tween> our current map center and the destination.
|
|
|
|
final camera = mapController;
|
|
|
|
final latTween =
|
|
|
|
Tween<double>(begin: camera.center.latitude, end: destLocation.latitude);
|
|
|
|
final lngTween = Tween<double>(
|
|
|
|
begin: camera.center.longitude, end: destLocation.longitude);
|
|
|
|
final zoomTween = Tween<double>(begin: camera.zoom, end: destZoom);
|
|
|
|
|
|
|
|
// Create a animation controller that has a duration and a TickerProvider.
|
|
|
|
final controller = AnimationController(
|
|
|
|
duration: const Duration(milliseconds: 500), vsync: ticker);
|
|
|
|
// The animation determines what path the animation will take. You can try different Curves values, although I found
|
|
|
|
// fastOutSlowIn to be my favorite.
|
|
|
|
final Animation<double> animation =
|
|
|
|
CurvedAnimation(parent: controller, curve: Curves.fastOutSlowIn);
|
|
|
|
|
|
|
|
// Note this method of encoding the target destination is a workaround.
|
|
|
|
// When proper animated movement is supported (see #1263) we should be able
|
|
|
|
// to detect an appropriate animated movement event which contains the
|
|
|
|
// target zoom/center.
|
|
|
|
final startIdWithTarget =
|
|
|
|
'$startedId#${destLocation.latitude},${destLocation.longitude},$destZoom';
|
|
|
|
bool hasTriggeredMove = false;
|
|
|
|
|
|
|
|
controller.addListener(() {
|
|
|
|
final String id;
|
|
|
|
if (animation.value == 1.0) {
|
|
|
|
id = finishedId;
|
|
|
|
} else if (!hasTriggeredMove) {
|
|
|
|
id = startIdWithTarget;
|
|
|
|
} else {
|
|
|
|
id = inProgressId;
|
|
|
|
}
|
|
|
|
hasTriggeredMove |= mapController.move(
|
|
|
|
LatLng(latTween.evaluate(animation), lngTween.evaluate(animation)),
|
|
|
|
currentZoom ? mapController.zoom : zoomTween.evaluate(animation),
|
|
|
|
id: id,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
animation.addStatusListener((status) {
|
|
|
|
if (status == AnimationStatus.completed) {
|
|
|
|
controller.dispose();
|
|
|
|
} else if (status == AnimationStatus.dismissed) {
|
|
|
|
controller.dispose();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
controller.forward();
|
|
|
|
}
|