This commit is contained in:
2026-04-21 12:57:33 +08:00
commit c000eb12f8
64 changed files with 7970 additions and 0 deletions

31
.gitignore vendored Normal file
View File

@@ -0,0 +1,31 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
build/
.omc/

30
.metadata Normal file
View File

@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "761747bfc538b5af34aa0d3fac380f1bc331ec49"
channel: "stable"
project_type: plugin
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
- platform: android
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View File

@@ -0,0 +1,198 @@
# Architecture
**Analysis Date:** 2026-04-16
## Pattern Overview
**Overall:** Flutter Platform Plugin (Federated Plugin Pattern)
This is an Android-only Flutter plugin that wraps the WCH CH34X UART library (`CH34XUARTDriver.jar`). It bridges Flutter/Dart code to native Android USB serial communication via `MethodChannel` and `EventChannel`.
**Key Characteristics:**
- Static facade pattern for public API (`Ch34Manager`)
- Platform interface pattern with token-based verification (`Ch34Platform`)
- MethodChannel for command invocation, EventChannel for streaming callbacks
- Single platform implementation (Android only, Kotlin/Java)
- Singleton WCH UART manager on the native side
## Layers
### Dart Public API Layer
- **Purpose:** Static facade providing the consumer-facing API
- **Location:** `D:\code\new_git_code\flutter\ch34\lib\src\ch34_manager.dart`
- **Contains:** Static methods delegating to `Ch34Platform.instance`
- **Depends on:** `Ch34Platform`
- **Used by:** Flutter app consuming the plugin
### Dart Platform Interface Layer
- **Purpose:** Abstract interface defining all plugin methods with token verification
- **Location:** `D:\code\new_git_code\flutter\ch34\lib\src\ch34_platform_interface.dart`
- **Contains:** `abstract class Ch34Platform extends PlatformInterface` with 33 method signatures
- **Depends on:** `plugin_platform_interface` package, `ch34_types.dart`
- **Used by:** `MethodChannelCh34` for concrete implementation
### Dart MethodChannel Implementation Layer
- **Purpose:** Bridges abstract platform interface to native Android via Flutter's platform channels
- **Location:** `D:\code\new_git_code\flutter\ch34\lib\src\ch34_method_channel.dart`
- **Contains:** `class MethodChannelCh34 extends Ch34Platform` with actual channel communication
- **Channels:**
- `MethodChannel('ch34')` - synchronous method calls
- `EventChannel('ch34/data')` - serial data stream
- `EventChannel('ch34/modem')` - modem status stream
- `EventChannel('ch34/usb_state')` - USB hotplug events
- **Depends on:** `dart:async`, `flutter/services.dart`
- **Used by:** `Ch34Manager` (via `Ch34Platform.instance`)
### Dart Types Layer
- **Purpose:** Shared data types and enums for the plugin
- **Location:** `D:\code\new_git_code\flutter\ch34\lib\src\types\ch34_types.dart`
- **Contains:**
- Enums: `DataBits`, `StopBits`, `Parity`, `GpioDirection`, `GpioValue`, `SerialErrorType`
- Data classes: `GpioStatus`, `SerialParameter`, `ModemStatus`, `UsbDeviceInfo`
- Exception: `Ch34Exception` (defined in `ch34_method_channel.dart`)
### Android Native Layer
- **Purpose:** Actual WCH UART library interaction, USB device management
- **Location:** `D:\code\new_git_code\flutter\ch34\android\src\main\java\com\example\ch34\`
- **Contains:**
- `Ch34Plugin.java` - Main plugin entry point, MethodCallHandler
- `Ch34DataStreamHandler.java` - Data EventChannel stream handler with WCH callback bridging
- `Ch34ModemStreamHandler.java` - Modem status EventChannel stream handler
- `Ch34UsbStateStreamHandler.java` - USB hotplug EventChannel with BroadcastReceiver
- `Ch34TypeConverter.java` - Type conversion between Dart and WCH types
- **Depends on:** `CH34XUARTDriver.jar` (bundled in `android/libs/`)
- **Used by:** Dart MethodChannel layer
### Backward Compatibility Layer
- **Location:** `D:\code\new_git_code\flutter\ch34\lib\ch34_platform_interface.dart`, `D:\code\new_git_code\flutter\ch34\lib\ch34_method_channel.dart`
- **Purpose:** Re-exports from `src/` for consumers using old import paths
## Data Flow
### Device Discovery Flow
1. Consumer calls `Ch34Manager.enumDevice()`
2. Delegates to `Ch34Platform.instance.enumDevice()`
3. `MethodChannelCh34.enumDevice()` invokes `methodChannel.invokeMethod('enumDevice')`
4. `Ch34Plugin.enumDevice()` calls `WCHUARTManager.enumDevice()` on native side
5. Native iterates devices, extracts VID/PID/serialCount/chipType
6. Returns `List<Map>` through MethodChannel
7. Dart side deserializes into `List<UsbDeviceInfo>`
### Data Read Flow (Callback Mode)
1. Consumer calls `Ch34Manager.registerDataCallback(deviceName, serialNumber, onData)`
2. `MethodChannelCh34` subscribes to `EventChannel('ch34/data')`
3. Invokes `methodChannel.invokeMethod('registerDataCallback')`
4. `Ch34Plugin` registers `Ch34DataStreamHandler.getWchCallback()` with `WCHUARTManager.registerDataCallback()`
5. When data arrives, WCH triggers `IDataCallback.onData()`
6. `Ch34DataStreamHandler` bridges data to EventChannel
7. Dart receives via `StreamSubscription`, invokes user callback
### Data Write Flow
1. Consumer calls `Ch34Manager.writeData(deviceName, serialNumber, data)`
2. Delegates through platform interface to `MethodChannelCh34`
3. `methodChannel.invokeMethod('writeData', {data: data, ...})`
4. `Ch34Plugin.writeData()` calls `WCHUARTManager.writeData(device, serial, data, length, timeout)`
5. Returns bytes written count
### USB Hotplug Event Flow
1. `Ch34UsbStateStreamHandler` registers `BroadcastReceiver` for `ACTION_USB_DEVICE_ATTACHED/DETACHED`
2. Android system sends broadcast on USB events
3. BroadcastReceiver extracts `UsbDevice` and calls `notifyStateChanged()`
4. EventChannel sends `{deviceName, connected}` to Dart
5. `MethodChannelCh34` invokes `_usbStateCallback`
### Device Open Flow
1. Consumer calls `Ch34Manager.openDevice(deviceName)`
2. `Ch34Plugin.openDevice()` attempts `WCHUARTManager.openDevice(device)`
3. If `NoPermissionException`, requests permission then retries after 2s delay
4. On success, adds to `openedDevices` map and notifies `usbStateStreamHandler`
## State Management
**Native State:**
- `WCHUARTManager` - Singleton instance, lazy-initialized in `ensureManagerInitialized()`
- `openedDevices: Map<String, UsbDevice>` - tracks currently opened devices
- Three `EventChannel.StreamHandler` instances - each manages its own `EventSink`
**Dart State:**
- `Ch34Platform._instance` - Singleton platform instance
- `MethodChannelCh34` maintains `_dataSubscription`, `_modemSubscription`, `_usbStateSubscription` - StreamSubscriptions for event channels
- Callback references: `_dataCallback`, `_modemCallback`, `_usbStateCallback`
**State Cleanup:**
- `Ch34Plugin.close()` calls `closeAllDevices()` to disconnect all and clear map
- `Ch34UsbStateStreamHandler.onCancel()` unregisters BroadcastReceiver
- `Ch34Manager.close()` triggers platform cleanup and cancels all subscriptions
## Key Abstractions
### Platform Interface (`Ch34Platform`)
- **Purpose:** Abstract contract that decouples Flutter code from native implementation
- **Example:** `D:\code\new_git_code\flutter\ch34\lib\src\ch34_platform_interface.dart`
- **Pattern:** Token-based platform interface verification from `plugin_platform_interface` package
- **Pattern:**
```dart
static final Object _token = Object();
static Ch34Platform get instance { ... }
static void registerDefaultInstance(Ch34Platform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
```
### Manager Facade (`Ch34Manager`)
- **Purpose:** Static facade simplifying API usage for consumers
- **Example:** `D:\code\new_git_code\flutter\ch34\lib\src\ch34_manager.dart`
- **Pattern:** All static methods, all delegate to `Ch34Platform.instance`
- **Pattern:**
```dart
static Future<List<UsbDeviceInfo>> enumDevice() {
return Ch34Platform.instance.enumDevice();
}
```
### Type Converter (`Ch34TypeConverter`)
- **Purpose:** Bridge between Dart enum indices and WCH native enum values
- **Example:** `D:\code\new_git_code\flutter\ch34\android\src\main\java\com\example\ch34\Ch34TypeConverter.java`
- **Pattern:** Static utility class with conversion methods for GPIO direction/value, serial error types
## Entry Points
### Dart Library Entry
- **Location:** `D:\code\new_git_code\flutter\ch34\lib\ch34.dart`
- **Triggers:** Consumer imports `package:ch34/ch34.dart`
- **Responsibilities:** Exports `Ch34Manager`, `Ch34Platform`, all types, and `Ch34Exception`
### Plugin Initialization (Android)
- **Location:** `D:\code\new_git_code\flutter\ch34\android\src\main\java\com\example\ch34\Ch34Plugin.java`
- **Trigger:** Flutter engine attaches plugin via `onAttachedToEngine()`
- **Responsibilities:** Creates MethodChannel, 3 EventChannels, sets MethodCallHandler
- **Declaration:** `D:\code\new_git_code\flutter\ch34\android\build.gradle` + `pubspec.yaml` declares `package: com.example.ch34`, `pluginClass: Ch34Plugin`
### MethodChannel Dispatcher
- **Location:** `D:\code\new_git_code\flutter\ch34\android\src\main\java\com\example\ch34\Ch34Plugin.java` line 80-187
- **Trigger:** Dart invokes `methodChannel.invokeMethod()`
- **Responsibilities:** Switch dispatch to 33 method handlers, error handling wrapper
## Error Handling
**Strategy:** Native exceptions caught and converted to MethodChannel error responses with error codes; Dart side uses null-coalescing defaults and `Ch34Exception` for explicit failures.
**Patterns:**
- **Native:** Each method handler has try-catch, returns `result.error(ERROR_CODE, message, null)`
- Error codes: `ENUM_DEVICE_FAILED`, `OPEN_DEVICE_FAILED`, `PERMISSION_DENIED`, `WRITE_DATA_FAILED`, etc.
- **Dart:** Default fallbacks on null results (e.g., `result ?? false`, `result ?? Uint8List(0)`)
- **Exception class:** `Ch34Exception` in `D:\code\new_git_code\flutter\ch34\lib\src\ch34_method_channel.dart` for throwing on Dart side
- **Logging:** All errors logged via `Log.e(TAG, "...")` on Android native side
## Cross-Cutting Concerns
**Logging:** Android `android.util.Log` with per-class TAGs (`Ch34Plugin`, `Ch34DataStream`, `Ch34ModemStream`, `Ch34UsbStateStream`). Dart side uses `flutter/foundation.dart` `debugPrint()` for callback errors.
**Validation:** Device name resolution via `getDeviceOrThrow()` which searches `openedDevices` map first, then falls back to `manager.enumDevice()` lookup.
**Authentication/Permission:** USB permission flow handled in `openDevice()` - attempts open, if `NoPermissionException`, calls `manager.requestPermission()` then retries after 2-second delay.
---
*Architecture analysis: 2026-04-16*

View File

@@ -0,0 +1,167 @@
# Codebase Concerns
**Analysis Date:** 2026-04-16
## Technical Debt
### Placeholder Content in LICENSE and CHANGELOG
- Issue: `LICENSE` contains "TODO: Add your license here" and `CHANGELOG.md` contains "TODO: Describe initial release." No license or changelog content exists.
- Files: `D:\code\new_git_code\flutter\ch34\LICENSE`, `D:\code\new_git_code\flutter\ch34\CHANGELOG.md`
- Impact: Legal risk - the package has no defined license. No version history for consumers.
- Fix approach: Add an appropriate open-source license (e.g., MIT). Document release history in CHANGELOG.
### Unimplemented Modem Error Callbacks
- Issue: In `registerModemStatusCallback`, the `IModemStatus` interface has three error callbacks (`onOverrunError`, `onParityError`, `onFrameError`) with empty bodies - they silently discard errors.
- Files: `D:\code\new_git_code\flutter\ch34\android\src\main\java\com\example\ch34\Ch34Plugin.java` (lines 667-677)
- Impact: Serial communication errors are invisible to the Flutter layer. Users cannot detect data corruption.
- Fix approach: Forward these errors through the EventChannel or the data callback, or expose a separate error stream.
### GPIO Status Map Always Returns Hardcoded Values
- Issue: `Ch34TypeConverter.gpioStatusToMap()` hardcodes `index` and `value` to 0, regardless of the actual `GPIO_Status` object. Only `enabled` and `direction` are read from the status.
- Files: `D:\code\new_git_code\flutter\ch34\android\src\main\java\com\example\ch34\Ch34TypeConverter.java` (lines 28-29)
- Impact: Dart side `queryGpioStatus()` and `queryAllGpioStatus()` always return incorrect `index` and `value` fields.
- Fix approach: Read `status.getIndex()` and `status.getValue()` from the WCH `GPIO_Status` object.
## Known Bugs
### Data Callback Only Supports Single Device Per Serial Number
- Issue: `Ch34DataStreamHandler` uses a single `_dataCallback` field. When multiple devices register data callbacks, each new registration overwrites the previous one. Only the last registered device's data is received.
- Files: `D:\code\new_git_code\flutter\ch34\lib\src\ch34_method_channel.dart` (line 44), `D:\code\new_git_code\flutter\ch34\android\src\main\java\com\example\ch34\Ch34Plugin.java` (lines 388-407)
- Impact: Multi-device scenarios lose data from all but the most recently registered device.
- Fix approach: Use a map keyed by `(deviceName, serialNumber)` to manage multiple independent subscriptions.
### Hardcoded USB Permission Wait Time
- Issue: In `openDevice`, when the initial open fails, a `Handler.postDelayed` waits 2000ms and retries. This is a magic number that may be insufficient on slow devices or excessive on fast ones.
- Files: `D:\code\new_git_code\flutter\ch34\android\src\main\java\com\example\ch34\Ch34Plugin.java` (line 278)
- Impact: Permission may not be granted in time on some devices, leading to false failure.
- Fix approach: Use a proper permission result callback or `PendingIntent` instead of a fixed delay.
### `tryOpenDevice` Swallows Non-Permission Exceptions
- Issue: In `tryOpenDevice`, any non-`NoPermissionException` error logs the error but returns `true`, treating it as success.
- Files: `D:\code\new_git_code\flutter\ch34\android\src\main\java\com\example\ch34\Ch34Plugin.java` (lines 291-300)
- Impact: Failed device opens are reported as successful, leading to downstream errors that are harder to debug.
- Fix approach: Return `false` for any exception and let the caller handle the error properly.
## Security Concerns
### No Input Validation on Native Method Arguments
- Issue: The Java plugin directly casts method call arguments without null checks or type validation (e.g., `call.argument("deviceName")` cast to `String` without null guards).
- Files: `D:\code\new_git_code\flutter\ch34\android\src\main\java\com\example\ch34\Ch34Plugin.java` (throughout)
- Impact: Malformed or malicious method call arguments from Flutter can cause `ClassCastException` or `NullPointerException` on the Android side.
- Fix approach: Add null/type checks before using arguments. Return error results for invalid inputs.
### `pubspec.lock` Ignored for Library Packages
- Issue: `pubspec.lock` is in `.gitignore`, which is correct for library packages. However, `example/pubspec.lock` is not explicitly tracked, meaning the example app's dependency versions are not pinned.
- Files: `D:\code\new_git_code\flutter\ch34\.gitignore` (line 26)
- Impact: Example app may resolve to different dependency versions on different machines, causing inconsistent test results.
- Fix approach: Consider committing `example/pubspec.lock` for reproducible example builds.
## Performance Bottlenecks
### Synchronous `enumDevice` in `getDeviceOrThrow`
- Issue: `getDeviceOrThrow` calls `manager.enumDevice()` to enumerate all devices just to find one by name. This is called for nearly every method invocation.
- Files: `D:\code\new_git_code\flutter\ch34\android\src\main\java\com\example\ch34\Ch34Plugin.java` (lines 772-787)
- Impact: USB enumeration is slow and repeated unnecessarily. Every method call (read, write, set parameters) triggers this scan if the device is not in `openedDevices`.
- Fix approach: Cache device references when they are discovered, or maintain a device lookup map.
### Blocking `readData` Without Timeout
- Issue: `readData` calls `manager.readData(device, serialNumber)` synchronously with no timeout. If no data arrives, the method blocks indefinitely.
- Files: `D:\code\new_git_code\flutter\ch34\android\src\main\java\com\example\ch34\Ch34Plugin.java` (lines 373-385), `D:\code\new_git_code\flutter\ch34\lib\src\ch34_method_channel.dart` (lines 176-185)
- Impact: Flutter UI thread could freeze if a `readData` call blocks on a quiet serial line.
- Fix approach: Use the async callback-based approach for reading, or enforce a configurable timeout.
## Fragile Areas
### Single Global WCHUARTManager Instance
- Issue: The entire plugin uses a single `WCHUARTManager` instance stored as a field. It is lazily initialized in `ensureManagerInitialized()` and never reset except on engine detach.
- Files: `D:\code\new_git_code\flutter\ch34\android\src\main\java\com\example\ch34\Ch34Plugin.java` (lines 57, 757-770)
- Impact: If the WCH library is in a bad state (e.g., after an unhandled error), there is no recovery mechanism. Hot restart during development may cause initialization issues.
- Fix approach: Add a `reset()` or `reinitialize()` method for recovery scenarios.
### EventChannel Single Instance for All Devices
- Issue: Each EventChannel (`ch34/data`, `ch34/modem`, `ch34/usb_state`) has a single StreamHandler instance for all devices. The data handler must multiplex all device data through one channel.
- Files: `D:\code\new_git_code\flutter\ch34\android\src\main\java\com\example\ch34\Ch34Plugin.java` (lines 66-76), `D:\code\new_git_code\flutter\ch34\android\src\main\java\com\example\ch34\Ch34DataStreamHandler.java`
- Impact: Adding device identification to each data event is the responsibility of the consumer. No per-device stream isolation.
- Fix approach: Include `deviceName` and `serialNumber` in each data event payload, or create per-device EventChannels.
### `removeDataCallback` Ignores `serialNumber`
- Issue: The Dart API `removeDataCallback` takes only `deviceName`, but `registerDataCallback` takes both `deviceName` and `serialNumber`. If a device has multiple serial ports, removing the callback for one serial number removes all.
- Files: `D:\code\new_git_code\flutter\ch34\lib\src\ch34_platform_interface.dart` (line 166), `D:\code\new_git_code\flutter\ch34\lib\src\ch34_manager.dart` (lines 160-162)
- Impact: Multi-port devices cannot independently manage callbacks per serial port.
- Fix approach: Add `serialNumber` parameter to `removeDataCallback`.
### Swallowed Exceptions in `enumDevice` Loop
- Issue: In `enumDevice`, exceptions during `getSerialCount` or `getChipType` are silently caught and ignored. The device is still reported but with incomplete data.
- Files: `D:\code\new_git_code\flutter\ch34\android\src\main\java\com\example\ch34\Ch34Plugin.java` (lines 215)
- Impact: Consumers may receive devices with `serialCount = -1` and `chipType = null` with no indication of what went wrong.
- Fix approach: At minimum log the exception, or distinguish between "no data available" and "error reading data".
## Missing Patterns
### No Structured Error Handling
- Issue: The plugin uses `result.error(code, message, null)` with string codes for error reporting. The Dart side never throws typed exceptions (except `Ch34Exception` which is only used for GPIO failures). Most errors silently return `false` or `0`.
- Files: `D:\code\new_git_code\flutter\ch34\lib\src\ch34_method_channel.dart` (throughout), `D:\code\new_git_code\flutter\ch34\android\src\main\java\com\example\ch34\Ch34Plugin.java` (throughout)
- Impact: Consumers cannot distinguish between different failure modes. Debugging requires enabling debug mode and reading logcat.
- Fix approach: Throw `Ch34Exception` with error code and message from all `result.error` responses. Define error code constants.
### No Lifecycle Management
- Issue: The plugin does not handle Android lifecycle events (e.g., app going to background). Active USB connections may be left open or become invalid.
- Files: `D:\code\new_git_code\flutter\ch34\android\src\main\java\com\example\ch34\Ch34Plugin.java`
- Impact: When the app resumes, existing connections may be in an undefined state.
- Fix approach: Implement `ActivityAware` or use `AppLifecycleState` in Dart to pause/resume or clean up connections.
### No Unit Test Coverage for Core Logic
- Issue: The only unit tests cover `getPlatformVersion` and `enumDevice` with stub returns. None of the data conversion, parameter handling, or error paths are tested.
- Files: `D:\code\new_git_code\flutter\ch34\test\ch34_test.dart`, `D:\code\new_git_code\flutter\ch34\test\ch34_method_channel_test.dart`, `D:\code\new_git_code\flutter\ch34\android\src\test\java\com\example\ch34\Ch34PluginTest.java`
- Impact: Refactoring or bug fixes risk introducing regressions with no automated detection.
- Fix approach: Add tests for type conversion, error handling, and the mock platform covering all API methods.
## Dependencies Risk
### Bundled JAR Dependency Without Version Pinning
- Issue: The WCH `CH34XUARTDriver.jar` is included as a local JAR file in `android/libs/` with no version information in the build file. It is loaded via `flatDir` repository.
- Files: `D:\code\new_git_code\flutter\ch34\android\build.gradle` (line 47)
- Impact: No easy way to track which version of the WCH library is used. Cannot update to newer versions without manual replacement. No transitive dependency management.
- Fix approach: Document the JAR version used. Consider hosting in a Maven repository for proper version management.
### Android Gradle Plugin Version is Outdated
- Issue: The build uses `com.android.tools.build:gradle:7.3.0` which is significantly outdated. Current versions are 8.x.
- Files: `D:\code\new_git_code\flutter\ch34\android\build.gradle` (line 11)
- Impact: May not be compatible with newer Flutter/Android SDK versions. Missing bug fixes and performance improvements.
- Fix approach: Update to AGP 8.x and test thoroughly. Update `compileSdk` and `targetSdk` accordingly.
### Java 8 Target Compatibility
- Issue: The plugin compiles with `JavaVersion.VERSION_1_8` as source and target.
- Files: `D:\code\new_git_code\flutter\ch34\android\build.gradle` (lines 35-36)
- Impact: Cannot use modern Java language features. May limit compatibility with future Android tooling.
- Fix approach: Consider updating to Java 11+ if the minimum SDK and tooling support it.
## Test Coverage Gaps
### No Integration Tests for USB Operations
- Issue: The single integration test only checks `getPlatformVersion`. No tests for actual USB device operations.
- Files: `D:\code\new_git_code\flutter\ch34\example\integration_test\plugin_integration_test.dart`
- Risk: USB-specific bugs are only caught through manual testing.
- Priority: High
### No Tests for Type Conversions
- Issue: `Ch34TypeConverter` has zero test coverage. The GPIO and error type conversions are not verified.
- Files: `D:\code\new_git_code\flutter\ch34\android\src\test\java\com\example\ch34\Ch34PluginTest.java`
- Risk: Conversion bugs (like the hardcoded GPIO values) go undetected.
- Priority: High
### No Tests for EventChannel Streams
- Issue: No tests verify that data callbacks, modem status, or USB state events flow correctly from native to Dart.
- Files: `D:\code\new_git_code\flutter\ch34\test\ch34_method_channel_test.dart`, `D:\code\new_git_code\flutter\ch34\test\ch34_test.dart`
- Risk: Stream subscription and cancellation logic may have bugs that only manifest at runtime.
- Priority: Medium
### No Tests for Exception Paths
- Issue: All tests use happy-path mock returns. No tests simulate errors, timeouts, or null responses.
- Files: `D:\code\new_git_code\flutter\ch34\test\ch34_test.dart`
- Risk: Error handling code is untested and may fail silently or crash.
- Priority: Medium
---
*Concerns audit: 2026-04-16*

View File

@@ -0,0 +1,237 @@
# Coding Conventions
**Analysis Date:** 2026-04-16
## Overview
This is a Flutter plugin package (`ch34`) that provides USB-to-serial communication for WCH CH34X series chips. The codebase is written in Dart with Android native code (Java). The plugin follows the standard Flutter plugin federated architecture pattern.
## Naming Patterns
**Files:**
- Dart files use `snake_case`: `ch34_manager.dart`, `ch34_platform_interface.dart`, `ch34_method_channel.dart`, `ch34_types.dart`
- Test files mirror source file names: `ch34_test.dart`, `ch34_method_channel_test.dart`
- Java native files use `PascalCase`: `Ch34Plugin.java`, `Ch34UsbStateStreamHandler.java`
**Classes:**
- PascalCase with `Ch34` prefix for public API: `Ch34Manager`, `Ch34Platform`, `MethodChannelCh34`, `Ch34Exception`
- Domain models without prefix: `GpioStatus`, `SerialParameter`, `ModemStatus`, `UsbDeviceInfo`
- Enum naming: `DataBits`, `StopBits`, `Parity`, `GpioDirection`, `GpioValue`, `SerialErrorType`
**Functions/Methods:**
- camelCase for all methods: `getPlatformVersion()`, `enumDevice()`, `openDevice()`, `setSerialParameter()`
- Callback-style methods use descriptive names: `setUsbStateListener()`, `registerDataCallback()`, `registerModemStatusCallback()`
- Private methods use underscore prefix: `_cancelDataSubscription()`
- Private constructor pattern: `Ch34Manager._()` (static-only class)
**Variables:**
- camelCase: `deviceName`, `serialNumber`, `gpioIndex`, `onStateChanged`
- Private fields use underscore prefix: `_dataSubscription`, `_modemCallback`
- Constants use const: `const MethodChannel('ch34')`
**Parameters:**
- Named parameters use curly braces for optional: `{int timeout = 0}`
- Required named parameters use `required` keyword in data classes
## Code Style
**Formatting:**
- Uses `flutter_lints` package (`^3.0.0`) with default `flutter.yaml` ruleset
- Config: `D:/code/new_git_code/flutter/ch34/analysis_options.yaml`
- Example app config: `D:/code/new_git_code/flutter/ch34/example/analysis_options.yaml` (same base, with commented-out custom rules)
- No custom lint rules are enabled beyond the default Flutter set
- No `.prettierrc` or other formatter config (uses Dart formatter defaults)
**SDK Constraints:**
- SDK: `>=3.4.3 <4.0.0` (`D:/code/new_git_code/flutter/ch34/pubspec.yaml`)
- Flutter: `>=3.3.0`
**Import Organization:**
1. Dart SDK imports first: `import 'dart:typed_data';`, `import 'dart:async';`
2. Flutter framework imports: `import 'package:flutter/services.dart';`
3. Third-party package imports: `import 'package:plugin_platform_interface/plugin_platform_interface.dart';`
4. Relative local imports last: `import 'ch34_platform_interface.dart';`, `import 'types/ch34_types.dart';`
- Groups separated by blank lines
**Path Aliases:**
- No path aliases detected; all imports use relative paths within the package
## Patterns
**Static Manager Pattern:**
`D:/code/new_git_code/flutter/ch34/lib/src/ch34_manager.dart` uses a private constructor (`Ch34Manager._()`) with all static methods, delegating to `Ch34Platform.instance`. This provides a clean public API without requiring instantiation.
```dart
class Ch34Manager {
Ch34Manager._();
static Future<String?> getPlatformVersion() {
return Ch34Platform.instance.getPlatformVersion();
}
}
```
**Platform Interface (Federated Plugin):**
`D:/code/new_git_code/flutter/ch34/lib/src/ch34_platform_interface.dart` extends `PlatformInterface` from `plugin_platform_interface`. Uses token-based verification for instance safety:
```dart
abstract class Ch34Platform extends PlatformInterface {
Ch34Platform() : super(token: _token);
static final Object _token = Object();
static Ch34Platform? _instance;
static Ch34Platform get instance { ... }
static set instance(Ch34Platform instance) { ... }
}
```
**MethodChannel + EventChannel Pattern:**
`D:/code/new_git_code/flutter/ch34/lib/src/ch34_method_channel.dart` uses:
- `MethodChannel` for request-response calls: `methodChannel.invokeMethod<bool>('openDevice', {...})`
- `EventChannel` for streaming data: `dataEventChannel.receiveBroadcastStream({'deviceName': deviceName}).listen(...)`
- `@visibleForTesting` annotation for testable fields (channel names)
**Data Class Pattern:**
`D:/code/new_git_code/flutter/ch34/lib/src/types/ch34_types.dart` uses immutable classes with:
- `const` constructors
- `fromMap()` factory for deserialization
- `toMap()` method for serialization
- `toString()` override for debugging
- `copyWith()` for mutable updates (e.g., `SerialParameter.copyWith()`)
- `==`/`hashCode` override for equality (e.g., `UsbDeviceInfo`)
```dart
class SerialParameter {
const SerialParameter({
this.baud = 115200,
this.dataBits = DataBits.bits8,
...
});
factory SerialParameter.fromMap(Map<dynamic, dynamic> map) { ... }
Map<String, dynamic> toMap() { ... }
SerialParameter copyWith({ ... }) { ... }
}
```
**Enum with Associated Values:**
Enums use associated integer values with `fromValue()` factory:
```dart
enum DataBits {
bits5(5), bits6(6), bits7(7), bits8(8);
const DataBits(this.value);
final int value;
static DataBits fromValue(int value) =>
DataBits.values.firstWhere((e) => e.value == value, orElse: () => DataBits.bits8);
}
```
**Null-Safe Default Handling:**
Method channel results consistently use null-coalescing for safe defaults:
```dart
return result ?? false; // boolean methods
return result ?? 0; // integer methods
return result ?? []; // list methods
return result ?? Uint8List(0); // byte data
```
**Section Comments:**
Code is organized with clear separator comments:
```dart
/// ==================== 基础方法 ====================
/// ==================== 设备枚举与识别 ====================
/// ==================== 数据读写 ====================
```
## Error Handling
**Custom Exception:**
`D:/code/new_git_code/flutter/ch34/lib/src/ch34_method_channel.dart` defines `Ch34Exception`:
```dart
class Ch34Exception implements Exception {
const Ch34Exception(this.message);
final String message;
@override
String toString() => 'Ch34Exception: $message';
}
```
**Usage Patterns:**
- Platform interface methods document `@throws Ch34Exception` in JSDoc comments for methods that may fail
- Actual throwing is limited: only GPIO methods throw `Ch34Exception` when native returns null
- Most boolean methods return `false` on failure rather than throwing
- Example app uses generic `catch (e)` for error display in UI:
```dart
} catch (e) {
setState(() {
_status = '扫描失败: $e';
});
}
```
- `StateError` thrown when platform instance not initialized:
```dart
throw StateError('Ch34Platform.instance has not been initialized...');
```
**Stream Error Handling:**
EventChannel streams use `onError` callback with `debugPrint`:
```dart
onError: (error) {
debugPrint('CH34 data callback error: $error');
}
```
## Logging
**Framework:** Flutter's `debugPrint` from `package:flutter/foundation.dart`
**Patterns:**
- Used only in error callbacks within the method channel implementation
- Log messages are descriptive with context prefix: `'CH34 USB state error: $error'`
- No logging framework beyond `debugPrint`
- Debug mode toggle available via `Ch34Manager.setDebug(bool)` (passes to native side)
## Comments & Documentation
**API Documentation:**
- All public methods have `///` Dart doc comments in Chinese
- Consistent param/return documentation using `@param` and `@return`
- Exception documentation using `@throws`
- Library-level docs at `D:/code/new_git_code/flutter/ch34/lib/ch34.dart` include usage example in code block
**Inline Comments:**
- Chinese section headers for method grouping
- Method-level comments explain purpose, parameters, and return values
**JSDoc/TSDoc Style:**
- Uses `///` (not `/** */`) for doc comments
- Parameter docs: `/// @param deviceName 设备名称。`
- Return docs: `/// @return \`true\` 成功,\`false\` 失败。`
## Module Design
**Exports:**
- Main entry `D:/code/new_git_code/flutter/ch34/lib/ch34.dart` exports all public types:
```dart
export 'src/ch34_manager.dart';
export 'src/types/ch34_types.dart';
export 'src/ch34_platform_interface.dart' show Ch34Platform;
export 'src/ch34_method_channel.dart' show Ch34Exception;
```
- Backward-compatible barrel files at `lib/` root:
- `D:/code/new_git_code/flutter/ch34/lib/ch34_platform_interface.dart`
- `D:/code/new_git_code/flutter/ch34/lib/ch34_method_channel.dart`
**Layer Separation:**
```
lib/ch34.dart -> Public API entry (exports)
lib/src/ch34_manager.dart -> Static facade (public-facing)
lib/src/ch34_platform_interface.dart -> Abstract interface
lib/src/ch34_method_channel.dart -> Concrete implementation
lib/src/types/ch34_types.dart -> Data types and enums
```
---
*Convention analysis: 2026-04-16*

