首次提交:太和e护APP调试模式websocket

This commit is contained in:
wyf
2026-04-25 14:46:47 +08:00
commit e5e5ca310f
14 changed files with 727 additions and 0 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
.dockerignore
Dockerfile
build/
.dart_tool/
.git/
.github/
.gitignore
.idea/
.packages

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

9
.idea/ble_debug_system.iml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/ble_debug_system.iml" filepath="$PROJECT_DIR$/.idea/ble_debug_system.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

3
CHANGELOG.md Normal file
View File

@@ -0,0 +1,3 @@
## 1.0.0
- Initial version.

21
Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
# Use latest stable channel SDK.
FROM dart:stable AS build
# Resolve app dependencies.
WORKDIR /app
COPY pubspec.* ./
RUN dart pub get
# Copy app source code (except anything in .dockerignore) and AOT compile app.
COPY . .
RUN dart compile exe bin/server.dart -o bin/server
# Build minimal serving image from AOT-compiled `/server`
# and the pre-built AOT-runtime in the `/runtime/` directory of the base image.
FROM scratch
COPY --from=build /runtime/ /
COPY --from=build /app/bin/server /app/bin/
# Start server.
EXPOSE 8089
CMD ["/app/bin/server"]

49
README.md Normal file
View File

@@ -0,0 +1,49 @@
A server app built using [Shelf](https://pub.dev/packages/shelf),
configured to enable running with [Docker](https://www.docker.com/).
This sample code handles HTTP GET requests to `/` and `/echo/<message>`
# Running the sample
## Running with the Dart SDK
You can run the example with the [Dart SDK](https://dart.dev/get-dart)
like this:
```
$ dart run bin/server.dart
Server listening on port 8080
```
And then from a second terminal:
```
$ curl http://0.0.0.0:8080
Hello, World!
$ curl http://0.0.0.0:8080/echo/I_love_Dart
I_love_Dart
```
## Running with Docker
If you have [Docker Desktop](https://www.docker.com/get-started) installed, you
can build and run with the `docker` command:
```
$ docker build . -t myserver
$ docker run -it -p 8080:8080 myserver
Server listening on port 8080
```
And then from a second terminal:
```
$ curl http://0.0.0.0:8080
Hello, World!
$ curl http://0.0.0.0:8080/echo/I_love_Dart
I_love_Dart
```
You should see the logging printed in the first terminal:
```
2021-05-06T15:47:04.620417 0:00:00.000158 GET [200] /
2021-05-06T15:47:08.392928 0:00:00.001216 GET [200] /echo/I_love_Dart
```

30
analysis_options.yaml Normal file
View File

@@ -0,0 +1,30 @@
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.
include: package:lints/recommended.yaml
# Uncomment the following section to specify additional rules.
# linter:
# rules:
# - camel_case_types
# analyzer:
# exclude:
# - path/to/excluded/files/**
# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints
# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options

385
bin/server.dart Normal file
View File

@@ -0,0 +1,385 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_web_socket/shelf_web_socket.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
// 用户连接信息
class UserConnection {
final String uuid;
final String deviceModel;
final WebSocketChannel channel;
final DateTime connectedAt;
String? currentDeviceId;
UserConnection({
required this.uuid,
required this.deviceModel,
required this.channel,
required this.connectedAt,
this.currentDeviceId,
});
}
// WebSocket 管理器
class WebSocketManager {
final Map<String, UserConnection> _devices = {};
final Map<String, WebSocketChannel> _webClients = {};
// 存储每个连接的注册信息
final Map<WebSocketChannel, String> _channelToClientId = {};
final Map<WebSocketChannel, String> _channelToDeviceUuid = {};
// 获取设备连接
UserConnection? getDevice(String uuid) {
return _devices[uuid];
}
// 获取所有设备列表
List<Map<String, dynamic>> getDeviceList() {
return _devices.entries.map((entry) {
return {
'uuid': entry.key,
'deviceModel': entry.value.deviceModel,
'connectedAt': entry.value.connectedAt.toIso8601String(),
'isConnected': true,
};
}).toList();
}
// 注册设备
void registerDevice(String uuid, String deviceModel, WebSocketChannel channel) {
_devices[uuid] = UserConnection(
uuid: uuid,
deviceModel: deviceModel,
channel: channel,
connectedAt: DateTime.now(),
);
_channelToDeviceUuid[channel] = uuid;
print('✅ 调试设备注册: $deviceModel ($uuid)');
_broadcastUserList();
}
// 注册网页客户端
void registerWebClient(String clientId, WebSocketChannel channel) {
_webClients[clientId] = channel;
_channelToClientId[channel] = clientId;
print('🌐 网页客户端注册: $clientId');
// 发送设备列表
_sendUserListToClient(channel);
}
// 处理网页消息
void handleWebMessage(WebSocketChannel channel, Map<String, dynamic> data) {
final clientId = _channelToClientId[channel];
if (clientId == null) {
print('未找到对应的网页客户端');
return;
}
print('💻 收到网页指令: ${data['type']} from client: $clientId');
final commandType = data['type'] as String;
final targetUuid = data['targetUuid'] as String?;
switch (commandType) {
case 'select_device':
if (targetUuid != null) {
final device = _devices[targetUuid];
if (device != null) {
print(' -> 网页选择控制设备: $targetUuid (${device.deviceModel})');
channel.sink.add(jsonEncode({
'type': 'device_selected',
'targetUuid': targetUuid,
'deviceModel': device.deviceModel,
'timestamp': DateTime.now().toIso8601String(),
}));
} else {
print(' -> 设备不存在: $targetUuid');
}
}
break;
case 'scan':
if (targetUuid != null) {
final device = _devices[targetUuid];
if (device != null) {
print(' -> 发送扫描命令到设备: $targetUuid');
device.channel.sink.add(jsonEncode({
'type': 'scan',
'timestamp': DateTime.now().toIso8601String(),
}));
} else {
print(' -> 设备不存在: $targetUuid');
}
}
break;
case 'stop_scan':
if (targetUuid != null) {
final device = _devices[targetUuid];
if (device != null) {
device.channel.sink.add(jsonEncode({
'type': 'stop_scan',
'timestamp': DateTime.now().toIso8601String(),
}));
print(' -> 发送停止扫描命令到设备: $targetUuid');
}
}
break;
case 'connect':
if (targetUuid != null) {
final device = _devices[targetUuid];
final deviceId = data['deviceId'] as String?;
if (device != null && deviceId != null) {
device.currentDeviceId = deviceId;
device.channel.sink.add(jsonEncode({
'type': 'connect',
'deviceId': deviceId,
'timestamp': DateTime.now().toIso8601String(),
}));
print(' -> 发送连接设备命令到: $targetUuid, 设备ID: $deviceId');
}
}
break;
case 'disconnect':
if (targetUuid != null) {
final device = _devices[targetUuid];
if (device != null) {
device.currentDeviceId = null;
device.channel.sink.add(jsonEncode({
'type': 'disconnect',
'timestamp': DateTime.now().toIso8601String(),
}));
print(' -> 发送断开连接命令到设备: $targetUuid');
}
}
break;
case 'send_data':
if (targetUuid != null) {
final device = _devices[targetUuid];
final sendData = data['data'];
if (device != null && sendData != null) {
device.channel.sink.add(jsonEncode({
'type': 'send_data',
'data': sendData,
'timestamp': DateTime.now().toIso8601String(),
}));
print(' -> 发送数据到设备: $targetUuid, 数据: $sendData');
}
}
break;
default:
print('未知命令: $commandType');
}
}
// 处理设备消息
void handleDeviceMessage(WebSocketChannel channel, Map<String, dynamic> data) {
final uuid = _channelToDeviceUuid[channel];
if (uuid == null) {
print('未找到对应的设备');
return;
}
print('📱 收到设备 $uuid 消息: ${data['type']}');
// 转发给所有网页客户端
for (var entry in _webClients.entries) {
entry.value.sink.add(jsonEncode({
'type': 'device_message',
'fromUser': uuid,
'data': data,
'timestamp': DateTime.now().toIso8601String(),
}));
}
}
// 移除设备
void removeDevice(WebSocketChannel channel) {
final uuid = _channelToDeviceUuid[channel];
if (uuid != null) {
_devices.remove(uuid);
_channelToDeviceUuid.remove(channel);
print('🗑️ 移除调试设备: $uuid');
_broadcastUserList();
}
}
// 移除网页客户端
void removeWebClient(WebSocketChannel channel) {
final clientId = _channelToClientId[channel];
if (clientId != null) {
_webClients.remove(clientId);
_channelToClientId.remove(channel);
print('网页客户端断开: $clientId');
}
}
void _sendUserListToClient(WebSocketChannel client) {
final deviceList = getDeviceList();
client.sink.add(jsonEncode({
'type': 'user_list',
'users': deviceList,
'timestamp': DateTime.now().toIso8601String(),
}));
print('📡 发送设备列表到网页客户端: ${deviceList.length} 个设备');
}
void _broadcastUserList() {
final deviceList = getDeviceList();
final message = jsonEncode({
'type': 'user_list',
'users': deviceList,
'timestamp': DateTime.now().toIso8601String(),
});
for (var client in _webClients.values) {
client.sink.add(message);
}
print('📡 广播设备列表: ${deviceList.length} 个设备');
}
}
// WebSocket 处理函数 - 所有消息都在这里统一处理
Future<Response> handleWebSocket(Request request, WebSocketManager manager) async {
final handler = webSocketHandler((WebSocketChannel channel, String? protocol) {
print('🔌 WebSocket 连接建立');
// 标识是否已经注册
bool isRegistered = false;
String? connectionType;
channel.stream.listen((message) {
try {
final data = jsonDecode(message);
final type = data['type'] as String?;
print('收到消息,类型: $type, 已注册: $isRegistered');
if (!isRegistered) {
// 处理注册消息
if (type == 'device_register') {
final uuid = data['uuid'] as String?;
final deviceModel = data['deviceModel'] as String?;
if (uuid != null && deviceModel != null) {
isRegistered = true;
connectionType = 'device';
manager.registerDevice(uuid, deviceModel, channel);
} else {
print('设备注册信息不完整');
channel.sink.close();
}
} else if (type == 'web_register') {
final clientId = data['clientId'] as String? ??
'web_${DateTime.now().millisecondsSinceEpoch}';
isRegistered = true;
connectionType = 'web';
manager.registerWebClient(clientId, channel);
} else {
print('等待注册消息,收到: $type');
channel.sink.close();
}
} else {
// 已注册,根据连接类型处理后续消息
if (connectionType == 'web') {
manager.handleWebMessage(channel, data);
} else if (connectionType == 'device') {
manager.handleDeviceMessage(channel, data);
} else {
print('未知连接类型: $connectionType');
}
}
} catch (e) {
print('解析消息失败: $e');
}
}, onDone: () {
// 连接关闭时的清理
if (connectionType == 'device') {
manager.removeDevice(channel);
} else if (connectionType == 'web') {
manager.removeWebClient(channel);
}
print('🔌 连接关闭');
}, onError: (error) {
print('连接错误: $error');
if (connectionType == 'device') {
manager.removeDevice(channel);
} else if (connectionType == 'web') {
manager.removeWebClient(channel);
}
});
});
return handler(request);
}
// CORS 中间件
Middleware createCorsMiddleware() {
return (Handler innerHandler) {
return (Request request) async {
if (request.method == 'OPTIONS') {
return Response.ok('', headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Origin, Content-Type, X-Requested-With',
'Access-Control-Allow-Credentials': 'true',
});
}
final response = await innerHandler(request);
return response.change(headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Origin, Content-Type, X-Requested-With',
'Access-Control-Allow-Credentials': 'true',
});
};
};
}
void main() async {
final manager = WebSocketManager();
Future<Response> router(Request request) async {
final path = request.url.path;
print('📨 请求: ${request.method} $path');
if (path == 'ws') {
return await handleWebSocket(request, manager);
}
if (path == 'users') {
return Response.ok(
jsonEncode(manager.getDeviceList()),
headers: {'Content-Type': 'application/json'},
);
}
return Response.notFound('Not Found');
}
final handler = const Pipeline()
.addMiddleware(logRequests())
.addMiddleware(createCorsMiddleware())
.addHandler(router);
//端口
final server = await io.serve(handler, '0.0.0.0', 8089);
print('\n🚀 ========================================');
print('✅ 服务器启动成功!');
print('🌐 HTTP 地址: http://${server.address.host}:${server.port}');
print('🔌 WebSocket 地址: ws://${server.address.host}:${server.port}/ws');
print('📋 用户列表 API: http://${server.address.host}:${server.port}/users');
print('========================================\n');
}

173
pubspec.lock Normal file
View File

@@ -0,0 +1,173 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.13.1"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.2"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.7"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.1.2"
lints:
dependency: "direct dev"
description:
name: lints
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.1"
meta:
dependency: transitive
description:
name: meta
sha256: df0c643f44ad098eb37988027a8e2b2b5a031fd3977f06bbfd3a76637e8df739
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.18.2"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.1"
shelf:
dependency: "direct main"
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.2"
shelf_cors_headers:
dependency: "direct main"
description:
name: shelf_cors_headers
sha256: a127c80f99bbef3474293db67a7608e3a0f1f0fcdb171dad77fa9bd2cd123ae4
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.1.5"
shelf_static:
dependency: "direct main"
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: "direct main"
description:
name: shelf_web_socket
sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.4"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.2"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.0"
web:
dependency: transitive
description:
name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.5.1"
web_socket_channel:
dependency: "direct main"
description:
name: web_socket_channel
sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.5"
sdks:
dart: ">=3.5.4 <4.0.0"

17
pubspec.yaml Normal file
View File

@@ -0,0 +1,17 @@
name: ble_debug_system
description: A server app using the shelf package and Docker.
version: 1.0.0
# repository: https://github.com/my_org/my_repo
environment:
sdk: ^3.5.4
dependencies:
shelf: ^1.4.0
shelf_web_socket: ^1.0.4
web_socket_channel: ^2.4.0
shelf_static: ^1.1.0
shelf_cors_headers: ^0.1.5
dev_dependencies:
lints: ^2.0.0