import 'dart:async'; import 'dart:io' as io; import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui; 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:fluttertoast/fluttertoast.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:stream_transform/stream_transform.dart'; import '../components/grid_clip_paint.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 createState() => _CompassState(); } class _CompassState extends State { // 串口相关 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(); initPort(); } Future initPort() async { // 20ms接收一次串口数据,防抖,定义接收缓存 final debounceTransformer = StreamTransformer.fromBind( (s) => s.transform(debounceBuffer(const Duration(milliseconds: 20)))); if (!isPortOpened) { Device theDevice = Device("/dev/ttyS2", "/dev/ttyS2"); 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 closePort() async { bool closeResult = await _serialPort.close(); setState(() { isPortOpened = !closeResult; }); } Future openCompass() async { print("compass open"); _serialPort .write(Uint8List.fromList(hexToUnits("7E01020100000000000000010D0A"))); } Future closeCompass() async { print("compass close"); _serialPort .write(Uint8List.fromList(hexToUnits("7E01020000000000000000010D0A"))); } Future openSideLaser() async { print("side open"); _serialPort .write(Uint8List.fromList(hexToUnits("7E01010100000000000000010D0A"))); } Future closeSideLaser() async { print("side close"); _serialPort .write(Uint8List.fromList(hexToUnits("7E01010000000000000000010D0A"))); } Future openUpLaser() async { _serialPort .write(Uint8List.fromList(hexToUnits("7e01010300000000000000010d0a"))); } Future closeUpLaser() async { _serialPort .write(Uint8List.fromList(hexToUnits("7e01010200000000000000010d0a"))); } Future openRange() async { _serialPort .write(Uint8List.fromList(hexToUnits("7e01040100000000000000010d0a"))); } Future raging() async { if (!isLock) { switchCompass(); } _serialPort .write(Uint8List.fromList(hexToUnits("7e01040200000000000000010d0a"))); } Future 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()); 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; } // 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; } 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: [ TextButton( onPressed: () { Navigator.pop(context); }, child: const Text('关闭')), TextButton( onPressed: () { Provider.of(context, listen: false) .saveRegion(); Navigator.pop(context); }, child: const Text('保存')) ], ); }).then((value) { Provider.of(context, listen: false).resetTemp(); }); } double get radaw { return (myaw + Provider.of(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; } 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), ), 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( builder: (builder, regionProvider, child) { return Consumer( 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(); }, ), ], )), // 最下面一行,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 { @override Rect getClip(Size size) => Rect.fromLTWH(0.0, 0.0, 100.0, 100.0); @override bool shouldReclip(CustomClipper oldClipper) => false; }