View File

@@ -0,0 +1,95 @@
# External Integrations
**Analysis Date:** 2026-04-16
## Hardware/Device
**WCH CH34X Series USB-to-Serial Chips:**
- **CH340, CH341, CH342, CH343, CH344, CH347** - Standard USB-to-serial converters
- **CH9101, CH9102, CH9103, CH9104** - Newer generation USB-to-serial chips
- **CH9143** - Multi-port USB-to-serial chip
**SDK/Driver:**
- Library: `CH34XUARTDriver.jar` (local JAR, 140KB)
- Location: `android/libs/CH34XUARTDriver.jar`
- Package: `cn.wch.uartlib`
- Manager: `WCHUARTManager.getInstance()` - Singleton pattern
**USB Host Protocol:**
- Android USB Host API (`android.hardware.usb.UsbManager`)
- Feature declaration: `android.hardware.usb.host` (optional, in `android/src/main/AndroidManifest.xml`)
- Permission model: Runtime USB permission via `WCHUARTManager.requestPermission()`
**VID/PID:**
- Default VID/PID managed by WCH driver
- Custom VID/PID supported via `Ch34Manager.addNewHardware(vid, pid)` - maps to `WCHUARTManager.addNewHardware()`
## Communication Channels
**MethodChannel (`'ch34'`):**
- Location: `lib/src/ch34_method_channel.dart` (Dart), `android/src/main/java/com/example/ch34/Ch34Plugin.java` (Java)
- 33 methods mapped: `enumDevice`, `openDevice`, `requestPermission`, `writeData`, `readData`, `setDtr`, `setRts`, `setBreak`, `enableGpio`, `setGpioVal`, `getGpioVal`, etc.
- Error codes: `ENUM_DEVICE_FAILED`, `OPEN_DEVICE_FAILED`, `PERMISSION_DENIED`, `WRITE_DATA_FAILED`, `GPIO_CHECK_FAILED`, etc.
**EventChannel (`'ch34/data'`):**
- Location: `android/src/main/java/com/example/ch34/Ch34DataStreamHandler.java`
- Payload: `byte[]` serial data bytes
- Registered via `manager.registerDataCallback(device, callback)`
**EventChannel (`'ch34/modem'`):**
- Location: `android/src/main/java/com/example/ch34/Ch34ModemStreamHandler.java`
- Payload: Map with `cts`, `dsr`, `ri`, `dcd` boolean values
- Registered via `manager.registerModemStatusCallback(device, IModemStatus)`
**EventChannel (`'ch34/usb_state'`):**
- Location: `android/src/main/java/com/example/ch34/Ch34UsbStateStreamHandler.java`
- Payload: Map with `deviceName` (String) and `connected` (boolean)
- Triggered by Android broadcast receiver (`UsbManager.ACTION_USB_DEVICE_ATTACHED`, `UsbManager.ACTION_USB_DEVICE_DETACHED`)
## Data Sources
**Serial Data:**
- Read: `Ch34Manager.readData(deviceName, serialNumber)` -> `Uint8List` (blocking)
- Stream: `Ch34Manager.registerDataCallback(deviceName, serialNumber, onData)` -> EventChannel push
- Write: `Ch34Manager.writeData(deviceName, serialNumber, data)` -> `int` bytes written
**GPIO Data:**
- Query: `Ch34Manager.queryGpioStatus(deviceName, gpioIndex)` -> `GpioStatus`
- Query all: `Ch34Manager.queryAllGpioStatus(deviceName)` -> `List<GpioStatus>`
- Control: `Ch34Manager.setGpioVal(deviceName, gpioIndex, value)` / `Ch34Manager.getGpioVal(deviceName, gpioIndex)`
**Modem Status:**
- `ModemStatus` class with fields: `cts` (Clear To Send), `dsr` (Data Set Ready), `ri` (Ring Indicator), `dcd` (Data Carrier Detect)
- Registered via `Ch34Manager.registerModemStatusCallback(deviceName, onModemStatus)`
**Error Types:**
- `SerialErrorType.framingError` - Framing error
- `SerialErrorType.parityError` - Parity error
- `SerialErrorType.overrunError` - Overrun error
- `SerialErrorType.breakInterrupt` - Break interrupt
- Query via `Ch34Manager.querySerialErrorCount(deviceName, serialNumber, errorType)`
## Authentication
**USB Device Permission:**
- Android runtime USB permission dialog via `WCHUARTManager.requestPermission(context, device)`
- No API keys, tokens, or credentials required
- Permission flow: `openDevice` -> if no permission, auto-request -> retry after 2000ms delay
## Third-party Services
**None.** This is a pure hardware driver plugin with no external API integrations, cloud services, or network dependencies.
## CI/CD & Deployment
**Hosting:** Not applicable (Flutter plugin package, published to pub.dev)
**CI Pipeline:** Not detected (no `.github/`, `.gitlab-ci.yml`, or `.circleci/` found)
## Webhooks & Callbacks
**Incoming:** None
**Outgoing:** None
---
*Integration audit: 2026-04-16*

104
.planning/codebase/STACK.md Normal file
View File

@@ -0,0 +1,104 @@
# Technology Stack
**Analysis Date:** 2026-04-16
## Languages
**Primary:**
- **Dart** >=3.4.3 - Flutter plugin Dart-side code (`lib/`, `test/`)
- **Java** 8 - Android native plugin implementation (`android/src/main/java/`)
**Secondary:**
- **Kotlin** 1.7.10 - Declared in example app Gradle plugins (`example/android/settings.gradle`) but not actively used in plugin code
## Runtime
**Environment:**
- **Flutter** >=3.3.0
- **Android SDK** - compileSdk 34, minSdk 21
**Package Manager:**
- **pub** (Dart) - Lockfile: `pubspec.lock` present
- **Gradle** 7.3.0 - Android build system
## Frameworks
**Core:**
- **Flutter** >=3.3.0 - Cross-platform plugin framework
- **Flutter Plugin API** - `FlutterPlugin`, `MethodCallHandler`, `MethodChannel`, `EventChannel`
**Testing:**
- **flutter_test** (Flutter SDK) - Unit testing framework
- **JUnit** 4.13.2 - Android native unit testing
- **Mockito** 5.0.0 - Java mocking for Android tests
- **integration_test** (Flutter SDK) - Integration testing
**Build/Dev:**
- **flutter_lints** ^3.0.0 - Lint rules
- **Gradle** 7.3.0 - Android build automation
## Key Dependencies
**Critical:**
- **plugin_platform_interface** ^2.0.2 - Federated plugin platform interface pattern (`Ch34Platform` extends `PlatformInterface`)
- **CH34XUARTDriver** (local JAR, 140KB) - WCH official UART library, located at `android/libs/CH34XUARTDriver.jar`. Provides `WCHUARTManager`, `ChipType2`, `IDataCallback`, `IModemStatus`, `GPIO_Status` etc.
**Infrastructure:**
- **cupertino_icons** ^1.0.6 - Example app only, Material/Cupertino icon support
**Android Native Dependencies (WCH SDK):**
- `cn.wch.uartlib.WCHUARTManager` - Core manager singleton
- `cn.wch.uartlib.chipImpl.type.ChipType2` - Chip type identification
- `cn.wch.uartlib.callback.IDataCallback` - Serial data callback interface
- `cn.wch.uartlib.callback.IModemStatus` - Modem status callback interface
- `cn.wch.uartlib.callback.IUsbStateChange` - USB state change callback
- `cn.wch.uartlib.exception.*` - Exception classes (ChipException, NoPermissionException, UartLibException)
## Configuration
**Environment:**
- `analysis_options.yaml` - Includes `package:flutter_lints/flutter.yaml`
- No `.env` or environment variable configuration required
- Secrets stored in example `local.properties` (flutter.sdk path)
**Build:**
- `pubspec.yaml` - Main plugin manifest, package: `ch34`, version `1.0.0`
- `pubspec.lock` - Dependency lockfile
- `android/build.gradle` - Android library build config (namespace: `com.example.ch34`)
- `android/settings.gradle` - Project name: `ch34`
- `example/android/build.gradle` - Example app root build
- `example/android/app/build.gradle` - Example app module (namespace: `com.example.ch34_example`)
- `example/android/settings.gradle` - Gradle plugins including dev.flutter.flutter-plugin-loader
## Platform Requirements
**Development:**
- Flutter SDK >=3.3.0
- Dart SDK >=3.4.3
- Android SDK compileSdk 34
- Java 8+ (sourceCompatibility/targetCompatibility = JavaVersion.VERSION_1_8)
**Production:**
- Android device with USB Host support (`android.hardware.usb.host` feature declared in `android/src/main/AndroidManifest.xml`)
- Android minSdk 21 (Android 5.0 Lollipop)
- Physical CH34X series USB-to-serial hardware connected via USB OTG
## Supported Chip Models
CH340, CH341, CH342, CH343, CH344, CH347, CH9101, CH9102, CH9103, CH9104, CH9143
## Build System
**Flutter Plugin:** Standard Flutter plugin structure with Android platform implementation.
**Communication Pattern:**
- `MethodChannel('ch34')` - 33 method calls for device control
- `EventChannel('ch34/data')` - Serial data streaming
- `EventChannel('ch34/modem')` - Modem status streaming
- `EventChannel('ch34/usb_state')` - USB plug/unplug events
**Architecture Pattern:** Federated plugin with platform interface (`Ch34Platform`), method channel implementation (`MethodChannelCh34`), and manager facade (`Ch34Manager`).
---
*Stack analysis: 2026-04-16*

View File

