1_BLE
1.1_BLE手柄
安装库
flutter pub add flutter_joystick
flutter pub add flutter_reactive_ble
添加安卓BLE权限
在android/app/src/main/AndroidManifest.xml中添加:
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30" />
<application
修改最小Android SDK版本
在android/app/build.gradle中进行更改:
Android {
defaultConfig {
minSdkVersion: 21
flutter代码
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:convert' as convert;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_joystick/flutter_joystick.dart';
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations(
[DeviceOrientation.landscapeRight, DeviceOrientation.landscapeLeft])
.then((_) {
runApp(const MyApp());
});
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(),
home: const JoyPad(),
);
}
}
class JoyPad extends StatefulWidget {
const JoyPad({Key? key}) : super(key: key);
@override
JoyPadState createState() => JoyPadState();
}
class JoyPadState extends State<JoyPad> {
var serviceId = Uuid.parse(
'4fafc201-1fb5-459e-8fcc-c5c9c331914b'); // REPLACE WITH YOUR SERVICE_ID
var characteristicId = Uuid.parse(
'beb5483e-36e1-4688-b7f5-ea07361b26a8'); // REPLACE WITH YOUR CHARACTERISTIC_ID
StreamSubscription? subscription;
StreamSubscription<ConnectionStateUpdate>? connection;
QualifiedCharacteristic? characteristic;
final bleManager = FlutterReactiveBle();
String connectionText = "";
late List<String> buttonCharacter = ['A', 'B', 'X', 'Y'];
late Map<String, String> user = {
"u": "0",
"d": "0",
"l": "0",
"r": "0",
"s": "0",
"t": "0",
"a": "0",
"b": "0",
"x": "0",
"y": "0",
};
@override
void initState() {
super.initState();
bleManager.statusStream.listen((status) {
log("STATUS: $status");
if (status == BleStatus.ready) initBle();
});
}
@override
void dispose() {
subscription?.cancel();
connection?.cancel();
super.dispose();
}
//停止扫描
Future<void> stopScan() async {
log('HF: stopping BLE scan');
await subscription?.cancel();
subscription = null;
}
//初始化状态
void initBle() {
subscription?.cancel();
//扫描设备
subscription = bleManager.scanForDevices(
withServices: [serviceId],
scanMode: ScanMode.lowLatency).listen((device) {
log("SCAN FOUND: ${device.name}");
stopScan();
//建立连接
connection = bleManager
.connectToDevice(
id: device.id,
servicesWithCharacteristicsToDiscover: {
serviceId: [characteristicId]
},
connectionTimeout: const Duration(seconds: 2),
)
.listen((connectionState) {
log("CONNECTING: $connectionState");
if (connectionState.connectionState ==
DeviceConnectionState.connected) {
setState(() {
bleManager.requestConnectionPriority(
deviceId: serviceId.toString(),
priority: ConnectionPriority.highPerformance); //设置优先级
bleManager.deinitialize();
characteristic = QualifiedCharacteristic(
serviceId: serviceId,
characteristicId: characteristicId,
deviceId: device.id);
connectionText = "一切就绪${device.name}";
});
} else {
log("NOT CONNECTED");
initBle(); // 尝试连接到设备,直到它在范围内。
}
}, onError: (Object error) {
log("error on connect: $error");
});
}, onError: (obj, stack) {
log('AN ERROR WHILE SCANNING:\r$obj\r$stack');
});
}
//写入数据
writeData(String data) async {
List<int> bytes = utf8.encode(data);
await bleManager.writeCharacteristicWithResponse(characteristic!,
value: bytes);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("ESP32S3 BLE NES手柄"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
child: characteristic == null
? const Center(
child: Text(
"Waiting...",
style: TextStyle(fontSize: 24, color: Colors.red),
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
// alignment: const Alignment(0, 0.1),
Joystick(
mode: JoystickMode.horizontalAndVertical,
listener: (details) {
if (details.x > 0) {
user["u"] = "1";
user["d"] = "0";
} else if (details.x < 0) {
user["u"] = "0";
user["d"] = "1";
} else {
user["u"] = "0";
user["d"] = "0";
}
if (details.y > 0) {
user["l"] = "1";
user["r"] = "0";
} else if (details.y < 0) {
user["l"] = "0";
user["r"] = "1";
} else {
user["l"] = "0";
user["r"] = "0";
}
String data = convert.jsonEncode(user);
writeData(data);
}),
Align(
alignment: Alignment.bottomCenter,
child: InkWell(
child: ElevatedButton(
onPressed: null,
style: ElevatedButton.styleFrom(
primary: const Color(0x00303134),
shape: const CircleBorder(),
padding: const EdgeInsets.all(20),
),
child: const Text('开始'),
),
onTapDown: (context) {
user["t"] = "1";
String data = convert.jsonEncode(user);
writeData(data);
},
onTapUp: (context) {
user["t"] = "0";
String data = convert.jsonEncode(user);
writeData(data);
},
)),
Align(
alignment: Alignment.bottomCenter,
child: InkWell(
child: ElevatedButton(
onPressed: null,
style: ElevatedButton.styleFrom(
primary: const Color(0x00303134),
shape: const CircleBorder(),
padding: const EdgeInsets.all(20),
),
child: const Text('选择'),
),
onTapDown: (context) {
user["s"] = "1";
String data = convert.jsonEncode(user);
writeData(data);
},
onTapUp: (context) {
user["s"] = "0";
String data = convert.jsonEncode(user);
writeData(data);
},
)),
Container(
height: 200,
width: 200,
decoration: BoxDecoration(
color: const Color(0x50616161),
borderRadius: BorderRadius.circular(100)
//more than 50% of width makes circle
),
child: Stack(
children: [
Align(
alignment: Alignment.topCenter,
child: InkWell(
child: ElevatedButton(
onPressed: null,
style: ElevatedButton.styleFrom(
primary: const Color(0x00303134),
shape: const CircleBorder(),
padding: const EdgeInsets.all(20),
),
child: Text(buttonCharacter[0]),
),
onTapDown: (context) {
user["a"] = "1";
String data = convert.jsonEncode(user);
writeData(data);
},
onTapUp: (context) {
user["a"] = "0";
String data = convert.jsonEncode(user);
writeData(data);
},
)),
Align(
alignment: Alignment.bottomCenter,
child: InkWell(
child: ElevatedButton(
onPressed: null,
style: ElevatedButton.styleFrom(
primary: const Color(0x00303134),
shape: const CircleBorder(),
padding: const EdgeInsets.all(20),
),
child: Text(buttonCharacter[1]),
),
onTapDown: (context) {
user["b"] = "1";
String data = convert.jsonEncode(user);
writeData(data);
},
onTapUp: (context) {
user["b"] = "0";
String data = convert.jsonEncode(user);
writeData(data);
},
)),
Align(
alignment: Alignment.centerLeft,
child: InkWell(
child: ElevatedButton(
onPressed: null,
style: ElevatedButton.styleFrom(
primary: const Color(0x00303134),
shape: const CircleBorder(),
padding: const EdgeInsets.all(20),
),
child: Text(buttonCharacter[2]),
),
onTapDown: (context) {
user["x"] = "1";
String data = convert.jsonEncode(user);
writeData(data);
},
onTapUp: (context) {
user["x"] = "0";
String data = convert.jsonEncode(user);
writeData(data);
},
)),
Align(
alignment: Alignment.centerRight,
child: InkWell(
child: ElevatedButton(
onPressed: null,
style: ElevatedButton.styleFrom(
primary: const Color(0x00303134),
shape: const CircleBorder(),
padding: const EdgeInsets.all(20),
),
child: Text(buttonCharacter[3]),
),
onTapDown: (context) {
user["y"] = "1";
String data = convert.jsonEncode(user);
writeData(data);
},
onTapUp: (context) {
user["y"] = "0";
String data = convert.jsonEncode(user);
writeData(data);
},
)),
],
),
),
],
),
)
],
)));
}
}
编译并真机运行
flutter run
1.2_触摸坐标
安装库
flutter pub add positioned_tap_detector_2
flutter pub add flutter_reactive_ble
flutter代码
import 'dart:async';
import 'dart:convert' show utf8;
import 'dart:convert' as convert;
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:positioned_tap_detector_2/positioned_tap_detector_2.dart';
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations(
[DeviceOrientation.landscapeRight, DeviceOrientation.landscapeLeft])
.then((_) {
runApp(const MyApp());
});
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'touch coordinates with BLE',
debugShowCheckedModeBanner: false,
home: const TouchCoordinates(),
theme: ThemeData.dark(),
);
}
}
class TouchCoordinates extends StatefulWidget {
const TouchCoordinates({Key? key}) : super(key: key);
@override
TouchCoordinatesState createState() => TouchCoordinatesState();
}
class TouchCoordinatesState extends State<TouchCoordinates> {
var serviceId = Uuid.parse(
'4fafc201-1fb5-459e-8fcc-c5c9c331914b'); // REPLACE WITH YOUR SERVICE_ID
var characteristicId = Uuid.parse(
'beb5483e-36e1-4688-b7f5-ea07361b26a8'); // REPLACE WITH YOUR CHARACTERISTIC_ID
StreamSubscription? subscription;
StreamSubscription<ConnectionStateUpdate>? connection;
QualifiedCharacteristic? characteristic;
final bleManager = FlutterReactiveBle();
TapPosition _position = TapPosition(Offset.zero, Offset.zero);
double screenWidth = 480;
double screenHeight = 240;
String connectionText = "";
late Map<String, String> user = {
"x": "0",
"y": "0",
};
@override
void initState() {
super.initState();
bleManager.statusStream.listen((status) {
log("STATUS: $status");
if (status == BleStatus.ready) initBle();
});
}
@override
void dispose() {
subscription?.cancel();
connection?.cancel();
super.dispose();
}
//停止扫描
Future<void> stopScan() async {
log('HF: stopping BLE scan');
await subscription?.cancel();
subscription = null;
}
//初始化状态
void initBle() {
subscription?.cancel();
//扫描设备
subscription = bleManager.scanForDevices(
withServices: [serviceId],
scanMode: ScanMode.lowLatency).listen((device) {
log("SCAN FOUND: ${device.name}");
stopScan();
//建立连接
connection = bleManager
.connectToDevice(
id: device.id,
servicesWithCharacteristicsToDiscover: {
serviceId: [characteristicId]
},
connectionTimeout: const Duration(seconds: 2),
)
.listen((connectionState) {
log("CONNECTING: $connectionState");
if (connectionState.connectionState ==
DeviceConnectionState.connected) {
setState(() {
characteristic = QualifiedCharacteristic(
serviceId: serviceId,
characteristicId: characteristicId,
deviceId: device.id);
connectionText = "一切就绪${device.name}";
});
} else {
log("NOT CONNECTED");
initBle(); // 尝试连接到设备,直到它在范围内。
}
}, onError: (Object error) {
log("error on connect: $error");
});
}, onError: (obj, stack) {
log('AN ERROR WHILE SCANNING:\r$obj\r$stack');
});
}
//写入数据
writeData(String data) async {
List<int> bytes = utf8.encode(data);
await bleManager.writeCharacteristicWithResponse(characteristic!,value: bytes);
}
void _onTap(TapPosition position) {
_updateState(position);
}
void _updateState(TapPosition position) {
setState(() {
_position = position;
});
}
String _formatOffset(Offset offset) {
user["x"] = "${offset.dx.toInt()}";
user["y"] = "${offset.dy.toInt()}";
String data = convert.jsonEncode(user);
writeData(data);
return "x:${user["x"]}, y:${user["y"]}";
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(connectionText),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
child: characteristic == null
? const Center(
child: Text(
"Waiting...",
style: TextStyle(fontSize: 24, color: Colors.red),
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Column(
children: [
PositionedTapDetector2(
onTap: _onTap,
child: Container(
width: screenWidth,
height: screenHeight,
color: const Color(0xffccccff),
),
),
Text(
'坐标: ${_formatOffset(_position.relative!)}'),
],
),
]),
),
],
),
));
}
}
2_串口
flutter pub add flutter_libserialport
import 'dart:convert' show Utf8Decoder;
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_libserialport/flutter_libserialport.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Linux Serial Port Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Linux Serial Port Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<SerialPort> portList = [];
SerialPort? _serialPort;
List<Uint8List> receiveDataList = [];
final textInputCtrl = TextEditingController();
@override
void initState() {
super.initState();
var i = 0;
for (final name in SerialPort.availablePorts) {
final sp = SerialPort(name);
if (kDebugMode) {
print('${++i}) $name');
print('\tDescription: ${sp.description}');
print('\tManufacturer: ${sp.manufacturer}');
print('\tSerial Number: ${sp.serialNumber}');
print('\tProduct ID: 0x${sp.productId?.toRadixString(16) ?? 00}');
print('\tVendor ID: 0x${sp.vendorId?.toRadixString(16) ?? 00}');
}
portList.add(sp);
}
if (portList.isNotEmpty) {
_serialPort = portList.first;
}
}
void changedDropDownItem(SerialPort sp) {
setState(() {
_serialPort = sp;
});
}
@override
Widget build(BuildContext context) {
var openButtonText = _serialPort == null
? 'N/A'
: _serialPort!.isOpen
? 'Close'
: 'Open';
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: SizedBox(
height: double.infinity,
child: Column(
children: <Widget>[
Expanded(
flex: 1,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
DropdownButton(
value: _serialPort,
items: portList.map((item) {
return DropdownMenuItem(
value: item, child: Text("${item.name}"));
}).toList(),
onChanged: (e) {
setState(() {
changedDropDownItem(e as SerialPort);
});
},
),
const SizedBox(
width: 50.0,
),
OutlinedButton(
child: Text(openButtonText),
onPressed: () {
if (_serialPort == null) {
return;
}
if (_serialPort!.isOpen) {
_serialPort!.close();
debugPrint('${_serialPort!.name} closed!');
} else {
if (_serialPort!.open(mode: SerialPortMode.readWrite)) {
SerialPortConfig config = _serialPort!.config;
// https://www.sigrok.org/api/libserialport/0.1.1/a00007.html#gab14927cf0efee73b59d04a572b688fa0
// https://www.sigrok.org/api/libserialport/0.1.1/a00004_source.html
config.baudRate = 115200;
config.parity = 0;
config.bits = 8;
config.cts = 0;
config.rts = 0;
config.stopBits = 1;
config.xonXoff = 0;
_serialPort!.config = config;
if (_serialPort!.isOpen) {
debugPrint('${_serialPort!.name} opened!');
}
final reader = SerialPortReader(_serialPort!);
reader.stream.listen((data) {
debugPrint('received: $data');
receiveDataList.add(data);
setState(() {});
}, onError: (error) {
if (error is SerialPortError) {
debugPrint(
'error: ${error.message.toString()}, code: ${error.errorCode}');
}
});
}
}
setState(() {});
},
),
],
),
),
Expanded(
flex: 8,
child: Card(
margin: const EdgeInsets.all(10.0),
child: ListView.builder(
itemCount: receiveDataList.length,
itemBuilder: (context, index) {
/*
OUTPUT for raw bytes
return Text(receiveDataList[index].toString());
*/
/* output for string */
return Text(
const Utf8Decoder(allowMalformed: true).convert(receiveDataList[index]));
}),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: TextField(
enabled: (_serialPort != null && _serialPort!.isOpen)
? true
: false,
controller: textInputCtrl,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
),
),
),
Flexible(
child: TextButton.icon(
onPressed: (_serialPort != null && _serialPort!.isOpen)
? () {
if (_serialPort!.write(Uint8List.fromList(
textInputCtrl.text.codeUnits)) ==
textInputCtrl.text.codeUnits.length) {
setState(() {
textInputCtrl.text = '';
});
}
}
: null,
icon: const Icon(Icons.send),
label: const Text("Send"),
),
),
],
),
],
),
),
);
}
}