From e5e5ca310f9df7e2fa75426a15eb4942bc41a1dd Mon Sep 17 00:00:00 2001 From: wyf <494641114@qq.com> Date: Sat, 25 Apr 2026 14:46:47 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A6=96=E6=AC=A1=E6=8F=90=E4=BA=A4=EF=BC=9A?= =?UTF-8?q?=E5=A4=AA=E5=92=8Ce=E6=8A=A4APP=E8=B0=83=E8=AF=95=E6=A8=A1?= =?UTF-8?q?=E5=BC=8Fwebsocket?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 9 + .gitignore | 3 + .idea/.gitignore | 8 + .idea/ble_debug_system.iml | 9 + .idea/misc.xml | 6 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + CHANGELOG.md | 3 + Dockerfile | 21 ++ README.md | 49 +++++ analysis_options.yaml | 30 +++ bin/server.dart | 385 +++++++++++++++++++++++++++++++++++++ pubspec.lock | 173 +++++++++++++++++ pubspec.yaml | 17 ++ 14 files changed, 727 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/ble_debug_system.iml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 CHANGELOG.md create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 analysis_options.yaml create mode 100644 bin/server.dart create mode 100644 pubspec.lock create mode 100644 pubspec.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..21504f8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.dockerignore +Dockerfile +build/ +.dart_tool/ +.git/ +.github/ +.gitignore +.idea/ +.packages diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a85790 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/ble_debug_system.iml b/.idea/ble_debug_system.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/ble_debug_system.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..639900d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..2cbeecb --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c412e08 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..e695d9d --- /dev/null +++ b/README.md @@ -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/` + +# 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 +``` diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..dee8927 --- /dev/null +++ b/analysis_options.yaml @@ -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 diff --git a/bin/server.dart b/bin/server.dart new file mode 100644 index 0000000..ef66baa --- /dev/null +++ b/bin/server.dart @@ -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 _devices = {}; + final Map _webClients = {}; + + // 存储每个连接的注册信息 + final Map _channelToClientId = {}; + final Map _channelToDeviceUuid = {}; + + // 获取设备连接 + UserConnection? getDevice(String uuid) { + return _devices[uuid]; + } + + // 获取所有设备列表 + List> 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 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 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 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 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'); +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..854e204 --- /dev/null +++ b/pubspec.lock @@ -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" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..50d8271 --- /dev/null +++ b/pubspec.yaml @@ -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 \ No newline at end of file