@@ -0,0 +1,217 @@
# Codebase Structure
**Analysis Date:** 2026-04-16
## Directory Layout
```
ch34/ # Flutter plugin root
├── lib/ # Dart source code
│ ├── ch34.dart # Main library entry point (exports)
│ ├── ch34_platform_interface.dart # Backward compat: re-exports src/
│ ├── ch34_method_channel.dart # Backward compat: re-exports src/
│ └── src/ # Core implementation
│ ├── ch34_manager.dart # Public static API facade
│ ├── ch34_platform_interface.dart # Abstract platform interface
│ ├── ch34_method_channel.dart # MethodChannel implementation
│ └── types/
│ └── ch34_types.dart # All data types and enums
├── android/ # Android native code
│ ├── build.gradle # Android library build config
│ ├── settings.gradle # Gradle settings
│ ├── libs/ # Native dependency JARs
│ │ └── CH34XUARTDriver.jar # WCH UART library
│ └── src/
│ ├── main/java/com/example/ch34/
│ │ ├── Ch34Plugin.java # Main plugin + MethodCallHandler
│ │ ├── Ch34DataStreamHandler.java # Data EventChannel handler
│ │ ├── Ch34ModemStreamHandler.java # Modem EventChannel handler
│ │ ├── Ch34UsbStateStreamHandler.java # USB hotplug handler
│ │ └── Ch34TypeConverter.java # Type conversion utilities
│ └── test/java/com/example/ch34/
│ └── Ch34PluginTest.java # Unit tests
├── example/ # Example Flutter app
│ ├── lib/
│ │ └── main.dart # Example app with device scan/connect/send
│ ├── test/
│ │ └── widget_test.dart # Widget test (stub)
│ └── integration_test/
│ └── plugin_integration_test.dart # Integration test
├── test/ # Plugin unit tests
│ ├── ch34_test.dart # Platform interface + manager tests
│ └── ch34_method_channel_test.dart # MethodChannel mock tests
├── docs/
│ └── CH34X-api_docs.md # WCH API documentation reference
├── pubspec.yaml # Plugin package manifest
├── analysis_options.yaml # Dart linting config
├── CHANGELOG.md # Version history
├── LICENSE # License file
├── README.md # Plugin documentation
└── .gitignore # Git ignore rules
```
## Directory Purposes
### `lib/` - Dart Source
- **Purpose:** All Dart code for the Flutter plugin
- **Key files:**
- `lib/ch34.dart` - Primary consumer import: `import 'package:ch34/ch34.dart';`
- `lib/src/ch34_manager.dart` - Static facade, all public APIs
- `lib/src/ch34_platform_interface.dart` - Abstract interface with 33 methods
- `lib/src/ch34_method_channel.dart` - MethodChannel + EventChannel implementation
- `lib/src/types/ch34_types.dart` - Shared data models and enums
### `android/` - Android Native
- **Purpose:** Java implementation bridging to WCH CH34X UART library
- **Key files:**
- `android/src/main/java/com/example/ch34/Ch34Plugin.java` - Plugin entry point (788 lines)
- `android/build.gradle` - Library configuration (compileSdk 34, minSdk 21, Java 8)
- `android/libs/CH34XUARTDriver.jar` - WCH vendor SDK (binary dependency)
### `example/` - Sample Application
- **Purpose:** Demonstrates plugin usage for development and testing
- **Key files:**
- `example/lib/main.dart` - Full working example: scan devices, open, set params, send/receive data
### `test/` - Plugin Tests
- **Purpose:** Unit tests for the Dart plugin code
- **Contains:** MethodChannel mock tests, platform interface mock implementation
## Key File Locations
**Entry Points:**
- `lib/ch34.dart`: Main library entry, consumer-facing import
- `android/src/main/java/com/example/ch34/Ch34Plugin.java`: Android plugin entry, registered via `pubspec.yaml`
**Configuration:**
- `pubspec.yaml`: Plugin declaration, Android platform registration (`package: com.example.ch34`, `pluginClass: Ch34Plugin`)
- `android/build.gradle`: Android library build configuration
- `analysis_options.yaml`: Dart linting (extends `flutter_lints`)
**Core Logic:**
- `lib/src/ch34_manager.dart`: Static API facade (all consumer methods)
- `lib/src/ch34_platform_interface.dart`: Abstract interface contract
- `lib/src/ch34_method_channel.dart`: Channel communication implementation
- `android/src/main/java/com/example/ch34/Ch34Plugin.java`: Native bridge logic
**Types:**
- `lib/src/types/ch34_types.dart`: All data classes (`UsbDeviceInfo`, `SerialParameter`, `ModemStatus`, `GpioStatus`) and enums (`DataBits`, `StopBits`, `Parity`, `GpioDirection`, `GpioValue`, `SerialErrorType`)
**Testing:**
- `test/ch34_method_channel_test.dart`: Unit tests with mock platform
- `test/ch34_test.dart`: Integration-style tests with real MethodChannel mock
- `example/integration_test/plugin_integration_test.dart`: On-device integration test
## Naming Conventions
**Files:**
- Dart: `snake_case.dart` for all files (e.g., `ch34_manager.dart`, `ch34_types.dart`)
- Java: `PascalCase.java` for all classes (e.g., `Ch34Plugin.java`, `Ch34TypeConverter.java`)
**Dart Classes:**
- Public API class: `Ch34Manager` (static methods, facade pattern)
- Platform interface: `Ch34Platform` (abstract, extends `PlatformInterface`)
- Channel implementation: `MethodChannelCh34` (extends `Ch34Platform`)
- Data types: `UsbDeviceInfo`, `SerialParameter`, `ModemStatus`, `GpioStatus`
- Enums: `DataBits`, `StopBits`, `Parity`, `GpioDirection`, `GpioValue`, `SerialErrorType`
- Exception: `Ch34Exception`
**Java Classes:**
- Main plugin: `Ch34Plugin` (implements `FlutterPlugin`, `MethodCallHandler`)
- Stream handlers: `Ch34DataStreamHandler`, `Ch34ModemStreamHandler`, `Ch34UsbStateStreamHandler`
- Utility: `Ch34TypeConverter` (static methods only, private constructor)
**MethodChannel names:**
- Method channel: `'ch34'`
- Data event channel: `'ch34/data'`
- Modem event channel: `'ch34/modem'`
- USB state event channel: `'ch34/usb_state'`
**Method names:** Match Dart method names exactly (e.g., `enumDevice`, `openDevice`, `setSerialParameter`, `writeData`, `readData`)
## Where to Add New Code
**New Dart API Method:**
1. Add abstract method signature to `lib/src/ch34_platform_interface.dart`
2. Add concrete implementation to `lib/src/ch34_method_channel.dart` (using `methodChannel.invokeMethod()`)
3. Add static wrapper to `lib/src/ch34_manager.dart` (delegating to `Ch34Platform.instance`)
4. Add mock implementation in `test/ch34_method_channel_test.dart` for testing
5. Add native handler in `android/src/main/java/com/example/ch34/Ch34Plugin.java`
**New EventChannel (streaming):**
1. Create new `*StreamHandler.java` in `android/src/main/java/com/example/ch34/`
2. Register in `Ch34Plugin.onAttachedToEngine()`
3. Add `EventChannel` and `StreamSubscription` in `lib/src/ch34_method_channel.dart`
4. Add platform interface methods to `Ch34Platform`
5. Add static wrappers to `Ch34Manager`
**New Data Type:**
- Add to `lib/src/types/ch34_types.dart`
- Add corresponding conversion in `android/src/main/java/com/example/ch34/Ch34TypeConverter.java`
**New Native Utility:**
- Add to `android/src/main/java/com/example/ch34/` as a new Java class or extend existing
## Module Boundaries
### Dart Layer
```
lib/ch34.dart (exports)
|
v
lib/src/ch34_manager.dart (static facade)
|
v
lib/src/ch34_platform_interface.dart (abstract contract)
|
v
lib/src/ch34_method_channel.dart (concrete implementation)
|
+---> lib/src/types/ch34_types.dart (shared types)
```
### Native Layer
```
Ch34Plugin.java (MethodCallHandler + 3 EventChannels)
|
+---> Ch34DataStreamHandler.java (data streaming)
+---> Ch34ModemStreamHandler.java (modem status streaming)
+---> Ch34UsbStateStreamHandler.java (USB hotplug streaming)
+---> Ch34TypeConverter.java (type conversion utilities)
|
v
CH34XUARTDriver.jar (WCH vendor SDK)
```
### Platform Bridge
```
Dart: MethodChannel('ch34') <---> Java: Ch34Plugin (MethodCallHandler)
Dart: EventChannel('ch34/data') <---> Java: Ch34DataStreamHandler
Dart: EventChannel('ch34/modem') <---> Java: Ch34ModemStreamHandler
Dart: EventChannel('ch34/usb_state') <---> Java: Ch34UsbStateStreamHandler
```
## Special Directories
**`android/libs/`:**
- Purpose: Contains binary JAR dependency (`CH34XUARTDriver.jar`)
- Generated: No, committed to repo
- Referenced by: `android/build.gradle` via `flatDir` repository
**`example/`:**
- Purpose: Self-contained Flutter app demonstrating plugin usage
- Generated: No, committed to repo
- Note: The example app package is `ch34_example`
**`docs/`:**
- Purpose: API documentation reference for WCH WCHUARTManager
- Contains: `CH34X-api_docs.md` - comprehensive API documentation in Markdown
**`lib/` root-level files:**
- Purpose: Backward compatibility re-exports
- `lib/ch34_platform_interface.dart` exports `src/ch34_platform_interface.dart`
- `lib/ch34_method_channel.dart` exports `src/ch34_method_channel.dart`
---
*Structure analysis: 2026-04-16*

View File

@@ -0,0 +1,232 @@
# Testing Patterns
**Analysis Date:** 2026-04-16
## Framework
**Runner:**
- `flutter_test` (Flutter SDK built-in)
- `integration_test` (Flutter SDK built-in, for E2E/integration tests)
- No explicit `test` package dependency (uses Flutter's bundled test framework)
**Assertion Library:**
- Built-in `flutter_test` matchers (`expect`, `findsOneWidget`, `isEmpty`, `isInstanceOf`)
**Dependencies:**
- `flutter_test` (sdk: flutter) - Unit and widget testing
- `flutter_lints` (^3.0.0) - Code analysis
- `plugin_platform_interface` (^2.0.2) - Required for mock platform interface mixin
**Run Commands:**
```bash
flutter test # Run all unit tests
flutter test --coverage # Run tests with coverage
flutter test test/ch34_test.dart # Run a specific test file
flutter test integration_test/ # Run integration tests (from example/)
```
## Test Structure
**Test File Organization:**
- Plugin tests in `D:/code/new_git_code/flutter/ch34/test/`:
- `ch34_test.dart` - Unit tests with mock platform
- `ch34_method_channel_test.dart` - MethodChannel-level tests with mock handler
- Example app tests in `D:/code/new_git_code/flutter/ch34/example/test/`:
- `widget_test.dart` - Widget tests for example app
- Integration tests in `D:/code/new_git_code/flutter/ch34/example/integration_test/`:
- `plugin_integration_test.dart` - End-to-end plugin tests on real device
**Naming:**
- Test files: `{source_file}_test.dart` pattern
- Integration test files: `{feature}_integration_test.dart`
**Test Entry Pattern:**
All tests call framework initialization before running:
```dart
// Unit tests
TestWidgetsFlutterBinding.ensureInitialized();
// Integration tests
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
```
**Suite Organization:**
Unit tests follow a consistent pattern:
```dart
// D:/code/new_git_code/flutter/ch34/test/ch34_test.dart
void main() {
MethodChannelCh34.registerDefault();
final Ch34Platform initialPlatform = Ch34Platform.instance;
test('$MethodChannelCh34 is the default instance', () {
expect(initialPlatform, isInstanceOf<MethodChannelCh34>());
});
test('getPlatformVersion', () async {
MockCh34Platform fakePlatform = MockCh34Platform();
Ch34Platform.instance = fakePlatform;
expect(await Ch34Manager.getPlatformVersion(), '42');
});
test('enumDevice returns empty list', () async {
MockCh34Platform fakePlatform = MockCh34Platform();
Ch34Platform.instance = fakePlatform;
final devices = await Ch34Manager.enumDevice();
expect(devices, isEmpty);
});
}
```
## Mocking
**Framework:** Manual mock classes (no mockito or mocktail dependency)
**Platform Mock Pattern:**
`D:/code/new_git_code/flutter/ch34/test/ch34_test.dart` uses `MockPlatformInterfaceMixin` for creating test doubles:
```dart
class MockCh34Platform
with MockPlatformInterfaceMixin
implements Ch34Platform {
@override
Future<String?> getPlatformVersion() => Future.value('42');
@override
Future<List<UsbDeviceInfo>> enumDevice() => Future.value([]);
// ... all 30+ methods must be overridden
}
```
**Mock Usage Pattern:**
Mock instances are swapped into the platform interface before each test:
```dart
MockCh34Platform fakePlatform = MockCh34Platform();
Ch34Platform.instance = fakePlatform;
expect(await Ch34Manager.getPlatformVersion(), '42');
```
**MethodChannel Mock Pattern:**
`D:/code/new_git_code/flutter/ch34/test/ch34_method_channel_test.dart` uses `setMockMethodCallHandler` for low-level channel mocking:
```dart
const MethodChannel channel = MethodChannel('ch34');
setUp(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
channel,
(MethodCall methodCall) async {
return '42'; // Returns same value for all method calls
},
);
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, null);
});
```
**What to Mock:**
- Platform interface layer (`Ch34Platform`) for testing `Ch34Manager` delegation
- `MethodChannel` for testing `MethodChannelCh34` channel mapping
**What NOT to Mock:**
- Native Android code (tested via integration tests on real device)
- EventChannel streams (not currently mocked in any test)
## Coverage
**Requirements:** No coverage target enforced
**Current State:**
- Only 2 unit test assertions beyond boilerplate (`getPlatformVersion`, `enumDevice returns empty list`)
- MethodChannel test has only 1 assertion (`getPlatformVersion`)
- Integration test has 1 assertion (`getPlatformVersion` on real device)
- Most platform methods have zero test coverage (GPIO, serial I/O, modem status, signal control, etc.)
**View Coverage:**
```bash
flutter test --coverage
# Coverage written to coverage/lcov.info
```
## Test Locations
**Unit Tests:**
- `D:/code/new_git_code/flutter/ch34/test/ch34_test.dart` - Ch34Manager + mock platform
- `D:/code/new_git_code/flutter/ch34/test/ch34_method_channel_test.dart` - MethodChannel tests
**Integration Tests:**
- `D:/code/new_git_code/flutter/ch34/example/integration_test/plugin_integration_test.dart` - Real device tests
**Widget Tests:**
- `D:/code/new_git_code/flutter/ch34/example/test/widget_test.dart` - Example app widget test (currently outdated - looks for "Running on:" text that no longer exists)
## Test Types
**Unit Tests:**
- Scope: Test `Ch34Manager` delegation to mocked platform
- Approach: Replace `Ch34Platform.instance` with `MockCh34Platform`
- Current coverage: 2 methods tested out of 30+ API methods
**Integration Tests:**
- Framework: `integration_test` package
- Approach: Run on real Android device with actual USB hardware
- Current coverage: 1 test (`getPlatformVersion`)
**E2E Tests:**
- No dedicated E2E test framework beyond `integration_test`
## Common Patterns
**Async Testing:**
All platform method tests use `async/await`:
```dart
test('enumDevice returns empty list', () async {
MockCh34Platform fakePlatform = MockCh34Platform();
Ch34Platform.instance = fakePlatform;
final devices = await Ch34Manager.enumDevice();
expect(devices, isEmpty);
});
```
**Error Testing:**
Mock throws exceptions to test error paths:
```dart
@override
Future<GpioStatus> queryGpioStatus(String deviceName, int gpioIndex) =>
throw const Ch34Exception('Not supported');
```
**Setup/Teardown Pattern:**
```dart
setUp(() {
// Configure mock method channel handler
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, handler);
});
tearDown(() {
// Clean up mock handler
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, null);
});
```
## CI Testing
**CI Pipeline:** Not detected - no `.github/workflows/`, `.gitlab-ci.yml`, or `azure-pipelines.yml` found.
**Recommendation:** Tests should be run via `flutter analyze && flutter test` in any CI pipeline.
---
*Testing analysis: 2026-04-16*

3
CHANGELOG.md Normal file
View File

@@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

105
CLAUDE.md Normal file
View File

@@ -0,0 +1,105 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
这是一个 Flutter 插件项目,为 WCH CH34X 系列 USB 转串口芯片提供 Flutter 接口支持。支持的芯片型号CH340/CH341/CH342/CH343/CH344/CH347/CH9101/CH9102/CH9103/CH9104/CH9143。
## 常用命令
```bash
# 安装依赖
flutter pub get
# 运行所有测试
flutter test
# 运行单个测试文件
flutter test test/ch34_test.dart
flutter test test/ch34_method_channel_test.dart
# 代码分析lint
flutter analyze
# 格式化代码
dart format .
# 构建示例 APK
cd example && flutter build apk
# 运行示例应用
cd example && flutter run
```
## 代码架构
### 目录结构
```
lib/
├── ch34.dart # 主入口,导出所有公共 API
├── ch34_method_channel.dart # 顶层导出(向后兼容)
├── ch34_platform_interface.dart # 顶层导出(向后兼容)
└── src/
├── ch34_manager.dart # 公共 API 管理器(静态方法门面)
├── ch34_platform_interface.dart # 抽象平台接口定义
├── ch34_method_channel.dart # MethodChannel 实现 + 异常类
└── types/
└── ch34_types.dart # 所有类型和枚举定义
```
### 核心类
- **`Ch34Manager`** - 公共 API 入口,所有方法为静态,委托给 `Ch34Platform.instance`
- **`Ch34Platform`** - 抽象平台接口,定义所有方法签名,继承自 `PlatformInterface`
- **`MethodChannelCh34`** - 平台接口的具体实现,通过 MethodChannel 和 EventChannel 与 Android 原生端通信
- **`Ch34Exception`** - 插件自定义异常类
### 通信通道
| 通道类型 | 名称 | 用途 |
|---------|------|------|
| MethodChannel | `ch34` | 双向方法调用 |
| EventChannel | `ch34/data` | 串口数据推送 |
| EventChannel | `ch34/modem` | Modem 状态变化 |
| EventChannel | `ch34/usb_state` | USB 插拔状态 |
### Android 原生端
```
android/src/main/java/com/example/ch34/
├── Ch34Plugin.java # Flutter 插件主类,注册 MethodChannel/EventChannel
├── Ch34DataStreamHandler.java # 数据 EventChannel 的 StreamHandler
├── Ch34ModemStreamHandler.java # Modem EventChannel 的 StreamHandler
├── Ch34UsbStateStreamHandler.java # USB 状态 EventChannel 的 StreamHandler
└── Ch34TypeConverter.java # Dart/Java 类型转换工具
```
### 数据流
```
Flutter App → Ch34Manager → Ch34Platform.instance → MethodChannelCh34 → MethodChannel → Android (Ch34Plugin)
```
### 类型系统 (`ch34_types.dart`)
- **枚举**: `DataBits`, `StopBits`, `Parity`, `GpioDirection`, `GpioValue`, `SerialErrorType`
- **数据类**: `GpioStatus`, `SerialParameter`, `ChipMasterFrequency`, `ModemStatus`, `UsbDeviceInfo`
- 所有数据类提供 `fromMap`/`toMap` 方法用于平台间序列化
## 测试策略
- `test/ch34_test.dart` - 使用 Mock 平台实例测试 `Ch34Manager` 接口
- `test/ch34_method_channel_test.dart` - 使用 BinaryMessenger 模拟测试 MethodChannel 调用
- 测试遵循 flutter plugin 的标准测试模式
## 示例应用
`example/` 目录包含完整的示例应用,展示设备扫描、打开、数据发送/接收的基本流程。
## 用户约束
- Flutter 项目中禁止使用 `Get.snackbar`,使用 `easyloading` 代替
- 所有方法需添加方法级注释
- 遵循高内聚低耦合设计原则

1
LICENSE Normal file
View File

@@ -0,0 +1 @@
TODO: Add your license here.

1
README.md Normal file
View File

@@ -0,0 +1 @@
# CH34 Flutter

4
analysis_options.yaml Normal file
View File

@@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

9
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx

59
android/build.gradle Normal file
View File

@@ -0,0 +1,59 @@
group = "com.example.ch34"
version = "1.0"
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:7.3.0")
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
flatDir{
dirs project(':ch34').file('libs')
}
}
}
apply plugin: "com.android.library"
android {
if (project.android.hasProperty("namespace")) {
namespace = "com.example.ch34"
}
compileSdk = 34
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
defaultConfig {
minSdk = 21
}
dependencies {
testImplementation("junit:junit:4.13.2")
testImplementation("org.mockito:mockito-core:5.0.0")
implementation(name: 'CH34XUARTDriver',ext: 'jar')
}
testOptions {
unitTests.all {
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen {false}
showStandardStreams = true
}
}
}
}

Binary file not shown.

1
android/settings.gradle Normal file
View File

@@ -0,0 +1 @@
rootProject.name = 'ch34'

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.ch34">
<!-- USB Host 功能声明 -->
<uses-feature android:name="android.hardware.usb.host" android:required="false" />
</manifest>

View File

@@ -0,0 +1,110 @@
package com.example.ch34;
import io.flutter.plugin.common.EventChannel;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.hardware.usb.UsbDevice;
import cn.wch.uartlib.callback.IDataCallback;
import java.util.HashMap;
import java.util.Map;
/**
* 数据 EventChannel StreamHandler。
*
* 负责将 WCH IDataCallback 接收到的数据通过 EventChannel 发送给 Flutter 端。
*/
public class Ch34DataStreamHandler implements EventChannel.StreamHandler {
private static final String TAG = "Ch34DataStream";
private EventChannel.EventSink eventSink;
private final Handler mainHandler = new Handler(Looper.getMainLooper());
/** 设备名 -> WCH 回调实例 */
private final Map<String, IDataCallback> wchCallbacks = new HashMap<>();
/** 数据回调接口,用于桥接 WCH 回调到 EventChannel */
public interface DataBridge {
void onData(int serialNumber, byte[] buffer, int length);
}
/** 设备名 -> 数据桥接回调 */
private final Map<String, DataBridge> dataBridges = new HashMap<>();
@Override
public void onListen(Object arguments, EventChannel.EventSink events) {
this.eventSink = events;
Log.d(TAG, "Data stream listening");
}
@Override
public void onCancel(Object arguments) {
this.eventSink = null;
Log.d(TAG, "Data stream cancelled");
}
/**
* 发送数据到 Flutter 端。
*
* 会在主线程上执行 eventSink.success(),确保线程安全。
*
* @param data 字节数组数据。
*/
public void sendData(byte[] data) {
mainHandler.post(() -> {
EventChannel.EventSink sink = eventSink;
if (sink != null) {
sink.success(data);
}
});
}
/**
* 注册设备的数据回调。
*
* @param device USB 设备。
* @param deviceName 设备名称。
* @param serialNumber 串口号。
* @param bridge 数据桥接回调。
*/
public void registerCallback(UsbDevice device, String deviceName, int serialNumber, DataBridge bridge) {
dataBridges.put(deviceName, bridge);
}
/**
* 获取设备的 WCH 回调实例。
*
* @param deviceName 设备名称。
* @return WCH IDataCallback 实例。
*/
public IDataCallback getWchCallback(String deviceName) {
if (wchCallbacks.containsKey(deviceName)) {
return wchCallbacks.get(deviceName);
}
IDataCallback callback = new IDataCallback() {
@Override
public void onData(int serialNumber, byte[] buffer, int length) {
DataBridge bridge = dataBridges.get(deviceName);
if (bridge != null) {
bridge.onData(serialNumber, buffer, length);
}
}
};
wchCallbacks.put(deviceName, callback);
return callback;
}
/**
* 移除设备的数据回调。
*
* @param deviceName 设备名称。
*/
public void removeCallback(String deviceName) {
wchCallbacks.remove(deviceName);
dataBridges.remove(deviceName);
}
}

View File

