Files
fengshui_compass/lib/pages/compass_page.dart

775 lines
29 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'dart:math';
import 'dart:typed_data';
import 'package:fengshui_compass/components/cross_paint.dart';
import 'package:fengshui_compass/components/my_icon.dart';
import 'package:fengshui_compass/pages/login_page.dart';
import 'package:fengshui_compass/states/region.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_serial_port_api/flutter_serial_port_api.dart';
import 'package:image_picker/image_picker.dart';
import 'package:provider/provider.dart';
import 'package:stream_transform/stream_transform.dart';
import '../components/region_selector.dart';
import '../states/compass_image.dart';
import '../utils/recv_parse.dart';
class CompassPage extends StatefulWidget {
const CompassPage({Key key}) : super(key: key);
@override
State<StatefulWidget> createState() => _CompassState();
}
class _CompassState extends State<CompassPage> with WidgetsBindingObserver {
// 串口相关
bool isPortOpened = false;
SerialPort _serialPort;
StreamSubscription _subscription;
// 传感器状态
bool isLock = true;
bool isUpClose = true;
bool isSideClose = true;
double w_x = 0.5;
double w_y = 0.5;
// 与磁北极夹角
double myaw = 0.0;
String distance = "";
var lista = [];
var listb = [];
var listc = [];
//从相册选择的图片名称
String selectedImageName;
void initDevice() {}
@override
void initState() {
super.initState();
print("init");
// 添加观察者以监听生命周期事件
WidgetsBinding.instance?.addObserver(this);
initPort();
}
@override
void dispose() {
turnOffPower();
// 在小部件被销毁时移除观察者,以防止内存泄漏
WidgetsBinding.instance?.removeObserver(this);
super.dispose();
print("dispose");
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// 在这里处理应用程序生命周期状态变化事件
if (state == AppLifecycleState.paused) {
// 应用程序进入后台
print('App entered background');
turnOffPower();
} else if (state == AppLifecycleState.resumed) {
// 应用程序回到前台
print('App returned to foreground');
initPort();
} else if (state == AppLifecycleState.inactive) {
// 应用程序处于非活动状态(例如:锁屏、电话来电)
print('App in inactive state');
turnOffPower();
}
}
Future<void> initPort() async {
await turnOnPower();
// todo:
setState(() {
isLock = true;
isUpClose = true;
isSideClose = true;
});
// 20ms接收一次串口数据防抖定义接收缓存
final debounceTransformer = StreamTransformer<Uint8List, dynamic>.fromBind(
(s) => s.transform(debounceBuffer(const Duration(milliseconds: 20))));
if (!isPortOpened) {
Device theDevice = Device("/dev/ttyS3", "/dev/ttyS3");
var serialPort = await FlutterSerialPortApi.createSerialPort(
theDevice, 115200,
parity: 0, dataBits: 8, stopBit: 1);
bool openResult = await serialPort.open();
setState(() {
_serialPort = serialPort;
isPortOpened = openResult;
});
await openRange();
_subscription = _serialPort.receiveStream
.transform(debounceTransformer)
.listen((recv) {
// recvData - 9E01040100000000000000065E
String recvData = formatReceivedData(recv);
print("Receive format: $recvData");
parsingRecvCom(recvData);
// 解析收到的结果,得到陀螺、地磁、测距结果
});
await openCompass();
}
}
Future<void> turnOnPower() async {
const platform = MethodChannel('toggle_power');
await platform.invokeMethod("turnOnPower");
}
Future<void> turnOffPower() async {
const platform = MethodChannel('toggle_power');
await platform.invokeMethod("turnOffPower");
if (mounted) {
setState(() {
isLock = false;
isUpClose = false;
isSideClose = false;
isPortOpened = false;
});
}
}
Future<void> closePort() async {
bool closeResult = await _serialPort.close();
setState(() {
isPortOpened = !closeResult;
});
}
Future<void> openCompass() async {
print("compass open");
_serialPort
.write(Uint8List.fromList(hexToUnits("7E01020100000000000000010D0A")));
}
Future<void> closeCompass() async {
print("compass close");
_serialPort
.write(Uint8List.fromList(hexToUnits("7E01020000000000000000010D0A")));
}
Future<void> openSideLaser() async {
print("side open");
_serialPort
.write(Uint8List.fromList(hexToUnits("7E01010100000000000000010D0A")));
}
Future<void> closeSideLaser() async {
print("side close");
_serialPort
.write(Uint8List.fromList(hexToUnits("7E01010000000000000000010D0A")));
}
Future<void> openUpLaser() async {
_serialPort
.write(Uint8List.fromList(hexToUnits("7e01010300000000000000010d0a")));
}
Future<void> closeUpLaser() async {
_serialPort
.write(Uint8List.fromList(hexToUnits("7e01010200000000000000010d0a")));
}
Future<void> openRange() async {
_serialPort
.write(Uint8List.fromList(hexToUnits("7e01040100000000000000010d0a")));
}
Future<void> raging() async {
if (!isLock) {
switchCompass();
}
_serialPort
.write(Uint8List.fromList(hexToUnits("7e01040200000000000000010d0a")));
}
Future<void> switchCompass() async {
if (isLock) {
await openCompass();
} else {
await closeCompass();
setState(() {
w_x = 0.5;
w_y = 0.5;
});
}
setState(() {
isLock = !isLock;
});
}
loginAction() {
Navigator.push(
context, MaterialPageRoute(builder: (context) => LoginPage()));
}
parsingRecvCom(str) {
if (str.contains("9E010201") &&
(str.length > (str.indexOf("9E010201") + 50))) {
var roll_l = hexToInt(str.substring(8, 10));
var roll_h = hexToInt(str.substring(10, 12));
var pitch_l = hexToInt(str.substring(12, 14));
var pitch_h = hexToInt(str.substring(14, 16));
// var yaw_l = hexToInt(str.substring(16, 18));
// var yaw_h = hexToInt(str.substring(18, 20));
var pos = str.indexOf("9E010301");
// var mx_h = hexToInt(str.substring(pos+8, pos+10));
// var mx_l = hexToInt(str.substring(pos+10, pos+12));
// var my_h = hexToInt(str.substring(pos+12, pos+14));
var myaw_flag = hexToInt(str.substring(pos + 14, pos + 16));
var myaw_h = hexToInt(str.substring(pos + 16, pos + 18));
var myaw_l = hexToInt(str.substring(pos + 18, pos + 20));
var roll_tmp = (roll_h * 256 + roll_l) * 180 / 32768;
var pitch_tmp = (pitch_h * 256 + pitch_l) * 180 / 32768;
// var yaw = (yaw_h * 256 + yaw_l) * 180 /32768;
// -180~180
var ff = myaw_flag == 1 ? -1 : 1;
var temp_myaw = (myaw_h * 256 + myaw_l) * 0.01 * ff + 180;
if (roll_tmp > 180) roll_tmp = roll_tmp - 360;
if (pitch_tmp > 180) pitch_tmp = pitch_tmp - 360;
var w_x_tmp = 0.0;
var w_y_tmp = 0.0;
// 倾角<30度
var w_total = sqrt(
roll_tmp.abs() * roll_tmp.abs() + pitch_tmp.abs() * pitch_tmp.abs());
print(w_total);
if (w_total <= 30) {
w_y_tmp = 0.5 - 0.07 * roll_tmp / 30.0;
w_x_tmp = 0.5 - 0.07 * pitch_tmp / 30.0;
} else if (w_total > 30) {
// //todo
// w_y_tmp = 0.5 - 0.07 * w_total / 30.0;
// w_x_tmp = 0.5 - 0.07 * w_total / 30.0;
var radius = sqrt(0.07 * 0.07 * 2);
//todo;
var _angle = atan2(roll_tmp, -pitch_tmp);
if (_angle < 0) {
_angle += 2 * pi;
}
w_y_tmp = 0.5 - radius * sin(_angle);
w_x_tmp = 0.5 + radius * cos(_angle);
}
// todo 其他情况
var meanValue;
// if (lista.length < 20) {
// lista.add(temp_myaw);
// meanValue = null;
// } else {
// lista.removeAt(0);
// lista.add(temp_myaw);
// meanValue = lista.map((e) => e).reduce((a, b) => a + b) / lista.length;
// }
if (mounted) {
setState(() {
// print("roll: $roll_tmp pitch: $pitch_tmp");
// print("w_x: $w_x_tmp, w_y: $w_y_tmp");
// if (meanValue != null) {
// myaw = meanValue;
// } else {
// myaw = 0;
// }
myaw = temp_myaw;
w_x = w_x_tmp;
w_y = w_y_tmp;
// 水平仪 0.5+-0.07范围
// x旋转y在动 y旋转x在动 roll改变y坐标pitch改变x坐标
});
}
} else if (str.contains("9E010401") &&
(str.length >= ((str.indexOf("9E010401") + 26)))) {
var pos = str.indexOf("9E010401");
int rh = hexToInt(str.substring(pos + 16, pos + 18));
int rl = hexToInt(str.substring(pos + 18, pos + 20));
int result = rh * 256 + rl;
print("测距$result");
setState(() {
distance = (result.toDouble() / 1000).toStringAsFixed(2);
});
} else {}
}
// 拼接城市
String spliceCityName(String pname, String cname) {
if (pname == '') return '未选择城市';
StringBuffer sb = StringBuffer();
sb.write(pname);
if (cname == '') return sb.toString();
sb.write(' - ');
sb.write(cname);
return sb.toString();
}
// bool strEmpty(String? value) {
// if (value == null) return true;
// return value.trim().isEmpty;
// }
selectRegion() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('选择城市校准磁偏角'),
content: RegionSelector(),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('关闭')),
TextButton(
onPressed: () {
Provider.of<RegionProvider>(context, listen: false)
.saveRegion();
Navigator.pop(context);
},
child: const Text('保存'))
],
);
}).then((value) {
Provider.of<RegionProvider>(context, listen: false).resetTemp();
});
}
double get radaw {
return (myaw + Provider.of<RegionProvider>(context).declination) *
2 *
pi /
360;
}
double getCorrectionAngle(double angle) {
double result;
if (angle < 0) {
result = angle + 360;
} else if (angle >= 0 && angle < 180) {
result = angle + 180;
} else if (angle > 180) {
result = angle - 180;
} else {
// result = 0.0;
result = angle;
}
return result;
}
double _scale = 1.0; // 放大倍数
Offset _origin = Offset(0.0, 0.0); // 放大原点
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/images/bg.png"), fit: BoxFit.cover)),
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
title: const Text(
'定盘星',
style: TextStyle(color: Colors.white),
),
// todo: leading can't click
// leading: IconButton(
// color: Colors.amber,
// icon: Icon(isLock ? MyIcons.icon_mima : MyIcons.icon_jiesuo),
// onPressed: switchCompass,
// ),
actions: [
//todo
// 更改背景图
IconButton(
color: Colors.amber,
icon: const Icon(
Icons.person,
size: 22,
),
onPressed: loginAction,
),
],
),
body: SafeArea(
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 600),
child: Consumer<RegionProvider>(
builder: (builder, regionProvider, child) {
return Consumer<CompassImageProvider>(
builder: (builder, compassImageProvider, child) {
return Stack(
alignment: Alignment.center,
children: [
// 罗盘
Column(
children: [
const Padding(padding: EdgeInsets.only(top: 145)),
Row(
children: [
Spacer(flex: 1),
Container(
width: 700,
height: 700,
child: Stack(
// fit: StackFit.loose,
children: [
ClipRect(
child: Transform.scale(
scale: _scale,
origin: _origin,
child: Transform.rotate(
angle: getCorrectionAngle((myaw +
regionProvider.declination)) *
2 *
pi /
360,
child: ClipOval(
child: Image(
width: 700,
height: 700,
image: compassImageProvider
.rotateImage ??
const AssetImage(
"assets/images/compass_rotated.png"),
fit: BoxFit.contain),
),
),
),
),
Align(
alignment: FractionalOffset(w_x, w_y),
child: const Image(
image:
AssetImage("assets/images/water.png"),
),
),
CrossPaint(),
GridView.count(
crossAxisCount: 3,
children: List.generate(
9,
(index) => Container(
// decoration: BoxDecoration(
// border: Border(
// bottom:
// const BorderSide(width: 1),
// right:
// const BorderSide(width: 1),
// left: index % 3 == 0
// ? const BorderSide(width: 1)
// : BorderSide.none,
// top: index <= 2
// ? const BorderSide(width: 1)
// : BorderSide.none),
// color: const Color.fromRGBO(
// 233, 233, 233, 0.3)),
height: 700 / 3,
width: 700 / 3,
child: GestureDetector(
// child: Text('grid $index'),
onDoubleTap: () {
setState(() {
_scale = 1.0;
});
},
onTap: () {
if (isLock) {
return;
}
setState(() {
switch (index) {
case 0:
_origin =
const Offset(-350, -350);
break;
case 1:
_origin =
const Offset(0, -350);
break;
case 2:
_origin =
const Offset(350, -350);
break;
case 3:
_origin =
const Offset(-350, 0);
break;
case 4:
_origin = const Offset(0, 0);
break;
case 5:
_origin =
const Offset(350, 0);
break;
case 6:
_origin =
const Offset(-350, 350);
break;
case 7:
_origin =
const Offset(0, 350);
break;
case 8:
_origin =
const Offset(350, 350);
break;
}
_scale = 3;
switchCompass();
// isLock = true;
});
},
),
),
),
),
// ClipRRect(
// borderRadius: BorderRadius.circular(225.0),
// child: Image(
// width: 700,
// height: 700,
// // alignment: Alignment.lerp(a, b, t),
// // image: compassImageProvider.rotateImage,
// image: compassImageProvider
// .rotateImage ??
// const AssetImage(
// "assets/images/compass_rotated.png"),
// fit: BoxFit.contain),
// )
ClipRect(
child: Image(
width: 700,
height: 700,
color: Colors.red,
// alignment: Alignment.lerp(a, b, t),
// image: compassImageProvider.rotateImage,
image: const AssetImage(
"assets/images/compass_rotated.png"),
fit: BoxFit.contain),
clipper: MyClipper(),
)
// GridClipPaint(0,0,1,2)
],
),
),
const Spacer(flex: 1),
],
)
],
),
// 最上面一行, lock azimuth login
Positioned(
top: 5,
child: Column(
children: [
const Image(
width: 15,
height: 15,
image: AssetImage("assets/images/arrow.png"),
fit: BoxFit.contain,
),
Text(
// "${azimuth.toStringAsFixed(2)}",
// ((myaw + regionProvider.declination) < 0
// ? 360 +
// (myaw + regionProvider.declination)
// : (myaw + regionProvider.declination))
// .toStringAsFixed(2),
getCorrectionAngle(
myaw + regionProvider.declination)
.toStringAsFixed(2),
style: const TextStyle(
color: Colors.amber, fontSize: 36),
),
],
)),
//磁偏角调整按钮
Positioned(
top: 5,
right: 6,
child: IconButton(
tooltip: '选择城市',
iconSize: 30,
onPressed: () => selectRegion(),
icon: const Icon(Icons.settings, color: Colors.amber),
)),
Positioned(
top: 5,
left: 6,
child: PopupMenuButton(
iconSize: 30,
icon: const Icon(Icons.photo, color: Colors.amber),
itemBuilder: (BuildContext context) => [
PopupMenuItem(
child: const ListTile(
leading: Icon(Icons.photo_album_rounded),
title: Text('选择新的罗盘图片'),
),
onTap: () {
ImagePicker()
.pickImage(source: ImageSource.gallery)
.then((res) {
if (res == null) {
return;
}
compassImageProvider
.setSelectedRotateImage(res);
});
},
),
PopupMenuItem(
enabled: compassImageProvider.rotateImage != null,
child: const ListTile(
leading: Icon(Icons.delete_rounded),
title: Text('恢复默认图片'),
),
onTap: () {
compassImageProvider.resetRotateImage();
},
),
],
)),
Positioned(
left: 5,
top: 54,
child: IconButton(
color: Colors.amber,
icon: Icon(
isLock ? MyIcons.icon_mima : MyIcons.icon_jiesuo),
onPressed: switchCompass,
),
),
// 最下面一行ranging value openlaser
Positioned(
bottom: 80,
left: 50,
child: IconButton(
onPressed: raging,
icon: const Icon(
MyIcons.icon_celiang,
size: 34,
),
color: Colors.amber)),
const Positioned(
width: 180,
height: 90,
bottom: 60,
child: Image(
// alignment: ,
image: AssetImage("assets/images/range_input.png"),
fit: BoxFit.contain,
)),
Positioned(
width: 180,
height: 90,
bottom: 60,
child: Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.only(right: 15),
child: Text(
"${distance} m",
style: const TextStyle(
color: Colors.amber, fontSize: 28),
),
),
)),
Positioned(
bottom: 60,
right: 40,
height: 120,
width: 100,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
color: Colors.amber,
onPressed: () {
if (isUpClose) {
openUpLaser();
} else {
closeUpLaser();
}
setState(() {
isUpClose = !isUpClose;
});
},
icon: Icon(
isUpClose
? MyIcons.icon_shangdeng
: MyIcons.icon_shangdnegguanbi,
size: 36)),
IconButton(
color: Colors.amber,
onPressed: () {
if (isSideClose) {
openSideLaser();
} else {
closeSideLaser();
}
setState(() {
isSideClose = !isSideClose;
});
},
icon: Icon(
isSideClose
? MyIcons.icon_zuoyoudneg
: MyIcons.icon_zuoyoudengguanbi,
size: 32,
))
],
),
)
],
);
});
},
),
)),
),
);
}
}
class MyClipper extends CustomClipper<Rect> {
@override
Rect getClip(Size size) => Rect.fromLTWH(0.0, 0.0, 100.0, 100.0);
@override
bool shouldReclip(CustomClipper<Rect> oldClipper) => false;
}