@@ -0,0 +1,68 @@
package com.example.ch34;
import io.flutter.plugin.common.EventChannel;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import java.util.Map;
/**
* Modem 状态 EventChannel StreamHandler。
*
* 负责将 Modem 状态变化通过 EventChannel 发送给 Flutter 端。
*/
public class Ch34ModemStreamHandler implements EventChannel.StreamHandler {
private static final String TAG = "Ch34ModemStream";
private EventChannel.EventSink eventSink;
private final Handler mainHandler = new Handler(Looper.getMainLooper());
private boolean isListening = false;
@Override
public void onListen(Object arguments, EventChannel.EventSink events) {
this.eventSink = events;
Log.d(TAG, "Modem stream listening");
}
@Override
public void onCancel(Object arguments) {
this.eventSink = null;
this.isListening = false;
Log.d(TAG, "Modem stream cancelled");
}
/**
* 开始监听 Modem 状态。
*/
public void startListening() {
this.isListening = true;
}
/**
* 停止监听 Modem 状态。
*/
public void stopListening() {
this.isListening = false;
this.eventSink = null;
}
/**
* 发送 Modem 状态到 Flutter 端。
*
* 会在主线程上执行 eventSink.success(),确保线程安全。
*
* @param status 状态 Map包含 cts/dsr/ri/dcd 布尔值。
*/
public void sendStatus(Map<String, Object> status) {
if (!isListening) return;
mainHandler.post(() -> {
EventChannel.EventSink sink = eventSink;
if (sink != null) {
sink.success(status);
}
});
}
}

View File

@@ -0,0 +1,892 @@
package com.example.ch34;
import androidx.annotation.NonNull;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import android.app.Application;
import android.content.Context;
import android.hardware.usb.UsbDevice;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import cn.wch.uartlib.WCHUARTManager;
import cn.wch.uartlib.chip.type.ChipType2;
import cn.wch.uartlib.callback.IDataCallback;
import cn.wch.uartlib.callback.IModemStatus;
import cn.wch.uartlib.exception.ChipException;
import cn.wch.uartlib.exception.NoPermissionException;
import cn.wch.uartlib.exception.UartLibException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* CH34X USB 转串口驱动 Flutter 插件。
*
* 实现 WCH WCHUARTManager 文档 4.34.38 共 36 个公开 APIgetInstance/init 由内部自动调用)
* 的 MethodChannel 映射,并通过 3 个 EventChannel 提供数据回调、Modem 状态和 USB 插拔事件流。
*/
public class Ch34Plugin implements FlutterPlugin, MethodCallHandler {
private static final String TAG = "Ch34Plugin";
private MethodChannel channel;
private Context context;
private EventChannel dataEventChannel;
private EventChannel modemEventChannel;
private EventChannel usbStateEventChannel;
private Ch34DataStreamHandler dataStreamHandler;
private Ch34ModemStreamHandler modemStreamHandler;
private Ch34UsbStateStreamHandler usbStateStreamHandler;
/** 已打开的设备映射deviceName -> UsbDevice */
private final Map<String, UsbDevice> openedDevices = new HashMap<>();
/** 全局 WCHUARTManager 实例 */
private WCHUARTManager manager;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
channel = new MethodChannel(binding.getBinaryMessenger(), "ch34");
channel.setMethodCallHandler(this);
context = binding.getApplicationContext();
dataEventChannel = new EventChannel(binding.getBinaryMessenger(), "ch34/data");
dataStreamHandler = new Ch34DataStreamHandler();
dataEventChannel.setStreamHandler(dataStreamHandler);
modemEventChannel = new EventChannel(binding.getBinaryMessenger(), "ch34/modem");
modemStreamHandler = new Ch34ModemStreamHandler();
modemEventChannel.setStreamHandler(modemStreamHandler);
usbStateEventChannel = new EventChannel(binding.getBinaryMessenger(), "ch34/usb_state");
usbStateStreamHandler = new Ch34UsbStateStreamHandler();
usbStateEventChannel.setStreamHandler(usbStateStreamHandler);
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
try {
switch (call.method) {
case "getPlatformVersion":
result.success("Android " + android.os.Build.VERSION.RELEASE);
break;
case "enumDevice":
enumDevice(result);
break;
case "getChipType":
getChipType(call, result);
break;
case "openDevice":
openDevice(call, result);
break;
case "requestPermission":
requestPermission(call, result);
break;
case "getSerialCount":
getSerialCount(call, result);
break;
case "getSerialBaud":
getSerialBaud(call, result);
break;
case "getChipMasterFrequency":
getChipMasterFrequency(call, result);
break;
case "enableSerial":
enableSerial(call, result);
break;
case "setSerialParameter":
setSerialParameter(call, result);
break;
case "writeData":
writeData(call, result);
break;
case "asyncWriteData":
asyncWriteData(call, result);
break;
case "readData":
readData(call, result);
break;
case "readDataWithTimeout":
readDataWithTimeout(call, result);
break;
case "registerDataCallback":
registerDataCallback(call, result);
break;
case "removeDataCallback":
removeDataCallback(call, result);
break;
case "isConnected":
isConnected(call, result);
break;
case "getConnectedDevices":
getConnectedDevices(result);
break;
case "disconnect":
disconnect(call, result);
break;
case "close":
close(result);
break;
case "isSupportGpio":
isSupportGpio(call, result);
break;
case "queryGpioCount":
queryGpioCount(call, result);
break;
case "queryGpioStatus":
queryGpioStatus(call, result);
break;
case "queryAllGpioStatus":
queryAllGpioStatus(call, result);
break;
case "enableGpio":
enableGpio(call, result);
break;
case "setGpioVal":
setGpioVal(call, result);
break;
case "getGpioVal":
getGpioVal(call, result);
break;
case "setDtr":
setDtr(call, result);
break;
case "setRts":
setRts(call, result);
break;
case "setBreak":
setBreak(call, result);
break;
case "registerModemStatusCallback":
registerModemStatusCallback(call, result);
break;
case "removeModemStatusCallback":
removeModemStatusCallback(call, result);
break;
case "querySerialErrorCount":
querySerialErrorCount(call, result);
break;
case "setReadTimeout":
setReadTimeout(call, result);
break;
case "addNewHardware":
addNewHardware(call, result);
break;
case "setDebug":
setDebug(call, result);
break;
case "isDebugMode":
isDebugMode(result);
break;
default:
result.notImplemented();
break;
}
} catch (Exception e) {
Log.e(TAG, "Unhandled method call: " + call.method, e);
result.error("INTERNAL_ERROR", e.getMessage(), null);
}
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
closeAllDevices();
if (dataEventChannel != null) dataEventChannel.setStreamHandler(null);
if (modemEventChannel != null) modemEventChannel.setStreamHandler(null);
if (usbStateEventChannel != null) usbStateEventChannel.setStreamHandler(null);
}
// ==================== 设备枚举与识别 ====================
private void enumDevice(@NonNull Result result) {
ensureManagerInitialized();
try {
ArrayList<UsbDevice> devices = manager.enumDevice();
List<Map<String, Object>> list = new ArrayList<>();
for (UsbDevice device : devices) {
String deviceName = device.getDeviceName();
int serialCount = -1;
String chipType = null;
try {
serialCount = manager.getSerialCount(device);
ChipType2 type = manager.getChipType(device);
if (type != null) {
chipType = type.getDescription();
}
} catch (Exception ignored) {
}
Map<String, Object> map = new HashMap<>();
map.put("deviceName", deviceName);
map.put("productId", device.getProductId());
map.put("vendorId", device.getVendorId());
map.put("serialCount", serialCount);
map.put("chipType", chipType);
list.add(map);
}
result.success(list);
} catch (Exception e) {
Log.e(TAG, "enumDevice error", e);
result.error("ENUM_DEVICE_FAILED", e.getMessage(), null);
}
}
private void getChipType(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
ChipType2 type = manager.getChipType(device);
if (type != null) {
result.success(type.getDescription());
} else {
result.success(null);
}
} catch (Exception e) {
Log.e(TAG, "getChipType error", e);
result.error("GET_CHIP_TYPE_FAILED", e.getMessage(), null);
}
}
// ==================== 设备打开与权限 ====================
private void openDevice(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
if (openedDevices.containsKey(deviceName)) {
result.success(true);
return;
}
boolean opened = tryOpenDevice(device);
if (!opened) {
manager.requestPermission(context, device);
new Handler(Looper.getMainLooper()).postDelayed(() -> {
try {
boolean retryResult = tryOpenDevice(device);
if (retryResult) {
openedDevices.put(deviceName, device);
usbStateStreamHandler.notifyStateChanged(deviceName, true);
result.success(true);
} else {
result.error("PERMISSION_DENIED", "USB permission not granted", null);
}
} catch (Exception e) {
result.error("OPEN_DEVICE_FAILED", e.getMessage(), null);
}
}, 2000);
return;
}
openedDevices.put(deviceName, device);
usbStateStreamHandler.notifyStateChanged(deviceName, true);
result.success(true);
} catch (Exception e) {
Log.e(TAG, "openDevice error", e);
result.error("OPEN_DEVICE_FAILED", e.getMessage(), null);
}
}
private boolean tryOpenDevice(@NonNull UsbDevice device) {
try {
manager.openDevice(device);
return true;
} catch (NoPermissionException e) {
return false;
} catch (Exception e) {
Log.e(TAG, "tryOpenDevice non-permission error", e);
return true;
}
}
private void requestPermission(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
manager.requestPermission(context, device);
result.success(true);
} catch (Exception e) {
Log.e(TAG, "requestPermission error", e);
result.error("REQUEST_PERMISSION_FAILED", e.getMessage(), null);
}
}
// ==================== 串口信息 ====================
private void getSerialCount(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
result.success(manager.getSerialCount(device));
} catch (Exception e) {
Log.e(TAG, "getSerialCount error", e);
result.error("GET_SERIAL_COUNT_FAILED", e.getMessage(), null);
}
}
private void getSerialBaud(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
int serialNumber = call.argument("serialNumber");
result.success(manager.getSerialBaud(device, serialNumber));
} catch (Exception e) {
Log.e(TAG, "getSerialBaud error", e);
result.error("GET_SERIAL_BAUD_FAILED", e.getMessage(), null);
}
}
private void getChipMasterFrequency(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
cn.wch.uartlib.base.other.ChipMasterFrequency freq =
manager.getChipMasterFrequency(device);
if (freq != null) {
Map<String, Object> map = new HashMap<>();
map.put("frequency", freq.getFrequency());
map.put("switchEnable", freq.isSwitchEnable());
map.put("coStatus", freq.getCoStatus());
result.success(map);
} else {
result.error("GET_CHIP_FREQ_FAILED", "Failed to get chip master frequency", null);
}
} catch (Exception e) {
Log.e(TAG, "getChipMasterFrequency error", e);
result.error("GET_CHIP_FREQ_FAILED", e.getMessage(), null);
}
}
private void enableSerial(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
int serialNumber = call.argument("serialNumber");
boolean enable = call.argument("enable");
result.success(manager.enableSerial(device, serialNumber, enable));
} catch (Exception e) {
Log.e(TAG, "enableSerial error", e);
result.error("ENABLE_SERIAL_FAILED", e.getMessage(), null);
}
}
// ==================== 串口参数设置 ====================
private void setSerialParameter(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
int serialNumber = call.argument("serialNumber");
int baud = call.argument("baud");
int dataBits = call.argument("dataBits");
int stopBits = call.argument("stopBits");
int parity = call.argument("parity");
boolean flow = call.argument("hardwareFlowControl");
boolean success = manager.setSerialParameter(
device, serialNumber, baud, dataBits, stopBits, parity, flow);
result.success(success);
} catch (Exception e) {
Log.e(TAG, "setSerialParameter error", e);
result.error("SET_SERIAL_PARAM_FAILED", e.getMessage(), null);
}
}
// ==================== 数据读写 ====================
private void writeData(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
int serialNumber = call.argument("serialNumber");
byte[] data = call.argument("data");
int length = data != null ? data.length : 0;
int timeout = call.argument("timeout");
int written = manager.syncWriteData(device, serialNumber, data, length, timeout);
result.success(written);
} catch (Exception e) {
Log.e(TAG, "writeData error", e);
result.error("WRITE_DATA_FAILED", e.getMessage(), null);
}
}
private void asyncWriteData(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
int serialNumber = call.argument("serialNumber");
byte[] data = call.argument("data");
manager.asyncWriteData(device, serialNumber, data);
result.success(null);
} catch (Exception e) {
Log.e(TAG, "asyncWriteData error", e);
result.error("ASYNC_WRITE_FAILED", e.getMessage(), null);
}
}
private void readData(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
int serialNumber = call.argument("serialNumber");
byte[] data = manager.readData(device, serialNumber);
result.success(data);
} catch (Exception e) {
Log.e(TAG, "readData error", e);
result.error("READ_DATA_FAILED", e.getMessage(), null);
}
}
private void readDataWithTimeout(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
int serialNumber = call.argument("serialNumber");
int vTime = call.argument("vTime");
int vMin = call.argument("vMin");
byte[] data = manager.readData(device, serialNumber, vTime, vMin);
result.success(data);
} catch (Exception e) {
Log.e(TAG, "readDataWithTimeout error", e);
result.error("READ_DATA_TIMEOUT_FAILED", e.getMessage(), null);
}
}
private void registerDataCallback(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
int serialNumber = call.argument("serialNumber");
dataStreamHandler.registerCallback(device, deviceName, serialNumber, (serial, buffer, length) -> {
byte[] data = new byte[length];
System.arraycopy(buffer, 0, data, 0, length);
dataStreamHandler.sendData(data);
});
manager.registerDataCallback(device, dataStreamHandler.getWchCallback(deviceName));
result.success(null);
} catch (Exception e) {
Log.e(TAG, "registerDataCallback error", e);
result.error("REGISTER_CALLBACK_FAILED", e.getMessage(), null);
}
}
private void removeDataCallback(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
dataStreamHandler.removeCallback(deviceName);
manager.removeDataCallback(device);
result.success(null);
} catch (Exception e) {
Log.e(TAG, "removeDataCallback error", e);
result.error("REMOVE_CALLBACK_FAILED", e.getMessage(), null);
}
}
// ==================== 连接状态 ====================
private void isConnected(@NonNull MethodCall call, @NonNull Result result) {
try {
String deviceName = call.argument("deviceName");
UsbDevice device = openedDevices.get(deviceName);
result.success(device != null);
} catch (Exception e) {
result.success(false);
}
}
private void getConnectedDevices(@NonNull Result result) {
result.success(new ArrayList<>(openedDevices.keySet()));
}
// ==================== 断开与关闭 ====================
private void disconnect(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = openedDevices.get(deviceName);
if (device != null) {
manager.disconnect(device);
openedDevices.remove(deviceName);
usbStateStreamHandler.notifyStateChanged(deviceName, false);
}
result.success(null);
} catch (Exception e) {
Log.e(TAG, "disconnect error", e);
result.error("DISCONNECT_FAILED", e.getMessage(), null);
}
}
private void close(@NonNull Result result) {
ensureManagerInitialized();
try {
closeAllDevices();
if (context instanceof Application) {
manager.close((Application) context);
}
result.success(null);
} catch (Exception e) {
Log.e(TAG, "close error", e);
result.error("CLOSE_FAILED", e.getMessage(), null);
}
}
private void closeAllDevices() {
ensureManagerInitialized();
for (Map.Entry<String, UsbDevice> entry : openedDevices.entrySet()) {
try {
manager.disconnect(entry.getValue());
usbStateStreamHandler.notifyStateChanged(entry.getKey(), false);
} catch (Exception e) {
Log.e(TAG, "disconnect error during closeAll", e);
}
}
openedDevices.clear();
}
// ==================== GPIO 功能 ====================
private void isSupportGpio(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
result.success(manager.isSupportGPIOFeature(device));
} catch (Exception e) {
Log.e(TAG, "isSupportGpio error", e);
result.error("GPIO_CHECK_FAILED", e.getMessage(), null);
}
}
private void queryGpioCount(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
result.success(manager.queryGPIOCount(device));
} catch (Exception e) {
Log.e(TAG, "queryGpioCount error", e);
result.error("QUERY_GPIO_COUNT_FAILED", e.getMessage(), null);
}
}
private void queryGpioStatus(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
int gpioIndex = call.argument("gpioIndex");
cn.wch.uartlib.base.gpio.GPIO_Status status = manager.queryGPIOStatus(device, gpioIndex);
result.success(Ch34TypeConverter.gpioStatusToMap(status, gpioIndex));
} catch (Exception e) {
Log.e(TAG, "queryGpioStatus error", e);
result.error("QUERY_GPIO_STATUS_FAILED", e.getMessage(), null);
}
}
private void queryAllGpioStatus(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
List<cn.wch.uartlib.base.gpio.GPIO_Status> statuses = manager.queryAllGPIOStatus(device);
List<Map<String, Object>> list = new ArrayList<>();
for (int i = 0; i < statuses.size(); i++) {
list.add(Ch34TypeConverter.gpioStatusToMap(statuses.get(i), i));
}
result.success(list);
} catch (Exception e) {
Log.e(TAG, "queryAllGpioStatus error", e);
result.error("QUERY_ALL_GPIO_FAILED", e.getMessage(), null);
}
}
private void enableGpio(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
int gpioIndex = call.argument("gpioIndex");
boolean enable = call.argument("enable");
int directionIndex = call.argument("direction");
cn.wch.uartlib.base.gpio.GPIO_DIR dir = Ch34TypeConverter.toGpioDirection(directionIndex);
boolean success = manager.enableGPIO(device, gpioIndex, enable, dir);
result.success(success);
} catch (Exception e) {
Log.e(TAG, "enableGpio error", e);
result.error("ENABLE_GPIO_FAILED", e.getMessage(), null);
}
}
private void setGpioVal(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
int gpioIndex = call.argument("gpioIndex");
int valueIndex = call.argument("value");
cn.wch.uartlib.base.gpio.GPIO_VALUE value = Ch34TypeConverter.toGpioValue(valueIndex);
boolean success = manager.setGPIOVal(device, gpioIndex, value);
result.success(success);
} catch (Exception e) {
Log.e(TAG, "setGpioVal error", e);
result.error("SET_GPIO_VAL_FAILED", e.getMessage(), null);
}
}
private void getGpioVal(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
int gpioIndex = call.argument("gpioIndex");
cn.wch.uartlib.base.gpio.GPIO_VALUE value = manager.getGPIOVal(device, gpioIndex);
result.success(value == cn.wch.uartlib.base.gpio.GPIO_VALUE.HIGH ? 1 : 0);
} catch (Exception e) {
Log.e(TAG, "getGpioVal error", e);
result.error("GET_GPIO_VAL_FAILED", e.getMessage(), null);
}
}
// ==================== 信号控制 ====================
private void setDtr(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
int serialNumber = call.argument("serialNumber");
boolean valid = call.argument("valid");
boolean success = manager.setDTR(device, serialNumber, valid);
result.success(success);
} catch (Exception e) {
Log.e(TAG, "setDtr error", e);
result.error("SET_DTR_FAILED", e.getMessage(), null);
}
}
private void setRts(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
int serialNumber = call.argument("serialNumber");
boolean valid = call.argument("valid");
boolean success = manager.setRTS(device, serialNumber, valid);
result.success(success);
} catch (Exception e) {
Log.e(TAG, "setRts error", e);
result.error("SET_RTS_FAILED", e.getMessage(), null);
}
}
private void setBreak(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
int serialNumber = call.argument("serialNumber");
boolean valid = call.argument("valid");
boolean success = manager.setBreak(device, serialNumber, valid);
result.success(success);
} catch (Exception e) {
Log.e(TAG, "setBreak error", e);
result.error("SET_BREAK_FAILED", e.getMessage(), null);
}
}
// ==================== Modem 状态回调 ====================
private void registerModemStatusCallback(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
modemStreamHandler.startListening();
manager.registerModemStatusCallback(device, new IModemStatus() {
@Override
public void onStatusChanged(int serialNumber, boolean isDCDRaised,
boolean isDSRRaised, boolean isCTSRaised, boolean isRINGRaised) {
Log.d(TAG, "Modem onStatusChanged: serial=" + serialNumber
+ " dcd=" + isDCDRaised + " dsr=" + isDSRRaised
+ " cts=" + isCTSRaised + " ri=" + isRINGRaised);
Map<String, Object> map = new HashMap<>();
map.put("dcd", isDCDRaised);
map.put("dsr", isDSRRaised);
map.put("cts", isCTSRaised);
map.put("ri", isRINGRaised);
Log.d(TAG, "Sending modem status to Flutter: " + map);
modemStreamHandler.sendStatus(map);
}
@Override
public void onOverrunError(int serialNumber) {
}
@Override
public void onParityError(int serialNumber) {
}
@Override
public void onFrameError(int serialNumber) {
}
});
result.success(null);
} catch (Exception e) {
Log.e(TAG, "registerModemStatusCallback error", e);
result.error("REGISTER_MODEM_CALLBACK_FAILED", e.getMessage(), null);
}
}
private void removeModemStatusCallback(@NonNull MethodCall call, @NonNull Result result) {
try {
modemStreamHandler.stopListening();
result.success(null);
} catch (Exception e) {
Log.e(TAG, "removeModemStatusCallback error", e);
result.error("REMOVE_MODEM_CALLBACK_FAILED", e.getMessage(), null);
}
}
// ==================== 错误查询 ====================
private void querySerialErrorCount(@NonNull MethodCall call, @NonNull Result result) {
ensureManagerInitialized();
try {
String deviceName = call.argument("deviceName");
UsbDevice device = getDeviceOrThrow(deviceName);
int serialNumber = call.argument("serialNumber");
String errorType = call.argument("errorType");
cn.wch.uartlib.base.error.SerialErrorType nativeType =
Ch34TypeConverter.toSerialErrorType(errorType);
int count = manager.querySerialErrorCount(device, serialNumber, nativeType);
result.success(count);
} catch (Exception e) {
Log.e(TAG, "querySerialErrorCount error", e);
result.error("QUERY_ERROR_COUNT_FAILED", e.getMessage(), null);
}
}
// ==================== 全局配置 ====================
private void setReadTimeout(@NonNull MethodCall call, @NonNull Result result) {
try {
int timeout = call.argument("timeout");
WCHUARTManager.setReadTimeout(timeout);
result.success(null);
} catch (Exception e) {
Log.e(TAG, "setReadTimeout error", e);
result.error("SET_TIMEOUT_FAILED", e.getMessage(), null);
}
}
private void addNewHardware(@NonNull MethodCall call, @NonNull Result result) {
try {
int vid = call.argument("vid");
int pid = call.argument("pid");
String chipType = call.argument("chipType");
cn.wch.uartlib.chip.type.ChipType2 type =
Ch34TypeConverter.toChipType(chipType);
WCHUARTManager.addNewHardwareAndChipType(vid, pid, type);
result.success(null);
} catch (Exception e) {
Log.e(TAG, "addNewHardware error", e);
result.error("ADD_HARDWARE_FAILED", e.getMessage(), null);
}
}
private void setDebug(@NonNull MethodCall call, @NonNull Result result) {
try {
boolean enabled = call.argument("enabled");
WCHUARTManager.setDebug(enabled);
result.success(null);
} catch (Exception e) {
Log.e(TAG, "setDebug error", e);
result.error("SET_DEBUG_FAILED", e.getMessage(), null);
}
}
private void isDebugMode(@NonNull Result result) {
result.success(WCHUARTManager.isDebugMode());
}
// ==================== 辅助方法 ====================
private void ensureManagerInitialized() {
if (manager == null) {
Application app;
if (context instanceof Application) {
app = (Application) context;
} else {
app = (Application) context.getApplicationContext();
}
manager = WCHUARTManager.getInstance();
manager.init(app);
usbStateStreamHandler.setManager(manager);
usbStateStreamHandler.setContext(context);
}
}
private UsbDevice getDeviceOrThrow(String deviceName) {
UsbDevice device = openedDevices.get(deviceName);
if (device == null) {
try {
ArrayList<UsbDevice> devices = manager.enumDevice();
for (UsbDevice d : devices) {
if (d.getDeviceName().equals(deviceName)) {
return d;
}
}
} catch (Exception ignored) {
}
throw new IllegalStateException("Device not found: " + deviceName);
}
return device;
}
}

View File

@@ -0,0 +1,134 @@
package com.example.ch34;
import java.util.HashMap;
import java.util.Map;
/**
* 类型转换工具类。
*
* 在 Dart 枚举值和 Java/WCH 类型之间进行转换。
*/
public class Ch34TypeConverter {
private Ch34TypeConverter() {
}
/**
* 将 GPIO 状态对象转换为 Map。
*
* @param status WCH GPIO_Status 对象。
* @param gpioIndex GPIO 编号。
* @return 包含 index/direction/value/enabled 的 Map。
*/
public static Map<String, Object> gpioStatusToMap(
cn.wch.uartlib.base.gpio.GPIO_Status status, int gpioIndex) {
Map<String, Object> map = new HashMap<>();
if (status != null) {
map.put("index", gpioIndex);
map.put("enabled", status.isEnabled());
map.put("direction", status.getDir() == cn.wch.uartlib.base.gpio.GPIO_DIR.OUT ? 1 : 0);
map.put("value", status.getValue() == cn.wch.uartlib.base.gpio.GPIO_VALUE.HIGH ? 1 : 0);
} else {
map.put("index", gpioIndex);
map.put("enabled", false);
map.put("direction", 0);
map.put("value", 0);
}
return map;
}
/**
* 将 Dart 端的方向索引转换为 WCH GPIO_DIR 枚举。
*
* @param index 0 = IN, 1 = OUT。
* @return WCH GPIO_DIR 枚举。
*/
public static cn.wch.uartlib.base.gpio.GPIO_DIR toGpioDirection(int index) {
if (index == 1) {
return cn.wch.uartlib.base.gpio.GPIO_DIR.OUT;
}
return cn.wch.uartlib.base.gpio.GPIO_DIR.IN;
}
/**
* 将 Dart 端的值索引转换为 WCH GPIO_VALUE 枚举。
*
* @param index 0 = LOW, 1 = HIGH。
* @return WCH GPIO_VALUE 枚举。
*/
public static cn.wch.uartlib.base.gpio.GPIO_VALUE toGpioValue(int index) {
if (index == 1) {
return cn.wch.uartlib.base.gpio.GPIO_VALUE.HIGH;
}
return cn.wch.uartlib.base.gpio.GPIO_VALUE.LOW;
}
/**
* 将芯片类型字符串转换为 WCH ChipType2 枚举。
*
* @param chipType 芯片类型描述字符串(如 "CH340"、"CH9102")。
* @return WCH ChipType2 枚举。
* @throws IllegalArgumentException 当 chipType 为 null 或未知类型时抛出。
*/
public static cn.wch.uartlib.chip.type.ChipType2 toChipType(String chipType) {
if (chipType == null) {
throw new IllegalArgumentException("chipType is null");
}
switch (chipType) {
case "CH340":
case "CH341":
return cn.wch.uartlib.chip.type.ChipType2.CHIP_CH341;
case "CH342":
return cn.wch.uartlib.chip.type.ChipType2.CHIP_CH342F;
case "CH343":
return cn.wch.uartlib.chip.type.ChipType2.CHIP_CH343GP;
case "CH344":
return cn.wch.uartlib.chip.type.ChipType2.CHIP_CH344L;
case "CH347":
return cn.wch.uartlib.chip.type.ChipType2.CHIP_CH347TF;
case "CH9101":
return cn.wch.uartlib.chip.type.ChipType2.CHIP_CH9101U;
case "CH9102":
return cn.wch.uartlib.chip.type.ChipType2.CHIP_CH9102F;
case "CH9103":
return cn.wch.uartlib.chip.type.ChipType2.CHIP_CH9103M;
case "CH9104":
return cn.wch.uartlib.chip.type.ChipType2.CHIP_CH9104L;
case "CH9143":
return cn.wch.uartlib.chip.type.ChipType2.CHIP_CH9143;
case "CH9111":
return cn.wch.uartlib.chip.type.ChipType2.CHIP_CH9111L_MODE0;
case "CH9114":
return cn.wch.uartlib.chip.type.ChipType2.CHIP_CH9114L;
case "CH339":
return cn.wch.uartlib.chip.type.ChipType2.CHIP_CH339W;
default:
throw new IllegalArgumentException("Unknown chipType: " + chipType);
}
}
/**
* 将 Dart 端的错误类型字符串转换为 WCH SerialErrorType 枚举。
*
* WCH 原生库仅支持 FRAME/PARITY/OVERRUN 三种错误类型。
*
* @param errorType 错误类型字符串。
* @return WCH SerialErrorType 枚举。
* @throws IllegalArgumentException 传入的错误类型字符串无法识别时抛出。
*/
public static cn.wch.uartlib.base.error.SerialErrorType toSerialErrorType(String errorType) {
if (errorType == null) {
throw new IllegalArgumentException("errorType is null");
}
switch (errorType) {
case "SerialErrorType.FramingError":
return cn.wch.uartlib.base.error.SerialErrorType.FRAME;
case "SerialErrorType.ParityError":
return cn.wch.uartlib.base.error.SerialErrorType.PARITY;
case "SerialErrorType.OverrunError":
return cn.wch.uartlib.base.error.SerialErrorType.OVERRUN;
default:
throw new IllegalArgumentException("Unknown SerialErrorType: " + errorType);
}
}
}

View File

@@ -0,0 +1,114 @@
package com.example.ch34;
import io.flutter.plugin.common.EventChannel;
import android.content.Context;
import android.hardware.usb.UsbDevice;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import cn.wch.uartlib.WCHUARTManager;
import cn.wch.uartlib.callback.IUsbStateChange;
import java.util.HashMap;
import java.util.Map;
/**
* USB 插拔状态 EventChannel StreamHandler。
*
* 基于 WCH 库的 {@link IUsbStateChange} 回调接收设备插入/拔出/权限变更事件,
* 并通过 EventChannel 通知 Flutter 端。
*/
public class Ch34UsbStateStreamHandler implements EventChannel.StreamHandler {
private static final String TAG = "Ch34UsbStateStream";
private EventChannel.EventSink eventSink;
private Context context;
private WCHUARTManager manager;
private final Handler mainHandler = new Handler(Looper.getMainLooper());
/** WCH 库 USB 状态回调,监听设备插拔与权限变化 */
private final IUsbStateChange wchUsbStateListener = new IUsbStateChange() {
@Override
public void usbDeviceAttach(UsbDevice device) {
if (device != null) {
notifyStateChanged(device.getDeviceName(), true);
}
}
@Override
public void usbDeviceDetach(UsbDevice device) {
if (device != null) {
notifyStateChanged(device.getDeviceName(), false);
}
}
@Override
public void usbDevicePermission(UsbDevice device, boolean granted) {
Log.d(TAG, "USB permission changed: "
+ (device != null ? device.getDeviceName() : "null")
+ ", granted=" + granted);
}
};
/**
* 设置 WCH 管理器。
*
* 在管理器可用后自动注册 USB 状态监听器。
*
* @param manager WCHUARTManager 实例。
*/
public void setManager(WCHUARTManager manager) {
this.manager = manager;
if (manager != null) {
try {
manager.setUsbStateListener(wchUsbStateListener);
Log.d(TAG, "WCH USB state listener registered");
} catch (Exception e) {
Log.e(TAG, "Failed to register WCH USB state listener", e);
}
}
}
/**
* 设置上下文。
*
* @param context Android Context。
*/
public void setContext(Context context) {
this.context = context;
}
@Override
public void onListen(Object arguments, EventChannel.EventSink events) {
this.eventSink = events;
Log.d(TAG, "USB state stream listening");
}
@Override
public void onCancel(Object arguments) {
this.eventSink = null;
Log.d(TAG, "USB state stream cancelled");
}
/**
* 通知设备状态变化。
*
* 会在主线程上执行 eventSink.success(),确保线程安全。
*
* @param deviceName 设备名称。
* @param connected `true` 已连接,`false` 已断开。
*/
public void notifyStateChanged(String deviceName, boolean connected) {
mainHandler.post(() -> {
EventChannel.EventSink sink = eventSink;
if (sink != null) {
Map<String, Object> map = new HashMap<>();
map.put("deviceName", deviceName);
map.put("connected", connected);
sink.success(map);
}
});
}
}

View File

@@ -0,0 +1,29 @@
package com.example.ch34;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import org.junit.Test;
/**
* This demonstrates a simple unit test of the Java portion of this plugin's implementation.
*
* Once you have built the plugin's example app, you can run these tests from the command
* line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or
* you can run them directly from IDEs that support JUnit such as Android Studio.
*/
public class Ch34PluginTest {
@Test
public void onMethodCall_getPlatformVersion_returnsExpectedValue() {
Ch34Plugin plugin = new Ch34Plugin();
final MethodCall call = new MethodCall("getPlatformVersion", null);
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
plugin.onMethodCall(call, mockResult);
verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE);
}
}

703
docs/CH34X-api_docs.md Normal file
View File

@@ -0,0 +1,703 @@
# CH34X 系列芯片串口 Android 程序开发说明
**版本1.8**
https://wch.cn
---
## 一、简介
本文档适用于 CH339 / CH340 / CH341 / CH342 / CH343 / CH344 / CH347 / CH9101 / CH9102 / CH9103 / CH9104 / CH9143 / CH9111 / CH9114 的 USB 转串口芯片开发说明。
本文档其余部分均以 CH340 系列 USB 转异步串口功能(以下简称 CH34XUART以及 GPIO 功能,以及 Android 4.4 和更新版本的系统进行说明。在功能基于 USB Host 实现连接,用户需调用相应的 API 实现与 Android 设备进行数据交互。
Android Host 和 USB Device 的通讯关系参考图:
```
USB HOST USB Device
Android (USB HOST/OTG) ─── USB 通讯 ─── 转接芯片 ─── UART ─── PC/MCU/串口设备
```
CH34X 串口转接芯片 Android 接口需要 Android 4.4 及以上版本系统,使用 CH34X 需要 Android 满足条件:
1. 基于 Android 4.4 及以上版本系统
2. Android 设备具备 USB Host或 OTG接口
本文档将着重介绍调用 Android USB Host 与 Device 通讯相关 API 以及相应的操作说明。关于 Android USB Host 协议说明,可以参考 Google 官方文档。
---
## 二、Android Host
本文档所指的程序是基于 Android 4.4 及以上版本系统下编写的。Android 应用程序的动画参考文章 Android Filter.xml 文件的 product id 和 vendor id基于 CH34X UART 开发的 Android 应用程序主要架构和步骤,如下图:
```
┌──────────────────────────────┐
│ Android Host │
│ ┌──────────────────────┐ │
│ │ User Layout │ │
│ └──────────────────────┘ │
│ │
│ ┌──────────────────────┐ │
│ │ CH34XUART │ │
│ │ Applications │ │
│ └──────────────────────┘ │
│ ↕ │
│ ┌──────────────────────┐ │
│ │ CH34XDriver.jar (lib)│ │
│ └──────────────────────┘ │
└──────────────────────────────┘
```
---
## 三、软件操作说明
用户需要在支持 USB Host 功能的 Android 设备上安装测试程序CH34XUARTDemo.apk点击打开测试程序测试程序显示当前 USB 设备的状态。点击"打开"后,如果未连接相应的 USB 芯片的设备,则会显示设备未连接状态;点击"断开",则会显示设备未连接。在连接条件后,按钮显示"已连接"。用户通过串口调试功能,发送数据可以接收返回的数据。
---
## 四、函数接口说明
### 4.1 getInstance
```java
public static WCHUARTManager getInstance()
```
用于获取全局唯一实例。
| 项目 | 说明 |
|------|------|
| 返回 | 返回全局唯一实例 |
---
### 4.2 init
```java
public void init(android.app.Application application)
```
初始化上下文,注册广播接收设备状态变化。
| 项目 | 说明 |
|------|------|
| 参数 | application - 全局上下文 |
---
### 4.3 enumDevice
```java
public java.util.ArrayList<android.hardware.usb.UsbDevice> enumDevice()
throws java.lang.Exception
```
枚举当前所有符合要求的 USB 设备。
| 项目 | 说明 |
|------|------|
| 返回 | android.hardware.usb.UsbDevice 设备列表 |
| 抛出 | java.lang.Exception |
---
### 4.4 getChipType
```java
public cn.wch.uartlib.chipImpl.type.ChipType2 getChipType(@NonNull
android.hardware.usb.UsbDevice usbDevice)
```
获取 usbDevice 的芯片类型。
| 项目 | 说明 |
|------|------|
| 参数 | usbDevice - USB 设备 |
| 返回 | 芯片类型usbDevice 是 WCH 芯片);否则表示无法识别 USB 设备的芯片类型 |
---
### 4.5 openDevice
```java
public boolean openDevice(@NonNull
android.hardware.usb.UsbDevice usbDevice)
throws cn.wch.uartlib.exception.UartLibException,
cn.wch.uartlib.exception.NoPermissionException,
cn.wch.uartlib.exception.ChipException
```
打开 USB 设备。
| 项目 | 说明 |
|------|------|
| 参数 | usbDevice - USB 设备 |
| 返回 | true 成功false 失败 |
| 抛出 | cn.wch.uartlib.exception.UartLibException<br>cn.wch.uartlib.exception.NoPermissionException<br>cn.wch.uartlib.exception.ChipException |
---
### 4.6 requestPermission
```java
public void requestPermission(@NonNull
android.content.Context context,
@NonNull android.hardware.usb.UsbDevice usbDevice)
throws cn.wch.uartlib.exception.UartLibException
```
请求 USB 设备权限。
| 项目 | 说明 |
|------|------|
| 参数 | context - 上下文<br>usbDevice - USB 设备 |
| 抛出 | cn.wch.uartlib.exception.UartLibException |
---
### 4.7 setUsbStateListener
```java
public void setUsbStateListener(@NonNull
cn.wch.uartlib.callback.IUsbStateChange usbStateListener)
```
监听设备的状态变化。
| 项目 | 说明 |
|------|------|
| 参数 | usbStateListener - 设备状态监听回调 |
---
### 4.8 getSerialCount
```java
public int getSerialCount(@NonNull
android.hardware.usb.UsbDevice usbDevice)
```
获取设备串口数量。
| 项目 | 说明 |
|------|------|
| 参数 | usbDevice - USB 设备 |
| 返回 | 返回串口数量。如果为负数,说明获取芯片类型失败 |
---
### 4.9 enableSerial
```java
public boolean enableSerial(@NonNull UsbDevice usbDevice,
int serialNumber, boolean enable) throws Exception
```
打开/关闭串口(实际仅对 CH9114 系列有效,其他类型设备无影响)。
| 项目 | 说明 |
|------|------|
| 参数 | usbDevice - USB 设备<br>serialNumber - 串口号<br>enable - false 关闭true 打开 |
| 返回 | true 设置成功false 设置失败 |
| 抛出 | java.lang.Exception |
---
### 4.10 setSerialParameter
```java
public boolean setSerialParameter(@NonNull
android.hardware.usb.UsbDevice usbDevice,
int serialNumber,
int baud,
int dataBit,
int stopBit,
int parityBit,
boolean flow)
throws java.lang.Exception
```
设置串口参数。
| 项目 | 说明 |
|------|------|
| 参数 | usbDevice - USB 设备<br>serialNumber - 串口号<br>baud - 波特率<br>dataBit - 数据位 5,6,7,8<br>stopBit - 停止位 1,2<br>parityBit - 校验位 0 NONE; 1 ODD; 2 EVEN; 3 MARK; 4 SPACE<br>flow - true 打开流控false 关闭 |
| 返回 | true 设置成功false 设置失败 |
| 抛出 | java.lang.Exception |
---
### 4.11 getSerialBaud
```java
public int getSerialBaud(@NonNull UsbDevice usbDevice, int serialNumber)
throws Exception
```
获取串口波特率(实际仅对 CH9114 系列有效,其他类型设备无影响)。
| 项目 | 说明 |
|------|------|
| 参数 | usbDevice - USB 设备<br>serialNumber - 串口号 |
| 返回 | 大于 0串口波特率小于 0出错 |
| 抛出 | java.lang.Exception |
---
### 4.12 getChipMasterFrequency
```java
public ChipMasterFrequency getChipMasterFrequency(@NonNull UsbDevice
usbDevice) throws Exception
```
获取芯片主频(实际仅对 CH9114 系列有效,其他类型设备无影响)。
| 项目 | 说明 |
|------|------|
| 参数 | usbDevice - USB 设备 |
| 返回 | ChipMasterFrequency 对象:<br>- int frequency 频率<br>- Boolean switchEnable 是否允许切换<br>- int CoStatus 当前状态 |
| 抛出 | java.lang.Exception |
---
### 4.13 syncWriteData
```java
public int syncWriteData(@NonNull UsbDevice usbDevice, int serialNumber,
byte[] data, int length, int timeout) throws Exception
```
发送串口数据(同步发送)。
| 项目 | 说明 |
|------|------|
| 参数 | usbDevice - USB 设备<br>serialNumber - 串口号<br>data - 将要发送的数据<br>length - 将要发送数据的长度<br>timeout - 超时时间 |
| 返回 | 发送成功返回数据的长度 |
| 抛出 | java.lang.Exception |
---
### 4.14 asyncWriteData
```java
public void asyncWriteData(@NonNull UsbDevice usbDevice, int serialNumber,
byte[] data) throws Exception
```
发送串口数据(异步发送,将数据加入缓存队列发送,不能返回状态和结果)。
| 项目 | 说明 |
|------|------|
| 参数 | usbDevice - USB 设备<br>serialNumber - 串口号<br>data - 将要发送的数据 |
| 抛出 | java.lang.Exception |
---
### 4.15 readData
```java
public byte[] readData(@NonNull
android.hardware.usb.UsbDevice usbDevice,
int serialNumber)
throws java.lang.Exception
```
读取串口数据。
| 项目 | 说明 |
|------|------|
| 参数 | usbDevice - USB 设备<br>serialNumber - 串口号 |
| 返回 | 读取到的数据 |
| 抛出 | cn.wch.uartlib.exception.ChipException |
---
### 4.16 readData重载方法
```java
public byte[] readData(@NonNull
android.hardware.usb.UsbDevice usbDevice,
int serialNumber,
int vTime,
int vMin)
throws java.lang.Exception
```
读取串口数据。
**说明:**
1. 当 vTime>0vMin>0 时read 函数将阻塞直到读到最后一个字符时开始计时,超过 vTime 时间后,如果接收到的数据不足 vMin 字节,则返回。否则继续等待直到收到 vMin 个字节或超时。
2. 当 vTime>0vMin=0 时read 函数立即返回,超时为每个字符等待 vTime 时间。
3. 当 vTime=0vMin>0 时read 函数一直阻塞,直到读到 vMin 个字节后返回。
| 项目 | 说明 |
|------|------|
| 参数 | usbDevice - USB 设备<br>serialNumber - 串口号<br>vTime - 等待时间<br>vMin - 最小读取字节数 |
| 返回 | 读取到的数据 |
| 抛出 | cn.wch.uartlib.exception.ChipException |
---
### 4.17 registerDataCallback
```java
public void registerDataCallback (@NonNull
android.hardware.usb.UsbDevice usbDevice,
cn.wch.uartlib.callback.IDataCallback dataCallback)
throws java.lang.Exception
```
注册串口数据回调(此方法可替代 readData 函数,用户通过回调方式接收数据)。注意:如果调用了 registerDataCallback(device,null),等同于 removeDataCallback(device)。
| 项目 | 说明 |
|------|------|
| 参数 | usbDevice - USB 设备<br>dataCallback - 数据回调 |
| 抛出 | java.lang.Exception |
---
### 4.18 removeDataCallback
```java
public void removeDataCallback (@NonNull
android.hardware.usb.UsbDevice usbDevice)
```
移除串口数据回调。
| 项目 | 说明 |
|------|------|
| 参数 | usbDevice - USB 设备 |
---
### 4.19 isConnected
```java
public boolean isConnected(@NonNull
android.hardware.usb.UsbDevice usbDevice)
```
判断 USB 设备是否已经连接。
| 项目 | 说明 |
|------|------|
| 参数 | usbDevice - USB 设备 |
| 返回 | true 已经连接false 没有打开 |
---
### 4.20 getConnectedDevices
```java
public java.util.ArrayList<android.hardware.usb.UsbDevice> getConnectedDevices()
```
获取当前已连接的设备。
| 项目 | 说明 |
|------|------|
| 返回 | 已经打开的设备列表 |
---
### 4.21 disconnect
```java
public void disconnect(@NonNull
android.hardware.usb.UsbDevice usbDevice)
```
断开 USB 设备连接。
| 项目 | 说明 |
|------|------|
| 参数 | usbDevice - USB 设备 |
---
### 4.22 close
```java
public void close(@NonNull Context context)
```
释放资源、断开所有连接设备。
| 项目 | 说明 |
|------|------|
| 参数 | context - 上下文 |
---
### 4.23 isSupportGPIOFeature
```java
public boolean isSupportGPIOFeature(UsbDevice device)
throws java.lang.Exception
```
检查本接口当前是否支持该设备的 GPIO 特性配置。应当在操作 GPIO 前调用。
| 项目 | 说明 |
|------|------|
| 参数 | device - USB 设备 |
| 返回 | true 支持false 不支持 |
| 抛出 | java.lang.Exception |
---
### 4.24 queryGPIOCount
```java
public int queryGPIOCount(UsbDevice device)
throws java.lang.Exception
```
查询该 USB 设备的 GPIO 数量。
| 项目 | 说明 |
|------|------|
| 参数 | device - USB 设备 |
| 返回 | GPIO 数量 |
| 抛出 | java.lang.Exception |
---
### 4.25 queryGPIOStatus
```java
public GPIO_Status queryGPIOStatus(UsbDevice device, int gpioIndex)
throws java.lang.Exception
```
查询该 USB 设备的某个 GPIO 状态。
| 项目 | 说明 |
|------|------|
| 参数 | device - USB 设备<br>gpioIndex - GPIO 序号,从 0 开始 |
| 返回 | GPIO 状态 |
| 抛出 | java.lang.Exception |
---
### 4.26 queryAllGPIOStatus
```java
public List<GPIO_Status> queryAllGPIOStatus(UsbDevice device)
throws java.lang.Exception
```
查询该 USB 设备的全部 GPIO 状态。
| 项目 | 说明 |
|------|------|
| 参数 | device - USB 设备 |
| 返回 | 全部 GPIO 状态 |
| 抛出 | java.lang.Exception |
---
### 4.27 enableGPIO
```java
public boolean enableGPIO(UsbDevice device, int gpioIndex, boolean enable,
GPIO_DIR dir)
throws java.lang.Exception
```
使能该硬件设备的某个 GPIO。
| 项目 | 说明 |
|------|------|
| 参数 | device - USB 设备<br>gpioIndex - GPIO 序号<br>enable - true 打开false 关闭<br>dir - GPIO 方向 |
| 抛出 | java.lang.Exception |
---
### 4.28 setGPIOVal
```java
public boolean setGPIOVal(UsbDevice device, int gpioIndex, GPIO_VALUE value)
throws java.lang.Exception
```
设置该硬件设备的某个 GPIO 的电平值。
| 项目 | 说明 |
|------|------|
| 参数 | device - USB 设备<br>gpioIndex - GPIO 序号<br>value - GPIO 电平值 |
| 返回 | true 操作成功false 操作失败 |
| 抛出 | java.lang.Exception |
---
### 4.29 getGPIOVal
```java
public GPIO_VALUE getGPIOVal(UsbDevice device, int gpioIndex)
throws java.lang.Exception
```
获取该硬件设备的某个 GPIO 的电平值。
| 项目 | 说明 |
|------|------|
| 参数 | device - USB 设备<br>gpioIndex - GPIO 序号 |
| 返回 | value - GPIO 电平值 |
| 抛出 | java.lang.Exception |
---
### 4.30 setDTR
```java
public boolean setDTR(@NonNull UsbDevice usbDevice, int serialNumber, boolean valid)
throws Exception
```
设置 DTR 信号。
| 项目 | 说明 |
|------|------|
| 参数 | device - USB 设备<br>serialNumber - 串口号<br>valid - 是否有效(低电平有效) |
| 返回 | true 操作成功false 操作失败 |
| 抛出 | java.lang.Exception |
---
### 4.31 setRTS
```java
public boolean setRTS(@NonNull UsbDevice usbDevice, int serialNumber, boolean valid)
throws Exception
```
设置 RTS 信号。
| 项目 | 说明 |
|------|------|
| 参数 | device - USB 设备<br>serialNumber - 串口号<br>valid - 是否有效(低电平有效) |
| 返回 | true 操作成功false 操作失败 |
| 抛出 | java.lang.Exception |
---
### 4.32 setBreak
```java
public boolean setBreak(@NonNull UsbDevice usbDevice, int serialNumber, boolean valid)
throws Exception
```
设置 Break 信号。
| 项目 | 说明 |
|------|------|
| 参数 | device - USB 设备<br>serialNumber - 串口号<br>valid - 是否有效(低电平有效) |
| 返回 | true 操作成功false 操作失败 |
| 抛出 | java.lang.Exception |
---
### 4.33 registerModemStatusCallback
```java
public void registerModemStatusCallback(@NonNull UsbDevice usbDevice, IModemStatus
modemStatus) throws Exception
```
注册 Modem 输入信号状态的回调。
| 项目 | 说明 |
|------|------|
| 参数 | device - USB 设备<br>modemStatus - 状态回调 |
| 抛出 | java.lang.Exception |
---
### 4.34 querySerialErrorCount
```java
public int querySerialErrorCount(@NonNull UsbDevice usbDevice, int
serialNumber, @NonNull SerialErrorType errorType) throws Exception
```
查询串口错误状态。
| 项目 | 说明 |
|------|------|
| 参数 | device - USB 设备<br>serialNumber - 串口号<br>errorType - 错误类型 |
| 返回 | 该种错误的次数 |
| 抛出 | java.lang.Exception |
---
### 4.35 setReadTimeout
```java
public static void setReadTimeout(int timeout)
```
设置读超时时间。默认为 0即采用异步方式读取设置为非 0 时采用同步方式,超时时间为 BulkTransfer 同步传输的超时时间。全局生效,应在 APP 初始化时设置。
| 项目 | 说明 |
|------|------|
| 参数 | timeout - 超时时间,单位为 ms |
---
### 4.36 addNewHardwareAndChipType
```java
public static void addNewHardwareAndChipType(int vid, int pid, ChipType2 chipType)
```
添加新的硬件 VID/PID 以及芯片类型。
| 项目 | 说明 |
|------|------|
| 参数 | vid - 硬件 vid<br>pid - 硬件 pid<br>chipType - 芯片类型 |
---
### 4.37 setDebug
```java
public static void setDebug(boolean open)
```
设置调试模式打开或者关闭。打开调试模式会打印日志,默认关闭。应在 APP 初始化时设置。
| 项目 | 说明 |
|------|------|
| 参数 | open - true 打开false 关闭 |
---
### 4.38 isDebugMode
```java
public static boolean isDebugMode()
```
返回当前是否在调试模式,是否打印日志。
| 项目 | 说明 |
|------|------|
| 返回 | true 处于调试模式false 不处于调试模式 |
---
## 附录
- **文档版本:** V1.8
- **文档名称:** CH34X 系列芯片串口 Android 程序开发说明
- **适用芯片型号:** CH339 / CH340 / CH341 / CH342 / CH343 / CH344 / CH347 / CH9101 / CH9102 / CH9103 / CH9104 / CH9143 / CH9111 / CH9114
- **系统要求:** Android 4.4 及以上版本
- **技术支持网站:** https://wch.cn

1265
docs/USAGE_GUIDE.md Normal file

File diff suppressed because it is too large Load Diff

43
example/.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

16
example/README.md Normal file
View File

@@ -0,0 +1,16 @@
# ch34_example
Demonstrates how to use the ch34 plugin.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

13
example/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,58 @@
plugins {
id "com.android.application"
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader("UTF-8") { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty("flutter.versionCode")
if (flutterVersionCode == null) {
flutterVersionCode = "1"
}
def flutterVersionName = localProperties.getProperty("flutter.versionName")
if (flutterVersionName == null) {
flutterVersionName = "1.0"
}
android {
namespace = "com.example.ch34_example"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.ch34_example"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutterVersionCode.toInteger()
versionName = flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.debug
}
}
}
flutter {
source = "../.."
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,72 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="ch34_example"
android:name="${applicationName}"
android:enabled="true"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- 添加你的 BroadcastReceiver 定义 -->
<!-- <receiver-->
<!-- android:name="com.example.ch34.MyBroadcastReceiver"-->
<!-- android:directBootAware="true"-->
<!-- android:enabled="true"-->
<!-- android:exported="true"> &lt;!&ndash; 根据需要设置为 true 或 false &ndash;&gt;-->
<!-- <intent-filter>-->
<!-- <action android:name="com.example.ch34.intent" />-->
<!-- </intent-filter>-->
<!-- </receiver>-->
<!-- <receiver-->
<!-- android:name="com.example.ch34_example.MyBroadcastReceiver"-->
<!-- android:directBootAware="true"-->
<!-- android:enabled="true"-->
<!-- android:exported="true">-->
<!-- <intent-filter>-->
<!-- <action android:name="com.example.ch34_example.intent" />-->
<!-- </intent-filter>-->
<!-- </receiver>-->
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,6 @@
package com.example.ch34_example;
import io.flutter.embedding.android.FlutterActivity;
public class MainActivity extends FlutterActivity {
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,18 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip

View File

@@ -0,0 +1,25 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.7.0" apply false
id "org.jetbrains.kotlin.android" version "2.0.20" apply false
}
include ":app"

View File

@@ -0,0 +1,13 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:ch34/ch34.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('getPlatformVersion test', (WidgetTester tester) async {
final String? version = await Ch34Manager.getPlatformVersion();
expect(version?.isNotEmpty, true);
});
}

189
example/lib/main.dart Normal file
View File

@@ -0,0 +1,189 @@
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:ch34/ch34.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
List<UsbDeviceInfo> _devices = [];
String _status = '未连接';
String _receivedData = '';
@override
void initState() {
super.initState();
initPlatformState();
}
Future<void> initPlatformState() async {
String platformVersion;
try {
platformVersion =
await Ch34Manager.getPlatformVersion() ?? 'Unknown platform version';
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
});
}
Future<void> _scanDevices() async {
try {
final devices = await Ch34Manager.enumDevice();
setState(() {
_devices = devices;
_status = '发现 ${devices.length} 个设备';
});
} catch (e) {
setState(() {
_status = '扫描失败: $e';
});
}
}
Future<void> _openDevice(String deviceName) async {
try {
final success = await Ch34Manager.openDevice(deviceName);
if (success) {
await Ch34Manager.setSerialParameter(
deviceName,
0,
const SerialParameter(baud: 115200),
);
await Ch34Manager.registerDataCallback(
deviceName,
0,
(Uint8List data) {
final hex = data.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
setState(() {
_receivedData = '收到: $hex';
});
},
);
setState(() {
_status = '已连接: $deviceName';
});
} else {
setState(() {
_status = '打开失败';
});
}
} catch (e) {
setState(() {
_status = '连接失败: $e';
});
}
}
Future<void> _sendData(String deviceName) async {
try {
final data = Uint8List.fromList([0x01, 0x02, 0x03]);
final sent = await Ch34Manager.writeData(deviceName, 0, data);
setState(() {
_status = '已发送 $sent 字节';
});
} catch (e) {
setState(() {
_status = '发送失败: $e';
});
}
}
Future<void> _disconnect(String deviceName) async {
try {
Ch34Manager.removeDataCallback(deviceName);
await Ch34Manager.disconnect(deviceName);
setState(() {
_status = '已断开';
});
} catch (e) {
setState(() {
_status = '断开失败: $e';
});
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('CH34X 示例'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('平台版本: $_platformVersion\n'),
Text('状态: $_status\n'),
if (_receivedData.isNotEmpty)
Text('接收数据: $_receivedData\n'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _scanDevices,
child: const Text('扫描设备'),
),
const SizedBox(height: 8),
if (_devices.isNotEmpty)
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _devices.length,
itemBuilder: (context, index) {
final device = _devices[index];
return ListTile(
title: Text('${device.deviceName}'),
subtitle: Text(
'VID: 0x${device.vendorId.toRadixString(16).toUpperCase()} '
'PID: 0x${device.productId.toRadixString(16).toUpperCase()} '
'端口: ${device.serialCount}'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.power),
onPressed: () => _openDevice(device.deviceName),
),
IconButton(
icon: const Icon(Icons.send),
onPressed: _status.contains('已连接')
? () => _sendData(device.deviceName)
: null,
),
IconButton(
icon: const Icon(Icons.power_off),
onPressed: _status.contains('已连接')
? () => _disconnect(device.deviceName)
: null,
),
],
),
);
},
),
],
),
),
),
);
}
}

283
example/pubspec.lock Normal file
View File

@@ -0,0 +1,283 @@
# 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"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
ch34:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "1.1.0"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.19.1"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.9"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.3"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_driver:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.2"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
integration_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.flutter-io.cn"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.17.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.1"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.8"
process:
dependency: transitive
description:
name: process
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.5"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
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"
sync_http:
dependency: transitive
description:
name: sync_http
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.10"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499"
url: "https://pub.flutter-io.cn"
source: hosted
version: "15.1.0"
webdriver:
dependency: transitive
description:
name: webdriver
sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.0"
sdks:
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"

85
example/pubspec.yaml Normal file
View File

@@ -0,0 +1,85 @@
name: ch34_example
description: "Demonstrates how to use the ch34 plugin."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
environment:
sdk: '>=3.4.3 <4.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
ch34:
# When depending on this package from a real application you should use:
# ch34: ^x.y.z
# See https://dart.dev/tools/pub/dependencies#version-constraints
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.6
dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^3.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages

View File

@@ -0,0 +1,27 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:ch34_example/main.dart';
void main() {
testWidgets('Verify Platform version', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that platform version is retrieved.
expect(
find.byWidgetPredicate(
(Widget widget) => widget is Text &&
widget.data!.startsWith('Running on:'),
),
findsOneWidget,
);
});
}

68
lib/ch34.dart Normal file
View File

@@ -0,0 +1,68 @@
/// CH34 Flutter 插件。
///
/// 用于与 WCH CH34X 系列 USB 转串口芯片进行通信。
///
/// 支持的芯片CH340/CH341/CH342/CH343/CH344/CH347/CH9101/CH9102/CH9103/CH9104/CH9143
///
/// 使用方法:
/// ```dart
/// import 'package:ch34/ch34.dart';
///
/// // 初始化插件(必须在首次调用 Ch34Manager 前调用)
/// Ch34.ensureInitialized();
///
/// // 枚举设备
/// final devices = await Ch34Manager.enumDevice();
///
/// // 打开设备
/// await Ch34Manager.openDevice(devices.first.deviceName);
///
/// // 设置参数
/// await Ch34Manager.setSerialParameter(
/// devices.first.deviceName,
/// 0,
/// const SerialParameter(baud: 9600),
/// );
///
/// // 发送数据
/// await Ch34Manager.writeData(
/// devices.first.deviceName,
/// 0,
/// Uint8List.fromList([0x01, 0x02]),
/// );
///
/// // 注册数据回调
/// Ch34Manager.registerDataCallback(
/// devices.first.deviceName,
/// 0,
/// (data) { print('Received: $data'); },
/// );
/// ```
library ch34;
export 'src/ch34_method_channel.dart' show Ch34Exception;
export 'src/ch34_platform_interface.dart' show Ch34Platform;
export 'src/types/ch34_types.dart';
export 'src/ch34_manager.dart';
import 'src/ch34_method_channel.dart';
/// CH34 插件入口类。
///
/// 提供插件初始化方法,确保平台实例已注册。
class Ch34 {
Ch34._();
static bool _initialized = false;
/// 初始化 CH34 插件。
///
/// 必须在首次调用 [Ch34Manager] 方法前调用此方法。
/// 重复调用不会产生副作用。
static void ensureInitialized() {
if (!_initialized) {
MethodChannelCh34.registerDefault();
_initialized = true;
}
}
}

View File

@@ -0,0 +1,5 @@
/// 顶层导出,保持向后兼容。
/// 所有实现已迁移到 `src/` 目录下。
library ch34_method_channel;
export 'src/ch34_method_channel.dart';

View File

@@ -0,0 +1,5 @@
/// 顶层导出,保持向后兼容。
/// 所有实现已迁移到 `src/` 目录下。
library ch34_platform_interface;
export 'src/ch34_platform_interface.dart';

491
lib/src/ch34_manager.dart Normal file
View File

@@ -0,0 +1,491 @@
import 'dart:typed_data';
import 'ch34_platform_interface.dart';
import 'types/ch34_types.dart';
/// CH34X USB 转串口插件的管理器。
///
/// 提供静态方法访问所有 WCH WCHUARTManager API。
/// 所有方法委托给 [Ch34Platform.instance] 实现。
class Ch34Manager {
Ch34Manager._();
/// ==================== 基础方法 ====================
/// 获取平台版本。
///
/// @return 平台版本字符串。
static Future<String?> getPlatformVersion() {
return Ch34Platform.instance.getPlatformVersion();
}
/// ==================== 设备枚举与识别 ====================
/// 枚举当前所有可用的 USB 设备。
///
/// @return 可用 USB 设备列表。
/// @throws Ch34Exception 如果枚举失败。
static Future<List<UsbDeviceInfo>> enumDevice() {
return Ch34Platform.instance.enumDevice();
}
/// 获取该 UsbDevice 的芯片型号。
///
/// @param deviceName 设备名称。
/// @return 芯片型号字符串null 表示无法识别。
static Future<String?> getChipType(String deviceName) {
return Ch34Platform.instance.getChipType(deviceName);
}
/// ==================== 设备打开与权限 ====================
/// 打开 USB 设备。
///
/// @param deviceName 设备名称。
/// @return `true` 成功,`false` 失败。
static Future<bool> openDevice(String deviceName) {
return Ch34Platform.instance.openDevice(deviceName);
}
/// 申请 USB 设备的权限。
///
/// @param deviceName 设备名称。
/// @return `true` 已授权,`false` 被拒绝。
static Future<bool> requestPermission(String deviceName) {
return Ch34Platform.instance.requestPermission(deviceName);
}
/// ==================== USB 状态监听 ====================
/// 注册 USB 设备插拔状态监听。
///
/// @param onStateChanged 状态变化回调。
static void setUsbStateListener(
void Function(String deviceName, bool connected) onStateChanged) {
Ch34Platform.instance.setUsbStateListener(onStateChanged);
}
/// 移除 USB 状态监听。
static void removeUsbStateListener() {
Ch34Platform.instance.removeUsbStateListener();
}
/// ==================== 串口信息 ====================
/// 获取设备的串口数目。
///
/// @param deviceName 设备名称。
/// @return 串口数目,-1 表示读取芯片型号失败。
static Future<int> getSerialCount(String deviceName) {
return Ch34Platform.instance.getSerialCount(deviceName);
}
/// 获取串口波特率。
///
/// 实际仅针对 CH9114 系列有效,其他类型设备无需调用。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @return 大于 0 表示串口波特率,小于 0 表示出错。
static Future<int> getSerialBaud(String deviceName, int serialNumber) {
return Ch34Platform.instance.getSerialBaud(deviceName, serialNumber);
}
/// 获取芯片主频。
///
/// 实际仅针对 CH9114 系列有效,其他类型设备无需调用。
///
/// @param deviceName 设备名称。
/// @return 芯片主频信息对象。
static Future<ChipMasterFrequency> getChipMasterFrequency(String deviceName) {
return Ch34Platform.instance.getChipMasterFrequency(deviceName);
}
/// 打开或关闭串口。
///
/// 实际仅针对 CH9114 系列有效,其他类型设备无需调用。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @param enable `true` 打开,`false` 关闭。
/// @return `true` 设置成功,`false` 设置失败。
static Future<bool> enableSerial(
String deviceName,
int serialNumber,
bool enable,
) {
return Ch34Platform.instance.enableSerial(deviceName, serialNumber, enable);
}
/// ==================== 串口参数设置 ====================
/// 设置串口参数。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号(从 0 开始)。
/// @param parameter 串口参数配置。
/// @return `true` 设置成功,`false` 设置失败。
static Future<bool> setSerialParameter(
String deviceName,
int serialNumber,
SerialParameter parameter,
) {
return Ch34Platform.instance.setSerialParameter(
deviceName,
serialNumber,
parameter,
);
}
/// ==================== 数据读写 ====================
/// 发送串口数据(同步发送)。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @param data 要发送的数据。
/// @param timeout 超时时间毫秒0 表示不超时。
/// @return 实际发送的字节数。
static Future<int> writeData(
String deviceName,
int serialNumber,
Uint8List data, {
int timeout = 0,
}) {
return Ch34Platform.instance.writeData(
deviceName,
serialNumber,
data,
timeout: timeout,
);
}
/// 发送串口数据(异步发送)。
///
/// 将数据加入缓存持续发送,不返回状态和结果。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @param data 要发送的数据。
static Future<void> asyncWriteData(
String deviceName,
int serialNumber,
Uint8List data,
) {
return Ch34Platform.instance.asyncWriteData(
deviceName,
serialNumber,
data,
);
}
/// 阻塞读取串口数据。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @return 读取到的数据。
static Future<Uint8List> readData(
String deviceName,
int serialNumber,
) {
return Ch34Platform.instance.readData(deviceName, serialNumber);
}
/// 主动读取串口数据(带超时参数)。
///
/// 读取行为说明:
/// - 当 vTime>0vMin>0 时:阻塞直到读取到第一个字符后开始计时,
/// 时间到或已读够 vMin 个字符则返回。
/// - 当 vTime>0vMin=0 时:读到数据立即返回,否则最多等待 vTime。
/// - 当 vTime=0vMin>0 时:一直阻塞直到读到 vMin 个字符后返回。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @param vTime 等待时间(毫秒)。
/// @param vMin 读取的最小字节数。
/// @return 读取到的数据。
static Future<Uint8List> readDataWithTimeout(
String deviceName,
int serialNumber,
int vTime,
int vMin,
) {
return Ch34Platform.instance.readDataWithTimeout(
deviceName,
serialNumber,
vTime,
vMin,
);
}
/// 注册串口数据回调。
///
/// 注册后数据自动推送,不需要主动调用 [readData]。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @param onData 数据接收回调。
static Future<void> registerDataCallback(
String deviceName,
int serialNumber,
void Function(Uint8List data) onData,
) {
return Ch34Platform.instance.registerDataCallback(
deviceName,
serialNumber,
onData,
);
}
/// 取消注册串口数据回调。
///
/// @param deviceName 设备名称。
static void removeDataCallback(String deviceName) {
Ch34Platform.instance.removeDataCallback(deviceName);
}
/// ==================== 连接状态 ====================
/// 判断 USB 设备是否已经连接。
///
/// @param deviceName 设备名称。
/// @return `true` 已连接,`false` 未连接。
static Future<bool> isConnected(String deviceName) {
return Ch34Platform.instance.isConnected(deviceName);
}
/// 获取当前已经打开的设备列表。
///
/// @return 已打开的设备名称列表。
static Future<List<String>> getConnectedDevices() {
return Ch34Platform.instance.getConnectedDevices();
}
/// ==================== 断开与关闭 ====================
/// 断开 USB 设备的连接。
///
/// @param deviceName 设备名称。
static Future<void> disconnect(String deviceName) {
return Ch34Platform.instance.disconnect(deviceName);
}
/// 释放资源,关闭所有串口设备。
static Future<void> close() {
return Ch34Platform.instance.close();
}
/// ==================== GPIO 功能 ====================
/// 查询设备是否支持 GPIO 功能。
///
/// @param deviceName 设备名称。
/// @return `true` 支持,`false` 不支持。
static Future<bool> isSupportGpio(String deviceName) {
return Ch34Platform.instance.isSupportGpio(deviceName);
}
/// 查询该 USB 设备的 GPIO 数目。
///
/// @param deviceName 设备名称。
/// @return GPIO 数目。
static Future<int> queryGpioCount(String deviceName) {
return Ch34Platform.instance.queryGpioCount(deviceName);
}
/// 查询该 USB 设备指定 GPIO 的状态。
///
/// @param deviceName 设备名称。
/// @param gpioIndex GPIO 编号(从 0 开始)。
/// @return GPIO 状态。
static Future<GpioStatus> queryGpioStatus(
String deviceName,
int gpioIndex,
) {
return Ch34Platform.instance.queryGpioStatus(deviceName, gpioIndex);
}
/// 查询该 USB 设备的所有 GPIO 状态。
///
/// @param deviceName 设备名称。
/// @return 全部 GPIO 状态列表。
static Future<List<GpioStatus>> queryAllGpioStatus(String deviceName) {
return Ch34Platform.instance.queryAllGpioStatus(deviceName);
}
/// 使能指定 GPIO。
///
/// @param deviceName 设备名称。
/// @param gpioIndex GPIO 编号。
/// @param enable `true` 使能,`false` 关闭。
/// @param direction GPIO 方向。
/// @return `true` 使能成功,`false` 使能失败。
static Future<bool> enableGpio(
String deviceName,
int gpioIndex,
bool enable,
GpioDirection direction,
) {
return Ch34Platform.instance.enableGpio(
deviceName,
gpioIndex,
enable,
direction,
);
}
/// 设置指定 GPIO 的电平值。
///
/// @param deviceName 设备名称。
/// @param gpioIndex GPIO 编号。
/// @param value GPIO 电平值。
/// @return `true` 设置成功,`false` 设置失败。
static Future<bool> setGpioVal(
String deviceName,
int gpioIndex,
GpioValue value,
) {
return Ch34Platform.instance.setGpioVal(
deviceName,
gpioIndex,
value,
);
}
/// 获取指定 GPIO 的电平值。
///
/// @param deviceName 设备名称。
/// @param gpioIndex GPIO 编号。
/// @return GPIO 电平值。
static Future<GpioValue> getGpioVal(
String deviceName,
int gpioIndex,
) {
return Ch34Platform.instance.getGpioVal(deviceName, gpioIndex);
}
/// ==================== 信号控制 ====================
/// 设置 DTR 信号。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @param valid 是否有效(低电平有效)。
/// @return `true` 设置成功,`false` 设置失败。
static Future<bool> setDtr(
String deviceName,
int serialNumber,
bool valid,
) {
return Ch34Platform.instance.setDtr(deviceName, serialNumber, valid);
}
/// 设置 RTS 信号。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @param valid 是否有效(低电平有效)。
/// @return `true` 设置成功,`false` 设置失败。
static Future<bool> setRts(
String deviceName,
int serialNumber,
bool valid,
) {
return Ch34Platform.instance.setRts(deviceName, serialNumber, valid);
}
/// 设置 Break 信号。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @param valid 是否有效(低电平有效)。
/// @return `true` 设置成功,`false` 设置失败。
static Future<bool> setBreakSignal(
String deviceName,
int serialNumber,
bool valid,
) {
return Ch34Platform.instance.setBreakSignal(
deviceName,
serialNumber,
valid,
);
}
/// ==================== Modem 状态回调 ====================
/// 注册 Modem 控制信号状态回调。
///
/// @param deviceName 设备名称。
/// @param onModemStatus Modem 状态变化回调。
static Future<void> registerModemStatusCallback(
String deviceName,
void Function(ModemStatus status) onModemStatus,
) {
return Ch34Platform.instance.registerModemStatusCallback(
deviceName,
onModemStatus,
);
}
/// 移除 Modem 状态回调。
///
/// @param deviceName 设备名称。
static void removeModemStatusCallback(String deviceName) {
Ch34Platform.instance.removeModemStatusCallback(deviceName);
}
/// ==================== 错误查询 ====================
/// 查询串口错误状态。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @param errorType 错误类型。
/// @return 该种错误出现的次数。
static Future<int> querySerialErrorCount(
String deviceName,
int serialNumber,
SerialErrorType errorType,
) {
return Ch34Platform.instance.querySerialErrorCount(
deviceName,
serialNumber,
errorType,
);
}
/// ==================== 全局配置 ====================
/// 设置读取超时时间。
///
/// 全局有效,应在 APP 初始化时调用。
///
/// @param timeout 超时时间(毫秒)。
static Future<void> setReadTimeout(int timeout) {
return Ch34Platform.instance.setReadTimeout(timeout);
}
/// 添加自定义硬件 VID/PID。
///
/// @param vid 硬件 VID。
/// @param pid 硬件 PID。
/// @param chipType 芯片类型(必填,如 "CH340"、"CH9102")。
static Future<void> addNewHardware(int vid, int pid, String chipType) {
return Ch34Platform.instance.addNewHardware(vid, pid, chipType);
}
/// 设置调试模式。
///
/// @param enabled `true` 开启,`false` 关闭。
static Future<void> setDebug(bool enabled) {
return Ch34Platform.instance.setDebug(enabled);
}
/// 返回当前是否处于调试模式。
///
/// @return `true` 处于调试模式,`false` 不处于。
static Future<bool> isDebugMode() {
return Ch34Platform.instance.isDebugMode();
}
}

View File

@@ -0,0 +1,589 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'ch34_platform_interface.dart';
import 'types/ch34_types.dart';
/// [Ch34Platform] 的 MethodChannel 实现。
///
/// 将平台接口方法映射到原生端 MethodChannel 调用,
/// 使用 EventChannel 处理数据回调、Modem 状态和 USB 插拔事件。
class MethodChannelCh34 extends Ch34Platform {
/// 注册默认实例。
static void registerDefault() {
Ch34Platform.instance = MethodChannelCh34._();
}
MethodChannelCh34._();
/// MethodChannel 名称。
@visibleForTesting
final methodChannel = const MethodChannel('ch34');
/// 数据 EventChannel 名称。
@visibleForTesting
final dataEventChannel = const EventChannel('ch34/data');
/// Modem 状态 EventChannel 名称。
@visibleForTesting
final modemEventChannel = const EventChannel('ch34/modem');
/// USB 状态 EventChannel 名称。
@visibleForTesting
final usbStateEventChannel = const EventChannel('ch34/usb_state');
// ==================== 事件流订阅 ====================
StreamSubscription? _dataSubscription;
StreamSubscription? _modemSubscription;
StreamSubscription? _usbStateSubscription;
void Function(String deviceName, bool connected)? _usbStateCallback;
void Function(Uint8List data)? _dataCallback;
void Function(ModemStatus status)? _modemCallback;
// ==================== 基础方法 ====================
@override
Future<String?> getPlatformVersion() async {
final version =
await methodChannel.invokeMethod<String>('getPlatformVersion');
return version;
}
// ==================== 设备枚举与识别 ====================
@override
Future<List<UsbDeviceInfo>> enumDevice() async {
final result = await methodChannel.invokeMethod<List>('enumDevice');
if (result == null) return [];
return result
.map((e) => UsbDeviceInfo.fromMap(e as Map<dynamic, dynamic>))
.toList();
}
@override
Future<String?> getChipType(String deviceName) async {
return await methodChannel.invokeMethod<String>(
'getChipType',
{'deviceName': deviceName},
);
}
// ==================== 设备打开与权限 ====================
@override
Future<bool> openDevice(String deviceName) async {
final result = await methodChannel.invokeMethod<bool>(
'openDevice',
{'deviceName': deviceName},
);
return result ?? false;
}
@override
Future<bool> requestPermission(String deviceName) async {
final result = await methodChannel.invokeMethod<bool>(
'requestPermission',
{'deviceName': deviceName},
);
return result ?? false;
}
// ==================== USB 状态监听 ====================
@override
void setUsbStateListener(
void Function(String deviceName, bool connected) onStateChanged) {
_usbStateCallback = onStateChanged;
_usbStateSubscription?.cancel();
_usbStateSubscription = usbStateEventChannel
.receiveBroadcastStream()
.listen(
(event) {
final map = event as Map<dynamic, dynamic>;
final deviceName = map['deviceName'] as String;
final connected = map['connected'] as bool;
_usbStateCallback?.call(deviceName, connected);
},
onError: (error) {
debugPrint('CH34 USB state error: $error');
},
);
}
@override
void removeUsbStateListener() {
_usbStateSubscription?.cancel();
_usbStateSubscription = null;
_usbStateCallback = null;
}
// ==================== 串口信息 ====================
@override
Future<int> getSerialCount(String deviceName) async {
final result = await methodChannel.invokeMethod<int>(
'getSerialCount',
{'deviceName': deviceName},
);
return result ?? -1;
}
@override
Future<int> getSerialBaud(String deviceName, int serialNumber) async {
final result = await methodChannel.invokeMethod<int>(
'getSerialBaud',
{
'deviceName': deviceName,
'serialNumber': serialNumber,
},
);
return result ?? -1;
}
@override
Future<ChipMasterFrequency> getChipMasterFrequency(String deviceName) async {
final result = await methodChannel.invokeMethod<Map>(
'getChipMasterFrequency',
{'deviceName': deviceName},
);
if (result == null) {
throw const Ch34Exception('Failed to get chip master frequency');
}
return ChipMasterFrequency.fromMap(result);
}
// ==================== 串口参数设置 ====================
@override
Future<bool> enableSerial(
String deviceName,
int serialNumber,
bool enable,
) async {
final result = await methodChannel.invokeMethod<bool>(
'enableSerial',
{
'deviceName': deviceName,
'serialNumber': serialNumber,
'enable': enable,
},
);
return result ?? false;
}
@override
Future<bool> setSerialParameter(
String deviceName,
int serialNumber,
SerialParameter parameter,
) async {
final result = await methodChannel.invokeMethod<bool>(
'setSerialParameter',
{
'deviceName': deviceName,
'serialNumber': serialNumber,
...parameter.toMap(),
},
);
return result ?? false;
}
// ==================== 数据读写 ====================
@override
Future<int> writeData(
String deviceName,
int serialNumber,
Uint8List data, {
int timeout = 0,
}) async {
final result = await methodChannel.invokeMethod<int>(
'writeData',
{
'deviceName': deviceName,
'serialNumber': serialNumber,
'data': data,
'timeout': timeout,
},
);
return result ?? 0;
}
@override
Future<void> asyncWriteData(
String deviceName,
int serialNumber,
Uint8List data,
) async {
await methodChannel.invokeMethod(
'asyncWriteData',
{
'deviceName': deviceName,
'serialNumber': serialNumber,
'data': data,
},
);
}
@override
Future<Uint8List> readData(String deviceName, int serialNumber) async {
final result = await methodChannel.invokeMethod<Uint8List>(
'readData',
{
'deviceName': deviceName,
'serialNumber': serialNumber,
},
);
return result ?? Uint8List(0);
}
@override
Future<Uint8List> readDataWithTimeout(
String deviceName,
int serialNumber,
int vTime,
int vMin,
) async {
final result = await methodChannel.invokeMethod<Uint8List>(
'readDataWithTimeout',
{
'deviceName': deviceName,
'serialNumber': serialNumber,
'vTime': vTime,
'vMin': vMin,
},
);
return result ?? Uint8List(0);
}
@override
Future<void> registerDataCallback(
String deviceName,
int serialNumber,
void Function(Uint8List data) onData,
) async {
_dataCallback = onData;
await _cancelDataSubscription();
_dataSubscription = dataEventChannel
.receiveBroadcastStream({'deviceName': deviceName})
.listen(
(event) {
if (event is Uint8List) {
_dataCallback?.call(event);
}
},
onError: (error) {
debugPrint('CH34 data callback error: $error');
},
);
await methodChannel.invokeMethod('registerDataCallback', {
'deviceName': deviceName,
'serialNumber': serialNumber,
});
}
@override
void removeDataCallback(String deviceName) {
_cancelDataSubscription();
methodChannel.invokeMethod('removeDataCallback', {
'deviceName': deviceName,
});
}
Future<void> _cancelDataSubscription() async {
await _dataSubscription?.cancel();
_dataSubscription = null;
_dataCallback = null;
}
// ==================== 连接状态 ====================
@override
Future<bool> isConnected(String deviceName) async {
final result = await methodChannel.invokeMethod<bool>(
'isConnected',
{'deviceName': deviceName},
);
return result ?? false;
}
@override
Future<List<String>> getConnectedDevices() async {
final result = await methodChannel.invokeMethod<List>(
'getConnectedDevices',
);
if (result == null) return [];
return result.map((e) => e.toString()).toList();
}
// ==================== 断开与关闭 ====================
@override
Future<void> disconnect(String deviceName) async {
await methodChannel.invokeMethod(
'disconnect',
{'deviceName': deviceName},
);
}
@override
Future<void> close() async {
// 清理所有订阅
await _cancelDataSubscription();
await _modemSubscription?.cancel();
_modemSubscription = null;
_modemCallback = null;
_usbStateSubscription?.cancel();
_usbStateSubscription = null;
_usbStateCallback = null;
await methodChannel.invokeMethod('close');
}
// ==================== GPIO 功能 ====================
@override
Future<bool> isSupportGpio(String deviceName) async {
final result = await methodChannel.invokeMethod<bool>(
'isSupportGpio',
{'deviceName': deviceName},
);
return result ?? false;
}
@override
Future<int> queryGpioCount(String deviceName) async {
final result = await methodChannel.invokeMethod<int>(
'queryGpioCount',
{'deviceName': deviceName},
);
return result ?? 0;
}
@override
Future<GpioStatus> queryGpioStatus(String deviceName, int gpioIndex) async {
final result = await methodChannel.invokeMethod<Map>(
'queryGpioStatus',
{
'deviceName': deviceName,
'gpioIndex': gpioIndex,
},
);
if (result == null) {
throw const Ch34Exception('Failed to query GPIO status');
}
return GpioStatus.fromMap(result);
}
@override
Future<List<GpioStatus>> queryAllGpioStatus(String deviceName) async {
final result = await methodChannel.invokeMethod<List>(
'queryAllGpioStatus',
{'deviceName': deviceName},
);
if (result == null) return [];
return result
.map((e) => GpioStatus.fromMap(e as Map<dynamic, dynamic>))
.toList();
}
@override
Future<bool> enableGpio(
String deviceName,
int gpioIndex,
bool enable,
GpioDirection direction,
) async {
final result = await methodChannel.invokeMethod<bool>(
'enableGpio',
{
'deviceName': deviceName,
'gpioIndex': gpioIndex,
'enable': enable,
'direction': direction.index,
},
);
return result ?? false;
}
@override
Future<bool> setGpioVal(
String deviceName,
int gpioIndex,
GpioValue value,
) async {
final result = await methodChannel.invokeMethod<bool>(
'setGpioVal',
{
'deviceName': deviceName,
'gpioIndex': gpioIndex,
'value': value.index,
},
);
return result ?? false;
}
@override
Future<GpioValue> getGpioVal(String deviceName, int gpioIndex) async {
final result = await methodChannel.invokeMethod<int>(
'getGpioVal',
{
'deviceName': deviceName,
'gpioIndex': gpioIndex,
},
);
if (result == null) {
throw const Ch34Exception('Failed to get GPIO value');
}
return GpioValue.values[result];
}
// ==================== 信号控制 ====================
@override
Future<bool> setDtr(String deviceName, int serialNumber, bool valid) async {
final result = await methodChannel.invokeMethod<bool>(
'setDtr',
{
'deviceName': deviceName,
'serialNumber': serialNumber,
'valid': valid,
},
);
return result ?? false;
}
@override
Future<bool> setRts(String deviceName, int serialNumber, bool valid) async {
final result = await methodChannel.invokeMethod<bool>(
'setRts',
{
'deviceName': deviceName,
'serialNumber': serialNumber,
'valid': valid,
},
);
return result ?? false;
}
@override
Future<bool> setBreakSignal(
String deviceName, int serialNumber, bool valid) async {
final result = await methodChannel.invokeMethod<bool>(
'setBreak',
{
'deviceName': deviceName,
'serialNumber': serialNumber,
'valid': valid,
},
);
return result ?? false;
}
// ==================== Modem 状态回调 ====================
@override
Future<void> registerModemStatusCallback(
String deviceName,
void Function(ModemStatus status) onModemStatus,
) async {
_modemCallback = onModemStatus;
await _modemSubscription?.cancel();
_modemSubscription = modemEventChannel
.receiveBroadcastStream({'deviceName': deviceName})
.listen(
(event) {
final map = event as Map<dynamic, dynamic>;
debugPrint('CH34 modem event received: $map');
_modemCallback?.call(ModemStatus.fromMap(map));
},
onError: (error) {
debugPrint('CH34 modem callback error: $error');
},
);
await methodChannel.invokeMethod('registerModemStatusCallback', {
'deviceName': deviceName,
});
}
@override
void removeModemStatusCallback(String deviceName) {
_modemSubscription?.cancel();
_modemSubscription = null;
_modemCallback = null;
methodChannel.invokeMethod('removeModemStatusCallback', {
'deviceName': deviceName,
});
}
// ==================== 错误查询 ====================
@override
Future<int> querySerialErrorCount(
String deviceName,
int serialNumber,
SerialErrorType errorType,
) async {
final result = await methodChannel.invokeMethod<int>(
'querySerialErrorCount',
{
'deviceName': deviceName,
'serialNumber': serialNumber,
'errorType': errorType.toNativeString(),
},
);
return result ?? 0;
}
// ==================== 全局配置 ====================
@override
Future<void> setReadTimeout(int timeout) async {
await methodChannel.invokeMethod(
'setReadTimeout',
{'timeout': timeout},
);
}
@override
Future<void> addNewHardware(int vid, int pid, String chipType) async {
await methodChannel.invokeMethod(
'addNewHardware',
{
'vid': vid,
'pid': pid,
'chipType': chipType,
},
);
}
@override
Future<void> setDebug(bool enabled) async {
await methodChannel.invokeMethod(
'setDebug',
{'enabled': enabled},
);
}
@override
Future<bool> isDebugMode() async {
final result =
await methodChannel.invokeMethod<bool>('isDebugMode');
return result ?? false;
}
}
/// CH34 插件自定义异常。
class Ch34Exception implements Exception {
const Ch34Exception(this.message);
/// 异常描述。
final String message;
@override
String toString() => 'Ch34Exception: $message';
}

View File

@@ -0,0 +1,418 @@
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'types/ch34_types.dart';
/// CH34X USB 转串口插件的抽象平台接口。
///
/// 定义所有 WCH WCHUARTManager API 的方法签名。
/// 具体实现由 `MethodChannelCh34` 提供。
abstract class Ch34Platform extends PlatformInterface {
/// Constructs a Ch34Platform.
Ch34Platform() : super(token: _token);
static final Object _token = Object();
static Ch34Platform? _instance;
/// The default instance of [Ch34Platform] to use.
///
/// Defaults to [MethodChannelCh34].
/// 注意: 需要在应用初始化时调用 [Ch34Platform.registerDefaultInstance] 注册默认实例。
static Ch34Platform get instance {
if (_instance == null) {
throw StateError(
'Ch34Platform.instance has not been initialized. '
'Ensure the plugin is properly registered.',
);
}
return _instance!;
}
/// 注册默认平台实例。
///
/// 通常在插件初始化时由 [MethodChannelCh34] 调用。
static void registerDefaultInstance(Ch34Platform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
/// Platform-specific implementations should set this with their own
/// platform-specific class that extends [Ch34Platform] when they register
/// themselves.
static set instance(Ch34Platform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
/// ==================== 基础方法 ====================
/// 获取平台版本。
///
/// @return 平台版本字符串。
Future<String?> getPlatformVersion();
/// ==================== 设备枚举与识别 ====================
/// 枚举当前所有可用的 USB 设备。
///
/// 返回设备信息列表,包含 VID/PID、串口数量等。
///
/// @return 可用 USB 设备列表。
/// @throws Ch34Exception 如果枚举失败。
Future<List<UsbDeviceInfo>> enumDevice();
/// 获取该 UsbDevice 的芯片型号。
///
/// @param deviceName 设备名称(由 [enumDevice] 返回的 deviceName
/// @return 芯片型号字符串。如果为 null表示无法识别。
Future<String?> getChipType(String deviceName);
/// ==================== 设备打开与权限 ====================
/// 打开 USB 设备。
///
/// 打开设备后可以进行串口通信。
///
/// @param deviceName 设备名称。
/// @return `true` 成功,`false` 失败。
Future<bool> openDevice(String deviceName);
/// 申请 USB 设备的权限。
///
/// @param deviceName 设备名称。
/// @return `true` 已授权,`false` 被拒绝。
Future<bool> requestPermission(String deviceName);
/// ==================== USB 状态监听 ====================
/// 注册 USB 设备插拔状态监听。
///
/// 通过 EventChannel 监听设备的连接和断开事件。
///
/// @param onStateChanged 状态变化回调,参数为设备名称和是否已连接。
void setUsbStateListener(void Function(String deviceName, bool connected) onStateChanged);
/// 移除 USB 状态监听。
void removeUsbStateListener();
/// ==================== 串口信息 ====================
/// 获取设备的串口数目。
///
/// @param deviceName 设备名称。
/// @return 串口数目;如果为 -1说明读取芯片型号失败。
Future<int> getSerialCount(String deviceName);
/// 获取串口波特率。
///
/// 实际仅针对 CH9114 系列有效,其他类型设备无需调用。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @return 大于 0 表示串口波特率,小于 0 表示出错。
Future<int> getSerialBaud(String deviceName, int serialNumber);
/// 获取芯片主频。
///
/// 实际仅针对 CH9114 系列有效,其他类型设备无需调用。
///
/// @param deviceName 设备名称。
/// @return 芯片主频信息对象。
Future<ChipMasterFrequency> getChipMasterFrequency(String deviceName);
/// 打开或关闭串口。
///
/// 实际仅针对 CH9114 系列有效,其他类型设备无需调用。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @param enable `true` 打开,`false` 关闭。
/// @return `true` 设置成功,`false` 设置失败。
/// @throws Ch34Exception 如果操作失败。
Future<bool> enableSerial(String deviceName, int serialNumber, bool enable);
/// ==================== 串口参数设置 ====================
/// 设置串口参数。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号(从 0 开始)。
/// @param parameter 串口参数配置。
/// @return `true` 设置成功,`false` 设置失败。
/// @throws Ch34Exception 如果参数无效或设备未打开。
Future<bool> setSerialParameter(
String deviceName,
int serialNumber,
SerialParameter parameter,
);
/// ==================== 数据读写 ====================
/// 发送串口数据(同步发送)。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @param data 要发送的数据。
/// @param timeout 超时时间毫秒0 表示不超时。
/// @return 实际发送的字节数。
/// @throws Ch34Exception 如果发送失败。
Future<int> writeData(
String deviceName,
int serialNumber,
Uint8List data, {
int timeout = 0,
});
/// 发送串口数据(异步发送)。
///
/// 将数据加入缓存持续发送,不返回状态和结果。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @param data 要发送的数据。
/// @throws Ch34Exception 如果发送失败。
Future<void> asyncWriteData(
String deviceName,
int serialNumber,
Uint8List data,
);
/// 阻塞读取串口数据。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @return 读取到的数据。
/// @throws Ch34Exception 如果读取失败。
Future<Uint8List> readData(String deviceName, int serialNumber);
/// 主动读取串口数据(带超时参数)。
///
/// 读取行为说明:
/// - 当 vTime>0vMin>0 时:读取将保持阻塞直到读取到第一个字符,
/// 读到了第一个字符之后开始计时,此后若时间到了 vTime 或者时间未到
/// 但已读够了 vMin 个字符则会返回。
/// - 当 vTime>0vMin=0 时:读取读到数据则立即返回,否则将为每个字符
/// 最多等待 vTime 时间。
/// - 当 vTime=0vMin>0 时:读取一直阻塞,直到读到 vMin 个字符后立即返回。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @param vTime 等待时间(毫秒)。
/// @param vMin 读取的最小字节数。
/// @return 读取到的数据。
/// @throws Ch34Exception 如果读取失败。
Future<Uint8List> readDataWithTimeout(
String deviceName,
int serialNumber,
int vTime,
int vMin,
);
/// 注册串口数据回调。
///
/// 注册后数据会通过 EventChannel 自动推送,不需要主动调用 [readData]。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @param onData 数据接收回调。
/// @throws Ch34Exception 如果注册失败。
Future<void> registerDataCallback(
String deviceName,
int serialNumber,
void Function(Uint8List data) onData,
);
/// 取消注册串口数据回调。
///
/// @param deviceName 设备名称。
void removeDataCallback(String deviceName);
/// ==================== 连接状态 ====================
/// 判断 USB 设备是否已经连接。
///
/// @param deviceName 设备名称。
/// @return `true` 已连接,`false` 未连接。
Future<bool> isConnected(String deviceName);
/// 获取当前已经打开的设备列表。
///
/// @return 已打开的设备名称列表。
Future<List<String>> getConnectedDevices();
/// ==================== 断开与关闭 ====================
/// 断开 USB 设备的连接。
///
/// @param deviceName 设备名称。
Future<void> disconnect(String deviceName);
/// 释放资源,关闭所有的串口设备。
Future<void> close();
/// ==================== GPIO 功能 ====================
/// 查询设备是否支持 GPIO 功能。
///
/// 应该在操作 GPIO 前调用。
///
/// @param deviceName 设备名称。
/// @return `true` 支持,`false` 不支持。
/// @throws Ch34Exception 如果查询失败。
Future<bool> isSupportGpio(String deviceName);
/// 查询该 USB 设备的 GPIO 数目。
///
/// @param deviceName 设备名称。
/// @return GPIO 数目。
/// @throws Ch34Exception 如果查询失败。
Future<int> queryGpioCount(String deviceName);
/// 查询该 USB 设备指定 GPIO 的状态。
///
/// @param deviceName 设备名称。
/// @param gpioIndex GPIO 编号(从 0 开始)。
/// @return GPIO 状态。
/// @throws Ch34Exception 如果查询失败。
Future<GpioStatus> queryGpioStatus(String deviceName, int gpioIndex);
/// 查询该 USB 设备的所有 GPIO 状态。
///
/// @param deviceName 设备名称。
/// @return 全部 GPIO 状态列表。
/// @throws Ch34Exception 如果查询失败。
Future<List<GpioStatus>> queryAllGpioStatus(String deviceName);
/// 使能指定 GPIO。
///
/// @param deviceName 设备名称。
/// @param gpioIndex GPIO 编号。
/// @param enable `true` 使能,`false` 关闭。
/// @param direction GPIO 方向。
/// @return `true` 使能成功,`false` 使能失败。
/// @throws Ch34Exception 如果操作失败。
Future<bool> enableGpio(
String deviceName,
int gpioIndex,
bool enable,
GpioDirection direction,
);
/// 设置指定 GPIO 的电平值。
///
/// @param deviceName 设备名称。
/// @param gpioIndex GPIO 编号。
/// @param value GPIO 电平值。
/// @return `true` 设置成功,`false` 设置失败。
/// @throws Ch34Exception 如果操作失败。
Future<bool> setGpioVal(
String deviceName,
int gpioIndex,
GpioValue value,
);
/// 获取指定 GPIO 的电平值。
///
/// @param deviceName 设备名称。
/// @param gpioIndex GPIO 编号。
/// @return GPIO 电平值。
/// @throws Ch34Exception 如果操作失败。
Future<GpioValue> getGpioVal(String deviceName, int gpioIndex);
/// ==================== 信号控制 ====================
/// 设置 DTR 信号。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @param valid 是否有效(低电平有效)。
/// @return `true` 设置成功,`false` 设置失败。
/// @throws Ch34Exception 如果操作失败。
Future<bool> setDtr(String deviceName, int serialNumber, bool valid);
/// 设置 RTS 信号。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @param valid 是否有效(低电平有效)。
/// @return `true` 设置成功,`false` 设置失败。
/// @throws Ch34Exception 如果操作失败。
Future<bool> setRts(String deviceName, int serialNumber, bool valid);
/// 设置 Break 信号。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @param valid 是否有效(低电平有效)。
/// @return `true` 设置成功,`false` 设置失败。
/// @throws Ch34Exception 如果操作失败。
Future<bool> setBreakSignal(String deviceName, int serialNumber, bool valid);
/// ==================== Modem 状态回调 ====================
/// 注册 Modem 控制信号状态回调。
///
/// @param deviceName 设备名称。
/// @param onModemStatus Modem 状态变化回调。
/// @throws Ch34Exception 如果注册失败。
Future<void> registerModemStatusCallback(
String deviceName,
void Function(ModemStatus status) onModemStatus,
);
/// 移除 Modem 状态回调。
///
/// @param deviceName 设备名称。
void removeModemStatusCallback(String deviceName);
/// ==================== 错误查询 ====================
/// 查询串口错误状态。
///
/// @param deviceName 设备名称。
/// @param serialNumber 串口号。
/// @param errorType 错误类型。
/// @return 该种错误出现的次数。
/// @throws Ch34Exception 如果查询失败。
Future<int> querySerialErrorCount(
String deviceName,
int serialNumber,
SerialErrorType errorType,
);
/// ==================== 全局配置 ====================
/// 设置读取超时时间。
///
/// 默认为 0使用 USBRequest 异步读取;
/// 如果不为 0使用同步传输超时单位为毫秒。
/// 全局有效,应在 APP 初始化时调用。
///
/// @param timeout 超时时间(毫秒)。
Future<void> setReadTimeout(int timeout);
/// 添加自定义硬件 VID/PID 及芯片类型。
///
/// 当用户修改了硬件设备的 VID 和 PID 后,需要将修改后的值添加到库中。
///
/// @param vid 硬件 VID。
/// @param pid 硬件 PID。
/// @param chipType 芯片类型(必填,如 "CH340"、"CH9102")。
Future<void> addNewHardware(int vid, int pid, String chipType);
/// 设置调试模式。
///
/// 开启调试模式会打印日志,默认关闭。
/// 应在 APP 初始化时调用。
///
/// @param enabled `true` 开启,`false` 关闭。
Future<void> setDebug(bool enabled);
/// 返回当前是否处于调试模式。
///
/// @return `true` 处于调试模式,`false` 不处于。
Future<bool> isDebugMode();
}

View File

@@ -0,0 +1,359 @@
/// CH34X 插件的所有类型和枚举定义。
///
/// 对应 WCH WCHUARTManager API 文档中的参数类型。
library ch34_types;
/// 数据位
enum DataBits {
bits5(5),
bits6(6),
bits7(7),
bits8(8);
const DataBits(this.value);
final int value;
static DataBits fromValue(int value) {
return DataBits.values.firstWhere(
(e) => e.value == value,
orElse: () => DataBits.bits8,
);
}
}
/// 停止位
enum StopBits {
one(1),
two(2);
const StopBits(this.value);
final int value;
static StopBits fromValue(int value) {
return StopBits.values.firstWhere(
(e) => e.value == value,
orElse: () => StopBits.one,
);
}
}
/// 校验位
enum Parity {
none(0),
odd(1),
even(2),
mark(3),
space(4);
const Parity(this.value);
final int value;
static Parity fromValue(int value) {
return Parity.values.firstWhere(
(e) => e.value == value,
orElse: () => Parity.none,
);
}
}
/// GPIO 方向
enum GpioDirection {
inDir,
outDir;
}
/// GPIO 电平值
enum GpioValue {
low,
high;
}
/// GPIO 状态
class GpioStatus {
const GpioStatus({
required this.index,
required this.direction,
required this.value,
required this.enabled,
});
factory GpioStatus.fromMap(Map<dynamic, dynamic> map) {
return GpioStatus(
index: map['index'] as int,
direction:
GpioDirection.values[map['direction'] as int? ?? 0],
value: GpioValue.values[map['value'] as int? ?? 0],
enabled: map['enabled'] as bool? ?? false,
);
}
/// GPIO 编号(从 0 开始)
final int index;
/// 方向
final GpioDirection direction;
/// 电平值
final GpioValue value;
/// 是否已使能
final bool enabled;
Map<String, dynamic> toMap() {
return {
'index': index,
'direction': direction.index,
'value': value.index,
'enabled': enabled,
};
}
@override
String toString() {
return 'GpioStatus(index: $index, direction: $direction, '
'value: $value, enabled: $enabled)';
}
}
/// 串口参数
class SerialParameter {
const SerialParameter({
this.baud = 115200,
this.dataBits = DataBits.bits8,
this.stopBits = StopBits.one,
this.parity = Parity.none,
this.hardwareFlowControl = false,
});
factory SerialParameter.fromMap(Map<dynamic, dynamic> map) {
return SerialParameter(
baud: map['baud'] as int? ?? 115200,
dataBits: DataBits.fromValue(map['dataBits'] as int? ?? 8),
stopBits: StopBits.fromValue(map['stopBits'] as int? ?? 1),
parity: Parity.fromValue(map['parity'] as int? ?? 0),
hardwareFlowControl: map['hardwareFlowControl'] as bool? ?? false,
);
}
/// 波特率
final int baud;
/// 数据位
final DataBits dataBits;
/// 停止位
final StopBits stopBits;
/// 校验位
final Parity parity;
/// 硬件流控
final bool hardwareFlowControl;
Map<String, dynamic> toMap() {
return {
'baud': baud,
'dataBits': dataBits.value,
'stopBits': stopBits.value,
'parity': parity.value,
'hardwareFlowControl': hardwareFlowControl,
};
}
@override
String toString() {
return 'SerialParameter(baud: $baud, dataBits: $dataBits, '
'stopBits: $stopBits, parity: $parity, '
'hardwareFlowControl: $hardwareFlowControl)';
}
SerialParameter copyWith({
int? baud,
DataBits? dataBits,
StopBits? stopBits,
Parity? parity,
bool? hardwareFlowControl,
}) {
return SerialParameter(
baud: baud ?? this.baud,
dataBits: dataBits ?? this.dataBits,
stopBits: stopBits ?? this.stopBits,
parity: parity ?? this.parity,
hardwareFlowControl:
hardwareFlowControl ?? this.hardwareFlowControl,
);
}
}
/// 芯片主频信息
///
/// 对应 WCH API 4.12 节 `getChipMasterFrequency` 返回值。
class ChipMasterFrequency {
const ChipMasterFrequency({
required this.frequency,
required this.switchEnable,
required this.coStatus,
});
factory ChipMasterFrequency.fromMap(Map<dynamic, dynamic> map) {
return ChipMasterFrequency(
frequency: map['frequency'] as int? ?? 0,
switchEnable: map['switchEnable'] as bool? ?? false,
coStatus: map['coStatus'] as int? ?? 0,
);
}
/// 芯片主频Hz
final int frequency;
/// 是否允许切换主频
final bool switchEnable;
/// 晶振状态
final int coStatus;
Map<String, dynamic> toMap() {
return {
'frequency': frequency,
'switchEnable': switchEnable,
'coStatus': coStatus,
};
}
@override
String toString() {
return 'ChipMasterFrequency(frequency: $frequency Hz, '
'switchEnable: $switchEnable, coStatus: $coStatus)';
}
}
/// Modem 状态
class ModemStatus {
const ModemStatus({
this.cts = false,
this.dsr = false,
this.ri = false,
this.dcd = false,
});
factory ModemStatus.fromMap(Map<dynamic, dynamic> map) {
return ModemStatus(
cts: map['cts'] as bool? ?? false,
dsr: map['dsr'] as bool? ?? false,
ri: map['ri'] as bool? ?? false,
dcd: map['dcd'] as bool? ?? false,
);
}
/// Clear To Send
final bool cts;
/// Data Set Ready
final bool dsr;
/// Ring Indicator
final bool ri;
/// Data Carrier Detect
final bool dcd;
Map<String, dynamic> toMap() {
return {
'cts': cts,
'dsr': dsr,
'ri': ri,
'dcd': dcd,
};
}
@override
String toString() {
return 'ModemStatus(CTS: $cts, DSR: $dsr, RI: $ri, DCD: $dcd)';
}
}
/// 串口错误类型
///
/// 对应 WCH 原生库的 `SerialErrorType` 枚举(仅包含 FRAME/PARITY/OVERRUN 三种)。
enum SerialErrorType {
framingError,
parityError,
overrunError;
/// 转换为原生端字符串标识。
String toNativeString() {
switch (this) {
case SerialErrorType.framingError:
return 'SerialErrorType.FramingError';
case SerialErrorType.parityError:
return 'SerialErrorType.ParityError';
case SerialErrorType.overrunError:
return 'SerialErrorType.OverrunError';
}
}
}
/// USB 设备信息
class UsbDeviceInfo {
const UsbDeviceInfo({
required this.deviceName,
required this.productId,
required this.vendorId,
required this.serialCount,
this.chipType,
});
factory UsbDeviceInfo.fromMap(Map<dynamic, dynamic> map) {
return UsbDeviceInfo(
deviceName: map['deviceName'] as String,
productId: map['productId'] as int,
vendorId: map['vendorId'] as int,
serialCount: map['serialCount'] as int? ?? 1,
chipType: map['chipType'] as String?,
);
}
/// 设备名称
final String deviceName;
/// 产品 ID
final int productId;
/// 厂商 ID
final int vendorId;
/// 串口数量
final int serialCount;
/// 芯片型号(如果识别)
final String? chipType;
Map<String, dynamic> toMap() {
return {
'deviceName': deviceName,
'productId': productId,
'vendorId': vendorId,
'serialCount': serialCount,
'chipType': chipType,
};
}
@override
String toString() {
return 'UsbDeviceInfo(name: $deviceName, VID: 0x${vendorId.toRadixString(16).toUpperCase().padLeft(4, '0')}, '
'PID: 0x${productId.toRadixString(16).toUpperCase().padLeft(4, '0')}, '
'ports: $serialCount, chip: $chipType)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is UsbDeviceInfo &&
other.deviceName == deviceName &&
other.productId == productId &&
other.vendorId == vendorId;
}
@override
int get hashCode => Object.hash(deviceName, productId, vendorId);
}

25
pubspec.yaml Normal file
View File

@@ -0,0 +1,25 @@
name: ch34
description: "CH34X"
version: 1.1.0
homepage:
environment:
sdk: '>=3.4.3 <4.0.0'
flutter: '>=3.3.0'
dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter:
plugin:
platforms:
android:
package: com.example.ch34
pluginClass: Ch34Plugin

View File

@@ -0,0 +1,31 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:ch34/src/ch34_method_channel.dart';
import 'package:ch34/src/ch34_platform_interface.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
MethodChannelCh34.registerDefault();
MethodChannelCh34 platform = Ch34Platform.instance as MethodChannelCh34;
const MethodChannel channel = MethodChannel('ch34');
setUp(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
channel,
(MethodCall methodCall) async {
return '42';
},
);
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, null);
});
test('getPlatformVersion', () async {
expect(await platform.getPlatformVersion(), '42');
});
}

205
test/ch34_test.dart Normal file
View File

@@ -0,0 +1,205 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:ch34/ch34.dart';
import 'package:ch34/src/ch34_method_channel.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
class MockCh34Platform
with MockPlatformInterfaceMixin
implements Ch34Platform {
@override
Future<String?> getPlatformVersion() => Future.value('42');
@override
Future<List<UsbDeviceInfo>> enumDevice() => Future.value([]);
@override
Future<String?> getChipType(String deviceName) => Future.value('CH340');
@override
Future<bool> openDevice(String deviceName) => Future.value(true);
@override
Future<bool> requestPermission(String deviceName) => Future.value(true);
@override
void setUsbStateListener(void Function(String, bool) onStateChanged) {}
@override
void removeUsbStateListener() {}
@override
Future<int> getSerialCount(String deviceName) => Future.value(1);
@override
Future<int> getSerialBaud(String deviceName, int serialNumber) =>
Future.value(115200);
@override
Future<ChipMasterFrequency> getChipMasterFrequency(String deviceName) =>
Future.value(const ChipMasterFrequency(
frequency: 12000000,
switchEnable: false,
coStatus: 0,
));
@override
Future<bool> enableSerial(String deviceName, int serialNumber, bool enable) =>
Future.value(true);
@override
Future<bool> setSerialParameter(
String deviceName,
int serialNumber,
SerialParameter parameter,
) => Future.value(true);
@override
Future<int> writeData(
String deviceName,
int serialNumber,
Uint8List data, {
int timeout = 0,
}) => Future.value(data.length);
@override
Future<void> asyncWriteData(
String deviceName,
int serialNumber,
Uint8List data,
) => Future.value();
@override
Future<Uint8List> readData(String deviceName, int serialNumber) =>
Future.value(Uint8List(0));
@override
Future<Uint8List> readDataWithTimeout(
String deviceName,
int serialNumber,
int vTime,
int vMin,
) => Future.value(Uint8List(0));
@override
Future<void> registerDataCallback(
String deviceName,
int serialNumber,
void Function(Uint8List) onData,
) => Future.value();
@override
void removeDataCallback(String deviceName) {}
@override
Future<bool> isConnected(String deviceName) => Future.value(true);
@override
Future<List<String>> getConnectedDevices() => Future.value([]);
@override
Future<void> disconnect(String deviceName) => Future.value();
@override
Future<void> close() => Future.value();
@override
Future<bool> isSupportGpio(String deviceName) => Future.value(false);
@override
Future<int> queryGpioCount(String deviceName) => Future.value(0);
@override
Future<GpioStatus> queryGpioStatus(String deviceName, int gpioIndex) =>
throw const Ch34Exception('Not supported');
@override
Future<List<GpioStatus>> queryAllGpioStatus(String deviceName) =>
Future.value([]);
@override
Future<bool> enableGpio(
String deviceName,
int gpioIndex,
bool enable,
GpioDirection direction,
) => Future.value(false);
@override
Future<bool> setGpioVal(
String deviceName,
int gpioIndex,
GpioValue value,
) => Future.value(false);
@override
Future<GpioValue> getGpioVal(String deviceName, int gpioIndex) =>
Future.value(GpioValue.low);
@override
Future<bool> setDtr(String deviceName, int serialNumber, bool valid) =>
Future.value(true);
@override
Future<bool> setRts(String deviceName, int serialNumber, bool valid) =>
Future.value(true);
@override
Future<bool> setBreakSignal(
String deviceName, int serialNumber, bool valid) =>
Future.value(true);
@override
Future<void> registerModemStatusCallback(
String deviceName,
void Function(ModemStatus) onModemStatus,
) => Future.value();
@override
void removeModemStatusCallback(String deviceName) {}
@override
Future<int> querySerialErrorCount(
String deviceName,
int serialNumber,
SerialErrorType errorType,
) => Future.value(0);
@override
Future<void> setReadTimeout(int timeout) => Future.value();
@override
Future<void> addNewHardware(int vid, int pid, String chipType) => Future.value();
@override
Future<void> setDebug(bool enabled) => Future.value();
@override
Future<bool> isDebugMode() => Future.value(false);
}
void main() {
MethodChannelCh34.registerDefault();
final Ch34Platform initialPlatform = Ch34Platform.instance;
test('$MethodChannelCh34 is the default instance', () {
expect(initialPlatform, isInstanceOf<MethodChannelCh34>());
});
test('getPlatformVersion', () async {
MockCh34Platform fakePlatform = MockCh34Platform();
Ch34Platform.instance = fakePlatform;
expect(await Ch34Manager.getPlatformVersion(), '42');
});
test('enumDevice returns empty list', () async {
MockCh34Platform fakePlatform = MockCh34Platform();
Ch34Platform.instance = fakePlatform;
final devices = await Ch34Manager.enumDevice();
expect(devices, isEmpty);
});
}