commit c000eb12f8504919abddc3f44c33eee3c38cd320 Author: leon <916117771@qq.com> Date: Tue Apr 21 12:57:33 2026 +0800 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b46d36a --- /dev/null +++ b/.gitignore @@ -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/ \ No newline at end of file diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..a77bebb --- /dev/null +++ b/.metadata @@ -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' diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md new file mode 100644 index 0000000..aaf690d --- /dev/null +++ b/.planning/codebase/ARCHITECTURE.md @@ -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` through MethodChannel +7. Dart side deserializes into `List` + +### 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` - 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> 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* diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md new file mode 100644 index 0000000..3900494 --- /dev/null +++ b/.planning/codebase/CONCERNS.md @@ -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* diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md new file mode 100644 index 0000000..a7dcd15 --- /dev/null +++ b/.planning/codebase/CONVENTIONS.md @@ -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 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('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 map) { ... } + Map 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* diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md new file mode 100644 index 0000000..d872ba5 --- /dev/null +++ b/.planning/codebase/INTEGRATIONS.md @@ -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` +- 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* diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md new file mode 100644 index 0000000..607bdd7 --- /dev/null +++ b/.planning/codebase/STACK.md @@ -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* diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md new file mode 100644 index 0000000..b318b7f --- /dev/null +++ b/.planning/codebase/STRUCTURE.md @@ -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* diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md new file mode 100644 index 0000000..d8743fc --- /dev/null +++ b/.planning/codebase/TESTING.md @@ -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()); + }); + + 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 getPlatformVersion() => Future.value('42'); + + @override + Future> 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 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* diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3e13539 --- /dev/null +++ b/CLAUDE.md @@ -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` 代替 +- 所有方法需添加方法级注释 +- 遵循高内聚低耦合设计原则 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b04c571 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# CH34 Flutter \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/analysis_options.yaml @@ -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 diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..161bdcd --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..d5397f5 --- /dev/null +++ b/android/build.gradle @@ -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 + } + } + } +} diff --git a/android/libs/CH34XUartDriver.jar b/android/libs/CH34XUartDriver.jar new file mode 100644 index 0000000..fa2340e Binary files /dev/null and b/android/libs/CH34XUartDriver.jar differ diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..aed0b67 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'ch34' diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..05751e4 --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/android/src/main/java/com/example/ch34/Ch34DataStreamHandler.java b/android/src/main/java/com/example/ch34/Ch34DataStreamHandler.java new file mode 100644 index 0000000..cc24b3c --- /dev/null +++ b/android/src/main/java/com/example/ch34/Ch34DataStreamHandler.java @@ -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 wchCallbacks = new HashMap<>(); + + /** 数据回调接口,用于桥接 WCH 回调到 EventChannel */ + public interface DataBridge { + void onData(int serialNumber, byte[] buffer, int length); + } + + /** 设备名 -> 数据桥接回调 */ + private final Map 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); + } +} diff --git a/android/src/main/java/com/example/ch34/Ch34ModemStreamHandler.java b/android/src/main/java/com/example/ch34/Ch34ModemStreamHandler.java new file mode 100644 index 0000000..d6e5883 --- /dev/null +++ b/android/src/main/java/com/example/ch34/Ch34ModemStreamHandler.java @@ -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 status) { + if (!isListening) return; + mainHandler.post(() -> { + EventChannel.EventSink sink = eventSink; + if (sink != null) { + sink.success(status); + } + }); + } +} diff --git a/android/src/main/java/com/example/ch34/Ch34Plugin.java b/android/src/main/java/com/example/ch34/Ch34Plugin.java new file mode 100644 index 0000000..f551376 --- /dev/null +++ b/android/src/main/java/com/example/ch34/Ch34Plugin.java @@ -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.3–4.38 共 36 个公开 API(getInstance/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 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 devices = manager.enumDevice(); + List> 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 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 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 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 statuses = manager.queryAllGPIOStatus(device); + List> 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 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 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; + } +} diff --git a/android/src/main/java/com/example/ch34/Ch34TypeConverter.java b/android/src/main/java/com/example/ch34/Ch34TypeConverter.java new file mode 100644 index 0000000..7d3c3d7 --- /dev/null +++ b/android/src/main/java/com/example/ch34/Ch34TypeConverter.java @@ -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 gpioStatusToMap( + cn.wch.uartlib.base.gpio.GPIO_Status status, int gpioIndex) { + Map 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); + } + } +} diff --git a/android/src/main/java/com/example/ch34/Ch34UsbStateStreamHandler.java b/android/src/main/java/com/example/ch34/Ch34UsbStateStreamHandler.java new file mode 100644 index 0000000..d16a91a --- /dev/null +++ b/android/src/main/java/com/example/ch34/Ch34UsbStateStreamHandler.java @@ -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 map = new HashMap<>(); + map.put("deviceName", deviceName); + map.put("connected", connected); + sink.success(map); + } + }); + } +} diff --git a/android/src/test/java/com/example/ch34/Ch34PluginTest.java b/android/src/test/java/com/example/ch34/Ch34PluginTest.java new file mode 100644 index 0000000..29b7403 --- /dev/null +++ b/android/src/test/java/com/example/ch34/Ch34PluginTest.java @@ -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); + } +} diff --git a/docs/CH34X-api_docs.md b/docs/CH34X-api_docs.md new file mode 100644 index 0000000..7d3fcec --- /dev/null +++ b/docs/CH34X-api_docs.md @@ -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 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
cn.wch.uartlib.exception.NoPermissionException
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 - 上下文
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 设备
serialNumber - 串口号
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 设备
serialNumber - 串口号
baud - 波特率
dataBit - 数据位 5,6,7,8
stopBit - 停止位 1,2
parityBit - 校验位 0 NONE; 1 ODD; 2 EVEN; 3 MARK; 4 SPACE
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 设备
serialNumber - 串口号 | +| 返回 | 大于 0:串口波特率;小于 0:出错 | +| 抛出 | java.lang.Exception | + +--- + +### 4.12 getChipMasterFrequency + +```java +public ChipMasterFrequency getChipMasterFrequency(@NonNull UsbDevice + usbDevice) throws Exception +``` + +获取芯片主频(实际仅对 CH9114 系列有效,其他类型设备无影响)。 + +| 项目 | 说明 | +|------|------| +| 参数 | usbDevice - USB 设备 | +| 返回 | ChipMasterFrequency 对象:
- int frequency 频率
- Boolean switchEnable 是否允许切换
- 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 设备
serialNumber - 串口号
data - 将要发送的数据
length - 将要发送数据的长度
timeout - 超时时间 | +| 返回 | 发送成功返回数据的长度 | +| 抛出 | java.lang.Exception | + +--- + +### 4.14 asyncWriteData + +```java +public void asyncWriteData(@NonNull UsbDevice usbDevice, int serialNumber, + byte[] data) throws Exception +``` + +发送串口数据(异步发送,将数据加入缓存队列发送,不能返回状态和结果)。 + +| 项目 | 说明 | +|------|------| +| 参数 | usbDevice - USB 设备
serialNumber - 串口号
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 设备
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>0,vMin>0 时,read 函数将阻塞直到读到最后一个字符时开始计时,超过 vTime 时间后,如果接收到的数据不足 vMin 字节,则返回。否则继续等待直到收到 vMin 个字节或超时。 +2. 当 vTime>0,vMin=0 时,read 函数立即返回,超时为每个字符等待 vTime 时间。 +3. 当 vTime=0,vMin>0 时,read 函数一直阻塞,直到读到 vMin 个字节后返回。 + +| 项目 | 说明 | +|------|------| +| 参数 | usbDevice - USB 设备
serialNumber - 串口号
vTime - 等待时间
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 设备
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 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 设备
gpioIndex - GPIO 序号,从 0 开始 | +| 返回 | GPIO 状态 | +| 抛出 | java.lang.Exception | + +--- + +### 4.26 queryAllGPIOStatus + +```java +public List 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 设备
gpioIndex - GPIO 序号
enable - true 打开;false 关闭
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 设备
gpioIndex - GPIO 序号
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 设备
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 设备
serialNumber - 串口号
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 设备
serialNumber - 串口号
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 设备
serialNumber - 串口号
valid - 是否有效(低电平有效) | +| 返回 | true 操作成功;false 操作失败 | +| 抛出 | java.lang.Exception | + +--- + +### 4.33 registerModemStatusCallback + +```java +public void registerModemStatusCallback(@NonNull UsbDevice usbDevice, IModemStatus + modemStatus) throws Exception +``` + +注册 Modem 输入信号状态的回调。 + +| 项目 | 说明 | +|------|------| +| 参数 | device - USB 设备
modemStatus - 状态回调 | +| 抛出 | java.lang.Exception | + +--- + +### 4.34 querySerialErrorCount + +```java +public int querySerialErrorCount(@NonNull UsbDevice usbDevice, int + serialNumber, @NonNull SerialErrorType errorType) throws Exception +``` + +查询串口错误状态。 + +| 项目 | 说明 | +|------|------| +| 参数 | device - USB 设备
serialNumber - 串口号
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
pid - 硬件 pid
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 diff --git a/docs/USAGE_GUIDE.md b/docs/USAGE_GUIDE.md new file mode 100644 index 0000000..980dd2d --- /dev/null +++ b/docs/USAGE_GUIDE.md @@ -0,0 +1,1265 @@ +# CH34 Flutter 插件使用手册 + +> 版本: 1.0.0 +> 支持的芯片: CH340/CH341/CH342/CH343/CH344/CH347/CH9101/CH9102/CH9103/CH9104/CH9143 + +--- + +## 目录 + +1. [简介](#一简介) +2. [安装与配置](#二安装与配置) +3. [快速开始](#三快速开始) +4. [API 参考](#四api-参考) +5. [类型定义](#五类型定义) +6. [完整示例](#六完整示例) +7. [常见问题](#七常见问题) + +--- + +## 一、简介 + +`ch34` 是一个 Flutter 插件,为 WCH CH34X 系列 USB 转串口芯片提供 Flutter 接口支持。通过该插件,Flutter 应用可以与 CH34X 芯片进行串口通信。 + +### 支持的芯片型号 + +| 芯片型号 | 说明 | +|---------|------| +| CH340 | 单串口 USB 转串口芯片 | +| CH341 | 并口/串口 USB 转换芯片 | +| CH342 | 双串口 USB 转串口芯片 | +| CH343 | 增强型单串口芯片 | +| CH344 | 四串口 USB 转串口芯片 | +| CH347 | 高速 USB 转串口芯片 | +| CH9101 | 单串口 USB 转串口芯片 | +| CH9102 | 双串口 USB 转串口芯片 | +| CH9103 | 四串口 USB 转串口芯片 | +| CH9104 | 八串口 USB 转串口芯片 | +| CH9143 | 多串口 USB 转串口芯片 | + +### 系统要求 + +- Android 4.4 及以上版本 +- 支持 USB Host 或 OTG 功能的 Android 设备 + +--- + +## 二、安装与配置 + +### 2.1 添加依赖 + +在 `pubspec.yaml` 中添加: + +```yaml +dependencies: + ch34: ^1.0.0 +``` + +### 2.2 导入插件 + +```dart +import 'package:ch34/ch34.dart'; +``` + +### 2.3 初始化配置 + +在应用启动时进行初始化配置: + +```dart +// 开启调试模式(可选,默认关闭) +await Ch34Manager.setDebug(true); + +// 设置全局读取超时时间(可选,默认 0 表示异步传输) +await Ch34Manager.setReadTimeout(1000); // 1000ms +``` + +--- + +## 三、快速开始 + +### 3.1 基本使用流程 + +```dart +// 1. 枚举可用设备 +final devices = await Ch34Manager.enumDevice(); +if (devices.isEmpty) { + print('未找到 CH34X 设备'); + return; +} + +// 2. 选择设备 +final device = devices.first; +print('找到设备: ${device.deviceName}, 芯片: ${device.chipType}'); + +// 3. 打开设备 +final opened = await Ch34Manager.openDevice(device.deviceName); +if (!opened) { + print('打开设备失败'); + return; +} + +// 4. 设置串口参数 +await Ch34Manager.setSerialParameter( + device.deviceName, + 0, // 串口号(从 0 开始) + const SerialParameter( + baud: 9600, + dataBits: DataBits.bits8, + stopBits: StopBits.one, + parity: Parity.none, + hardwareFlowControl: false, + ), +); + +// 5. 注册数据回调 +Ch34Manager.registerDataCallback( + device.deviceName, + 0, + (data) { + print('收到数据: $data'); + }, +); + +// 6. 发送数据 +await Ch34Manager.writeData( + device.deviceName, + 0, + Uint8List.fromList([0x01, 0x02, 0x03]), +); + +// 7. 使用完毕后断开 +await Ch34Manager.disconnect(device.deviceName); +``` + +### 3.2 权限处理 + +```dart +// 请求 USB 设备权限 +final granted = await Ch34Manager.requestPermission(device.deviceName); +if (!granted) { + print('USB 权限被拒绝'); + return; +} +``` + +--- + +## 四、API 参考 + +### 4.1 基础方法 + +#### getPlatformVersion + +获取平台版本。 + +```dart +static Future getPlatformVersion() +``` + +**返回值**: 平台版本字符串 + +--- + +### 4.2 设备枚举与识别 + +#### enumDevice + +枚举当前所有可用的 USB 设备。 + +```dart +static Future> enumDevice() +``` + +**返回值**: 可用 USB 设备列表 +**异常**: `Ch34Exception` - 枚举失败时抛出 + +#### getChipType + +获取指定设备的芯片型号。 + +```dart +static Future getChipType(String deviceName) +``` + +**参数**: +- `deviceName` - 设备名称 + +**返回值**: 芯片型号字符串,`null` 表示无法识别 + +--- + +### 4.3 设备打开与权限 + +#### openDevice + +打开 USB 设备。 + +```dart +static Future openDevice(String deviceName) +``` + +**参数**: +- `deviceName` - 设备名称 + +**返回值**: `true` 成功,`false` 失败 + +#### requestPermission + +申请 USB 设备的权限。 + +```dart +static Future requestPermission(String deviceName) +``` + +**参数**: +- `deviceName` - 设备名称 + +**返回值**: `true` 已授权,`false` 被拒绝 + +--- + +### 4.4 USB 状态监听 + +#### setUsbStateListener + +注册 USB 设备插拔状态监听。 + +```dart +static void setUsbStateListener( + void Function(String deviceName, bool connected) onStateChanged, +) +``` + +**参数**: +- `onStateChanged` - 状态变化回调函数 + +#### removeUsbStateListener + +移除 USB 状态监听。 + +```dart +static void removeUsbStateListener() +``` + +--- + +### 4.5 串口信息 + +#### getSerialCount + +获取设备的串口数目。 + +```dart +static Future getSerialCount(String deviceName) +``` + +**参数**: +- `deviceName` - 设备名称 + +**返回值**: 串口数目,`-1` 表示读取芯片型号失败 + +#### getSerialBaud + +获取串口波特率(仅 CH9114 系列有效)。 + +```dart +static Future getSerialBaud(String deviceName, int serialNumber) +``` + +**参数**: +- `deviceName` - 设备名称 +- `serialNumber` - 串口号 + +**返回值**: 大于 0 表示波特率,小于 0 表示出错 + +#### getChipMasterFrequency + +获取芯片主频(仅 CH9114 系列有效)。 + +```dart +static Future getChipMasterFrequency(String deviceName) +``` + +**参数**: +- `deviceName` - 设备名称 + +**返回值**: 芯片主频信息对象 + +#### enableSerial + +打开或关闭串口(仅 CH9114 系列有效)。 + +```dart +static Future enableSerial( + String deviceName, + int serialNumber, + bool enable, +) +``` + +**参数**: +- `deviceName` - 设备名称 +- `serialNumber` - 串口号 +- `enable` - `true` 打开,`false` 关闭 + +**返回值**: `true` 设置成功,`false` 设置失败 + +--- + +### 4.6 串口参数设置 + +#### setSerialParameter + +设置串口参数。 + +```dart +static Future setSerialParameter( + String deviceName, + int serialNumber, + SerialParameter parameter, +) +``` + +**参数**: +- `deviceName` - 设备名称 +- `serialNumber` - 串口号(从 0 开始) +- `parameter` - 串口参数配置 + +**返回值**: `true` 设置成功,`false` 设置失败 + +--- + +### 4.7 数据读写 + +#### writeData + +发送串口数据(同步发送)。 + +```dart +static Future writeData( + String deviceName, + int serialNumber, + Uint8List data, { + int timeout = 0, +}) +``` + +**参数**: +- `deviceName` - 设备名称 +- `serialNumber` - 串口号 +- `data` - 要发送的数据 +- `timeout` - 超时时间(毫秒),0 表示不超时 + +**返回值**: 实际发送的字节数 + +#### asyncWriteData + +发送串口数据(异步发送)。 + +```dart +static Future asyncWriteData( + String deviceName, + int serialNumber, + Uint8List data, +) +``` + +**参数**: +- `deviceName` - 设备名称 +- `serialNumber` - 串口号 +- `data` - 要发送的数据 + +**说明**: 将数据加入缓存持续发送,不返回状态和结果 + +#### readData + +阻塞读取串口数据。 + +```dart +static Future readData( + String deviceName, + int serialNumber, +) +``` + +**参数**: +- `deviceName` - 设备名称 +- `serialNumber` - 串口号 + +**返回值**: 读取到的数据 + +#### readDataWithTimeout + +主动读取串口数据(带超时参数)。 + +```dart +static Future readDataWithTimeout( + String deviceName, + int serialNumber, + int vTime, + int vMin, +) +``` + +**参数**: +- `deviceName` - 设备名称 +- `serialNumber` - 串口号 +- `vTime` - 等待时间(毫秒) +- `vMin` - 读取的最小字节数 + +**返回值**: 读取到的数据 + +**读取行为说明**: +- `vTime>0, vMin>0`: 阻塞直到读取到第一个字符后开始计时,时间到或已读够 vMin 个字符则返回 +- `vTime>0, vMin=0`: 读到数据立即返回,否则最多等待 vTime +- `vTime=0, vMin>0`: 一直阻塞直到读到 vMin 个字符后返回 + +#### registerDataCallback + +注册串口数据回调。 + +```dart +static Future registerDataCallback( + String deviceName, + int serialNumber, + void Function(Uint8List data) onData, +) +``` + +**参数**: +- `deviceName` - 设备名称 +- `serialNumber` - 串口号 +- `onData` - 数据接收回调函数 + +**说明**: 注册后数据自动推送,不需要主动调用 `readData`。**推荐使用此方式接收数据**。 + +#### removeDataCallback + +取消注册串口数据回调。 + +```dart +static void removeDataCallback(String deviceName) +``` + +**参数**: +- `deviceName` - 设备名称 + +--- + +### 4.8 连接状态 + +#### isConnected + +判断 USB 设备是否已经连接。 + +```dart +static Future isConnected(String deviceName) +``` + +**参数**: +- `deviceName` - 设备名称 + +**返回值**: `true` 已连接,`false` 未连接 + +#### getConnectedDevices + +获取当前已经打开的设备列表。 + +```dart +static Future> getConnectedDevices() +``` + +**返回值**: 已打开的设备名称列表 + +--- + +### 4.9 断开与关闭 + +#### disconnect + +断开 USB 设备的连接。 + +```dart +static Future disconnect(String deviceName) +``` + +**参数**: +- `deviceName` - 设备名称 + +#### close + +释放资源,关闭所有串口设备。 + +```dart +static Future close() +``` + +--- + +### 4.10 GPIO 功能 + +#### isSupportGpio + +查询设备是否支持 GPIO 功能。 + +```dart +static Future isSupportGpio(String deviceName) +``` + +**参数**: +- `deviceName` - 设备名称 + +**返回值**: `true` 支持,`false` 不支持 + +#### queryGpioCount + +查询该 USB 设备的 GPIO 数目。 + +```dart +static Future queryGpioCount(String deviceName) +``` + +**参数**: +- `deviceName` - 设备名称 + +**返回值**: GPIO 数目 + +#### queryGpioStatus + +查询指定 GPIO 的状态。 + +```dart +static Future queryGpioStatus( + String deviceName, + int gpioIndex, +) +``` + +**参数**: +- `deviceName` - 设备名称 +- `gpioIndex` - GPIO 编号(从 0 开始) + +**返回值**: GPIO 状态 + +#### queryAllGpioStatus + +查询所有 GPIO 状态。 + +```dart +static Future> queryAllGpioStatus(String deviceName) +``` + +**参数**: +- `deviceName` - 设备名称 + +**返回值**: 全部 GPIO 状态列表 + +#### enableGpio + +使能指定 GPIO。 + +```dart +static Future enableGpio( + String deviceName, + int gpioIndex, + bool enable, + GpioDirection direction, +) +``` + +**参数**: +- `deviceName` - 设备名称 +- `gpioIndex` - GPIO 编号 +- `enable` - `true` 使能,`false` 关闭 +- `direction` - GPIO 方向 + +**返回值**: `true` 使能成功,`false` 使能失败 + +#### setGpioVal + +设置指定 GPIO 的电平值。 + +```dart +static Future setGpioVal( + String deviceName, + int gpioIndex, + GpioValue value, +) +``` + +**参数**: +- `deviceName` - 设备名称 +- `gpioIndex` - GPIO 编号 +- `value` - GPIO 电平值 + +**返回值**: `true` 设置成功,`false` 设置失败 + +#### getGpioVal + +获取指定 GPIO 的电平值。 + +```dart +static Future getGpioVal( + String deviceName, + int gpioIndex, +) +``` + +**参数**: +- `deviceName` - 设备名称 +- `gpioIndex` - GPIO 编号 + +**返回值**: GPIO 电平值 + +--- + +### 4.11 信号控制 + +#### setDtr + +设置 DTR 信号。 + +```dart +static Future setDtr( + String deviceName, + int serialNumber, + bool valid, +) +``` + +**参数**: +- `deviceName` - 设备名称 +- `serialNumber` - 串口号 +- `valid` - 是否有效(低电平有效) + +**返回值**: `true` 设置成功,`false` 设置失败 + +#### setRts + +设置 RTS 信号。 + +```dart +static Future setRts( + String deviceName, + int serialNumber, + bool valid, +) +``` + +**参数**: +- `deviceName` - 设备名称 +- `serialNumber` - 串口号 +- `valid` - 是否有效(低电平有效) + +**返回值**: `true` 设置成功,`false` 设置失败 + +#### setBreakSignal + +设置 Break 信号。 + +```dart +static Future setBreakSignal( + String deviceName, + int serialNumber, + bool valid, +) +``` + +**参数**: +- `deviceName` - 设备名称 +- `serialNumber` - 串口号 +- `valid` - 是否有效(低电平有效) + +**返回值**: `true` 设置成功,`false` 设置失败 + +--- + +### 4.12 Modem 状态回调 + +#### registerModemStatusCallback + +注册 Modem 控制信号状态回调。 + +```dart +static Future registerModemStatusCallback( + String deviceName, + void Function(ModemStatus status) onModemStatus, +) +``` + +**参数**: +- `deviceName` - 设备名称 +- `onModemStatus` - Modem 状态变化回调 + +#### removeModemStatusCallback + +移除 Modem 状态回调。 + +```dart +static void removeModemStatusCallback(String deviceName) +``` + +**参数**: +- `deviceName` - 设备名称 + +--- + +### 4.13 错误查询 + +#### querySerialErrorCount + +查询串口错误状态。 + +```dart +static Future querySerialErrorCount( + String deviceName, + int serialNumber, + SerialErrorType errorType, +) +``` + +**参数**: +- `deviceName` - 设备名称 +- `serialNumber` - 串口号 +- `errorType` - 错误类型 + +**返回值**: 该种错误出现的次数 + +--- + +### 4.14 全局配置 + +#### setReadTimeout + +设置读取超时时间。 + +```dart +static Future setReadTimeout(int timeout) +``` + +**参数**: +- `timeout` - 超时时间(毫秒) + +**说明**: 全局有效,应在 APP 初始化时调用 + +#### addNewHardware + +添加自定义硬件 VID/PID。 + +```dart +static Future addNewHardware( + int vid, + int pid, { + String? chipType, +}) +``` + +**参数**: +- `vid` - 硬件 VID +- `pid` - 硬件 PID +- `chipType` - 芯片类型(可选) + +#### setDebug + +设置调试模式。 + +```dart +static Future setDebug(bool enabled) +``` + +**参数**: +- `enabled` - `true` 开启,`false` 关闭 + +#### isDebugMode + +返回当前是否处于调试模式。 + +```dart +static Future isDebugMode() +``` + +**返回值**: `true` 处于调试模式,`false` 不处于 + +--- + +## 五、类型定义 + +### 5.1 枚举类型 + +#### DataBits - 数据位 + +```dart +enum DataBits { + bits5(5), // 5 位数据位 + bits6(6), // 6 位数据位 + bits7(7), // 7 位数据位 + bits8(8); // 8 位数据位(默认) +} +``` + +#### StopBits - 停止位 + +```dart +enum StopBits { + one(1), // 1 位停止位(默认) + two(2), // 2 位停止位 +} +``` + +#### Parity - 校验位 + +```dart +enum Parity { + none(0), // 无校验(默认) + odd(1), // 奇校验 + even(2), // 偶校验 + mark(3), // 标记校验 + space(4), // 空格校验 +} +``` + +#### GpioDirection - GPIO 方向 + +```dart +enum GpioDirection { + inDir, // 输入方向 + outDir, // 输出方向 +} +``` + +#### GpioValue - GPIO 电平值 + +```dart +enum GpioValue { + low, // 低电平 + high, // 高电平 +} +``` + +#### SerialErrorType - 串口错误类型 + +```dart +enum SerialErrorType { + framingError, // 帧错误 + parityError, // 校验错误 + overrunError, // 溢出错误 + breakInterrupt, // 中断错误 +} +``` + +### 5.2 数据类 + +#### SerialParameter - 串口参数 + +```dart +class SerialParameter { + const SerialParameter({ + this.baud = 115200, // 波特率 + this.dataBits = DataBits.bits8, // 数据位 + this.stopBits = StopBits.one, // 停止位 + this.parity = Parity.none, // 校验位 + this.hardwareFlowControl = false, // 硬件流控 + }); +} +``` + +**常用波特率**: 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200 + +#### UsbDeviceInfo - USB 设备信息 + +```dart +class UsbDeviceInfo { + final String deviceName; // 设备名称 + final int productId; // 产品 ID (PID) + final int vendorId; // 厂商 ID (VID) + final int serialCount; // 串口数量 + final String? chipType; // 芯片型号(如果识别) +} +``` + +#### GpioStatus - GPIO 状态 + +```dart +class GpioStatus { + final int index; // GPIO 编号 + final GpioDirection direction; // 方向 + final GpioValue value; // 电平值 + final bool enabled; // 是否已使能 +} +``` + +#### ChipMasterFrequency - 芯片主频 + +```dart +class ChipMasterFrequency { + final int frequency; // 芯片主频(Hz) + final bool switchEnable; // 是否允许切换主频 + final int coStatus; // 晶振状态 +} +``` + +#### ModemStatus - Modem 状态 + +```dart +class ModemStatus { + final bool cts; // Clear To Send + final bool dsr; // Data Set Ready + final bool ri; // Ring Indicator + final bool dcd; // Data Carrier Detect +} +``` + +--- + +## 六、完整示例 + +### 6.1 设备扫描与管理 + +```dart +import 'package:ch34/ch34.dart'; +import 'dart:typed_data'; + +class SerialDeviceManager { + static List _devices = []; + + /// 扫描并连接设备 + static Future scanAndConnect() async { + try { + // 获取已连接设备 + final connected = await Ch34Manager.getConnectedDevices(); + print('已连接设备: $connected'); + + // 枚举所有可用设备 + _devices = await Ch34Manager.enumDevice(); + print('发现 ${_devices.length} 个设备'); + + for (final device in _devices) { + print('设备: ${device.deviceName}'); + print(' VID: 0x${device.vendorId.toRadixString(16).toUpperCase().padLeft(4, '0')}'); + print(' PID: 0x${device.productId.toRadixString(16).toUpperCase().padLeft(4, '0')}'); + print(' 芯片: ${device.chipType ?? "未知"}'); + print(' 串口数: ${device.serialCount}'); + } + } on Ch34Exception catch (e) { + print('设备扫描失败: $e'); + } + } +} +``` + +### 6.2 串口通信封装 + +```dart +class SerialPortService { + String? _deviceName; + int _serialNumber = 0; + bool _isConnected = false; + + /// 打开串口 + Future open(String deviceName, {int baudRate = 9600}) async { + try { + // 请求权限 + final granted = await Ch34Manager.requestPermission(deviceName); + if (!granted) { + return false; + } + + // 打开设备 + final opened = await Ch34Manager.openDevice(deviceName); + if (!opened) { + return false; + } + + // 设置参数 + final configured = await Ch34Manager.setSerialParameter( + deviceName, + _serialNumber, + SerialParameter( + baud: baudRate, + dataBits: DataBits.bits8, + stopBits: StopBits.one, + parity: Parity.none, + ), + ); + + if (!configured) { + await Ch34Manager.disconnect(deviceName); + return false; + } + + _deviceName = deviceName; + _isConnected = true; + return true; + } catch (e) { + print('打开串口失败: $e'); + return false; + } + } + + /// 关闭串口 + Future close() async { + if (_deviceName != null) { + Ch34Manager.removeDataCallback(_deviceName!); + await Ch34Manager.disconnect(_deviceName!); + _deviceName = null; + _isConnected = false; + } + } + + /// 发送数据 + Future send(List data) async { + if (!_isConnected || _deviceName == null) { + throw StateError('串口未连接'); + } + + return await Ch34Manager.writeData( + _deviceName!, + _serialNumber, + Uint8List.fromList(data), + ); + } + + /// 异步发送数据 + Future sendAsync(List data) async { + if (!_isConnected || _deviceName == null) { + throw StateError('串口未连接'); + } + + await Ch34Manager.asyncWriteData( + _deviceName!, + _serialNumber, + Uint8List.fromList(data), + ); + } + + /// 注册数据接收回调 + Future onData(void Function(Uint8List data) callback) async { + if (_deviceName == null) { + throw StateError('串口未连接'); + } + + await Ch34Manager.registerDataCallback( + _deviceName!, + _serialNumber, + callback, + ); + } + + /// 获取连接状态 + bool get isConnected => _isConnected; +} +``` + +### 6.3 GPIO 控制示例 + +```dart +class GpioController { + /// 配置并设置 GPIO + static Future configureGpio( + String deviceName, + int gpioIndex, + ) async { + // 检查是否支持 GPIO + final supported = await Ch34Manager.isSupportGpio(deviceName); + if (!supported) { + print('设备不支持 GPIO 功能'); + return; + } + + // 查询 GPIO 数量 + final count = await Ch34Manager.queryGpioCount(deviceName); + if (gpioIndex >= count) { + print('GPIO 编号超出范围'); + return; + } + + // 使能 GPIO(输出方向) + final enabled = await Ch34Manager.enableGpio( + deviceName, + gpioIndex, + true, + GpioDirection.outDir, + ); + + if (!enabled) { + print('使能 GPIO 失败'); + return; + } + + // 设置高电平 + await Ch34Manager.setGpioVal( + deviceName, + gpioIndex, + GpioValue.high, + ); + + // 读取电平值验证 + final value = await Ch34Manager.getGpioVal(deviceName, gpioIndex); + print('GPIO $gpioIndex 电平: $value'); + } + + /// 查询所有 GPIO 状态 + static Future printAllGpioStatus(String deviceName) async { + final statuses = await Ch34Manager.queryAllGpioStatus(deviceName); + for (final status in statuses) { + print('GPIO ${status.index}: ' + '方向=${status.direction}, ' + '电平=${status.value}, ' + '使能=${status.enabled}'); + } + } +} +``` + +### 6.4 Modem 状态监听 + +```dart +class ModemMonitor { + static Future startMonitoring(String deviceName) async { + await Ch34Manager.registerModemStatusCallback( + deviceName, + (ModemStatus status) { + print('Modem 状态变化:'); + print(' CTS: ${status.cts}'); + print(' DSR: ${status.dsr}'); + print(' RI: ${status.ri}'); + print(' DCD: ${status.dcd}'); + }, + ); + } + + static void stopMonitoring(String deviceName) { + Ch34Manager.removeModemStatusCallback(deviceName); + } +} +``` + +### 6.5 错误检测 + +```dart +class ErrorDetector { + static Future checkErrors(String deviceName, int serialNumber) async { + final framingErrors = await Ch34Manager.querySerialErrorCount( + deviceName, + serialNumber, + SerialErrorType.framingError, + ); + + final parityErrors = await Ch34Manager.querySerialErrorCount( + deviceName, + serialNumber, + SerialErrorType.parityError, + ); + + print('帧错误: $framingErrors, 校验错误: $parityErrors'); + } +} +``` + +--- + +## 七、常见问题 + +### 7.1 找不到设备 + +**问题**: `enumDevice()` 返回空列表 + +**解决方法**: +1. 确认设备支持 USB Host/OTG 功能 +2. 检查 USB 线缆连接是否正常 +3. 确认 Android 设备已开启 OTG 功能(部分手机需要手动开启) +4. 检查芯片是否在支持列表中 + +### 7.2 权限被拒绝 + +**问题**: `requestPermission()` 返回 `false` + +**解决方法**: +1. 确保在 `AndroidManifest.xml` 中添加了 USB 权限声明 +2. 用户需要在系统弹窗中点击"允许" +3. 可以尝试重新请求权限 + +### 7.3 数据收发异常 + +**问题**: 发送数据成功但收不到回复 + +**解决方法**: +1. 确认串口参数(波特率、数据位、停止位、校验位)与对端设备一致 +2. 使用 `registerDataCallback` 代替 `readData`,推荐回调方式接收数据 +3. 检查线缆连接和电平匹配(3.3V vs 5V) +4. 使用 `querySerialErrorCount` 检查是否有串口错误 + +### 7.4 多串口设备使用 + +**问题**: CH344/CH9104 等多串口设备如何使用 + +**解决方法**: +```dart +// 多串口设备每个串口都需要单独配置 +for (int i = 0; i < device.serialCount; i++) { + await Ch34Manager.setSerialParameter( + device.deviceName, + i, // 不同的串口号 + const SerialParameter(baud: 9600), + ); + + Ch34Manager.registerDataCallback( + device.deviceName, + i, + (data) { + print('串口 $i 收到数据: $data'); + }, + ); +} +``` + +### 7.5 设备热插拔处理 + +**问题**: 设备拔出后如何检测 + +**解决方法**: +```dart +// 注册 USB 状态监听 +Ch34Manager.setUsbStateListener((deviceName, connected) { + if (!connected) { + print('设备 $deviceName 已拔出'); + // 清理资源 + Ch34Manager.removeDataCallback(deviceName); + } else { + print('设备 $deviceName 已插入'); + // 重新打开设备 + } +}); +``` + +### 7.6 自定义 VID/PID + +**问题**: 如何支持非标准 VID/PID 的设备 + +**解决方法**: +```dart +// 添加自定义硬件 +await Ch34Manager.addNewHardware( + 0x1234, // 自定义 VID + 0x5678, // 自定义 PID + chipType: 'CH340', // 可选:指定芯片类型 +); +``` + +--- + +## 附录:通信通道说明 + +插件内部使用以下通道与 Android 原生端通信: + +| 通道类型 | 名称 | 用途 | +|---------|------|------| +| MethodChannel | `ch34` | 双向方法调用 | +| EventChannel | `ch34/data` | 串口数据推送 | +| EventChannel | `ch34/modem` | Modem 状态变化 | +| EventChannel | `ch34/usb_state` | USB 插拔状态 | + +--- + +*文档版本: 1.0.0* +*插件版本: 1.0.0* diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..29a3a50 --- /dev/null +++ b/example/.gitignore @@ -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 diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..a820e25 --- /dev/null +++ b/example/README.md @@ -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. diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/example/analysis_options.yaml @@ -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 diff --git a/example/android/.gitignore b/example/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/example/android/.gitignore @@ -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 diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle new file mode 100644 index 0000000..7056dd4 --- /dev/null +++ b/example/android/app/build.gradle @@ -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 = "../.." +} diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..fb3d65a --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/android/app/src/main/java/com/example/ch34_example/MainActivity.java b/example/android/app/src/main/java/com/example/ch34_example/MainActivity.java new file mode 100644 index 0000000..64ed4e1 --- /dev/null +++ b/example/android/app/src/main/java/com/example/ch34_example/MainActivity.java @@ -0,0 +1,6 @@ +package com.example.ch34_example; + +import io.flutter.embedding.android.FlutterActivity; + +public class MainActivity extends FlutterActivity { +} diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle new file mode 100644 index 0000000..d2ffbff --- /dev/null +++ b/example/android/build.gradle @@ -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 +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties new file mode 100644 index 0000000..3b5b324 --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..348c409 --- /dev/null +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/example/android/settings.gradle b/example/android/settings.gradle new file mode 100644 index 0000000..dbf9ff3 --- /dev/null +++ b/example/android/settings.gradle @@ -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" diff --git a/example/integration_test/plugin_integration_test.dart b/example/integration_test/plugin_integration_test.dart new file mode 100644 index 0000000..c5ad9e8 --- /dev/null +++ b/example/integration_test/plugin_integration_test.dart @@ -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); + }); +} diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..5231180 --- /dev/null +++ b/example/lib/main.dart @@ -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 createState() => _MyAppState(); +} + +class _MyAppState extends State { + String _platformVersion = 'Unknown'; + List _devices = []; + String _status = '未连接'; + String _receivedData = ''; + + @override + void initState() { + super.initState(); + initPlatformState(); + } + + Future 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 _scanDevices() async { + try { + final devices = await Ch34Manager.enumDevice(); + setState(() { + _devices = devices; + _status = '发现 ${devices.length} 个设备'; + }); + } catch (e) { + setState(() { + _status = '扫描失败: $e'; + }); + } + } + + Future _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 _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 _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, + ), + ], + ), + ); + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock new file mode 100644 index 0000000..c7043eb --- /dev/null +++ b/example/pubspec.lock @@ -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" diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..47ec0bc --- /dev/null +++ b/example/pubspec.yaml @@ -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 diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart new file mode 100644 index 0000000..7e1f80b --- /dev/null +++ b/example/test/widget_test.dart @@ -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, + ); + }); +} diff --git a/lib/ch34.dart b/lib/ch34.dart new file mode 100644 index 0000000..74fe51c --- /dev/null +++ b/lib/ch34.dart @@ -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; + } + } +} diff --git a/lib/ch34_method_channel.dart b/lib/ch34_method_channel.dart new file mode 100644 index 0000000..f984df5 --- /dev/null +++ b/lib/ch34_method_channel.dart @@ -0,0 +1,5 @@ +/// 顶层导出,保持向后兼容。 +/// 所有实现已迁移到 `src/` 目录下。 +library ch34_method_channel; + +export 'src/ch34_method_channel.dart'; diff --git a/lib/ch34_platform_interface.dart b/lib/ch34_platform_interface.dart new file mode 100644 index 0000000..7cc6f3b --- /dev/null +++ b/lib/ch34_platform_interface.dart @@ -0,0 +1,5 @@ +/// 顶层导出,保持向后兼容。 +/// 所有实现已迁移到 `src/` 目录下。 +library ch34_platform_interface; + +export 'src/ch34_platform_interface.dart'; diff --git a/lib/src/ch34_manager.dart b/lib/src/ch34_manager.dart new file mode 100644 index 0000000..b19d052 --- /dev/null +++ b/lib/src/ch34_manager.dart @@ -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 getPlatformVersion() { + return Ch34Platform.instance.getPlatformVersion(); + } + + /// ==================== 设备枚举与识别 ==================== + + /// 枚举当前所有可用的 USB 设备。 + /// + /// @return 可用 USB 设备列表。 + /// @throws Ch34Exception 如果枚举失败。 + static Future> enumDevice() { + return Ch34Platform.instance.enumDevice(); + } + + /// 获取该 UsbDevice 的芯片型号。 + /// + /// @param deviceName 设备名称。 + /// @return 芯片型号字符串,null 表示无法识别。 + static Future getChipType(String deviceName) { + return Ch34Platform.instance.getChipType(deviceName); + } + + /// ==================== 设备打开与权限 ==================== + + /// 打开 USB 设备。 + /// + /// @param deviceName 设备名称。 + /// @return `true` 成功,`false` 失败。 + static Future openDevice(String deviceName) { + return Ch34Platform.instance.openDevice(deviceName); + } + + /// 申请 USB 设备的权限。 + /// + /// @param deviceName 设备名称。 + /// @return `true` 已授权,`false` 被拒绝。 + static Future 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 getSerialCount(String deviceName) { + return Ch34Platform.instance.getSerialCount(deviceName); + } + + /// 获取串口波特率。 + /// + /// 实际仅针对 CH9114 系列有效,其他类型设备无需调用。 + /// + /// @param deviceName 设备名称。 + /// @param serialNumber 串口号。 + /// @return 大于 0 表示串口波特率,小于 0 表示出错。 + static Future getSerialBaud(String deviceName, int serialNumber) { + return Ch34Platform.instance.getSerialBaud(deviceName, serialNumber); + } + + /// 获取芯片主频。 + /// + /// 实际仅针对 CH9114 系列有效,其他类型设备无需调用。 + /// + /// @param deviceName 设备名称。 + /// @return 芯片主频信息对象。 + static Future getChipMasterFrequency(String deviceName) { + return Ch34Platform.instance.getChipMasterFrequency(deviceName); + } + + /// 打开或关闭串口。 + /// + /// 实际仅针对 CH9114 系列有效,其他类型设备无需调用。 + /// + /// @param deviceName 设备名称。 + /// @param serialNumber 串口号。 + /// @param enable `true` 打开,`false` 关闭。 + /// @return `true` 设置成功,`false` 设置失败。 + static Future 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 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 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 asyncWriteData( + String deviceName, + int serialNumber, + Uint8List data, + ) { + return Ch34Platform.instance.asyncWriteData( + deviceName, + serialNumber, + data, + ); + } + + /// 阻塞读取串口数据。 + /// + /// @param deviceName 设备名称。 + /// @param serialNumber 串口号。 + /// @return 读取到的数据。 + static Future readData( + String deviceName, + int serialNumber, + ) { + return Ch34Platform.instance.readData(deviceName, serialNumber); + } + + /// 主动读取串口数据(带超时参数)。 + /// + /// 读取行为说明: + /// - 当 vTime>0,vMin>0 时:阻塞直到读取到第一个字符后开始计时, + /// 时间到或已读够 vMin 个字符则返回。 + /// - 当 vTime>0,vMin=0 时:读到数据立即返回,否则最多等待 vTime。 + /// - 当 vTime=0,vMin>0 时:一直阻塞直到读到 vMin 个字符后返回。 + /// + /// @param deviceName 设备名称。 + /// @param serialNumber 串口号。 + /// @param vTime 等待时间(毫秒)。 + /// @param vMin 读取的最小字节数。 + /// @return 读取到的数据。 + static Future 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 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 isConnected(String deviceName) { + return Ch34Platform.instance.isConnected(deviceName); + } + + /// 获取当前已经打开的设备列表。 + /// + /// @return 已打开的设备名称列表。 + static Future> getConnectedDevices() { + return Ch34Platform.instance.getConnectedDevices(); + } + + /// ==================== 断开与关闭 ==================== + + /// 断开 USB 设备的连接。 + /// + /// @param deviceName 设备名称。 + static Future disconnect(String deviceName) { + return Ch34Platform.instance.disconnect(deviceName); + } + + /// 释放资源,关闭所有串口设备。 + static Future close() { + return Ch34Platform.instance.close(); + } + + /// ==================== GPIO 功能 ==================== + + /// 查询设备是否支持 GPIO 功能。 + /// + /// @param deviceName 设备名称。 + /// @return `true` 支持,`false` 不支持。 + static Future isSupportGpio(String deviceName) { + return Ch34Platform.instance.isSupportGpio(deviceName); + } + + /// 查询该 USB 设备的 GPIO 数目。 + /// + /// @param deviceName 设备名称。 + /// @return GPIO 数目。 + static Future queryGpioCount(String deviceName) { + return Ch34Platform.instance.queryGpioCount(deviceName); + } + + /// 查询该 USB 设备指定 GPIO 的状态。 + /// + /// @param deviceName 设备名称。 + /// @param gpioIndex GPIO 编号(从 0 开始)。 + /// @return GPIO 状态。 + static Future queryGpioStatus( + String deviceName, + int gpioIndex, + ) { + return Ch34Platform.instance.queryGpioStatus(deviceName, gpioIndex); + } + + /// 查询该 USB 设备的所有 GPIO 状态。 + /// + /// @param deviceName 设备名称。 + /// @return 全部 GPIO 状态列表。 + static Future> 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 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 setGpioVal( + String deviceName, + int gpioIndex, + GpioValue value, + ) { + return Ch34Platform.instance.setGpioVal( + deviceName, + gpioIndex, + value, + ); + } + + /// 获取指定 GPIO 的电平值。 + /// + /// @param deviceName 设备名称。 + /// @param gpioIndex GPIO 编号。 + /// @return GPIO 电平值。 + static Future getGpioVal( + String deviceName, + int gpioIndex, + ) { + return Ch34Platform.instance.getGpioVal(deviceName, gpioIndex); + } + + /// ==================== 信号控制 ==================== + + /// 设置 DTR 信号。 + /// + /// @param deviceName 设备名称。 + /// @param serialNumber 串口号。 + /// @param valid 是否有效(低电平有效)。 + /// @return `true` 设置成功,`false` 设置失败。 + static Future 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 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 setBreakSignal( + String deviceName, + int serialNumber, + bool valid, + ) { + return Ch34Platform.instance.setBreakSignal( + deviceName, + serialNumber, + valid, + ); + } + + /// ==================== Modem 状态回调 ==================== + + /// 注册 Modem 控制信号状态回调。 + /// + /// @param deviceName 设备名称。 + /// @param onModemStatus Modem 状态变化回调。 + static Future 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 querySerialErrorCount( + String deviceName, + int serialNumber, + SerialErrorType errorType, + ) { + return Ch34Platform.instance.querySerialErrorCount( + deviceName, + serialNumber, + errorType, + ); + } + + /// ==================== 全局配置 ==================== + + /// 设置读取超时时间。 + /// + /// 全局有效,应在 APP 初始化时调用。 + /// + /// @param timeout 超时时间(毫秒)。 + static Future setReadTimeout(int timeout) { + return Ch34Platform.instance.setReadTimeout(timeout); + } + + /// 添加自定义硬件 VID/PID。 + /// + /// @param vid 硬件 VID。 + /// @param pid 硬件 PID。 + /// @param chipType 芯片类型(必填,如 "CH340"、"CH9102")。 + static Future addNewHardware(int vid, int pid, String chipType) { + return Ch34Platform.instance.addNewHardware(vid, pid, chipType); + } + + /// 设置调试模式。 + /// + /// @param enabled `true` 开启,`false` 关闭。 + static Future setDebug(bool enabled) { + return Ch34Platform.instance.setDebug(enabled); + } + + /// 返回当前是否处于调试模式。 + /// + /// @return `true` 处于调试模式,`false` 不处于。 + static Future isDebugMode() { + return Ch34Platform.instance.isDebugMode(); + } +} diff --git a/lib/src/ch34_method_channel.dart b/lib/src/ch34_method_channel.dart new file mode 100644 index 0000000..20c9e8c --- /dev/null +++ b/lib/src/ch34_method_channel.dart @@ -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 getPlatformVersion() async { + final version = + await methodChannel.invokeMethod('getPlatformVersion'); + return version; + } + + // ==================== 设备枚举与识别 ==================== + + @override + Future> enumDevice() async { + final result = await methodChannel.invokeMethod('enumDevice'); + if (result == null) return []; + return result + .map((e) => UsbDeviceInfo.fromMap(e as Map)) + .toList(); + } + + @override + Future getChipType(String deviceName) async { + return await methodChannel.invokeMethod( + 'getChipType', + {'deviceName': deviceName}, + ); + } + + // ==================== 设备打开与权限 ==================== + + @override + Future openDevice(String deviceName) async { + final result = await methodChannel.invokeMethod( + 'openDevice', + {'deviceName': deviceName}, + ); + return result ?? false; + } + + @override + Future requestPermission(String deviceName) async { + final result = await methodChannel.invokeMethod( + '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; + 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 getSerialCount(String deviceName) async { + final result = await methodChannel.invokeMethod( + 'getSerialCount', + {'deviceName': deviceName}, + ); + return result ?? -1; + } + + @override + Future getSerialBaud(String deviceName, int serialNumber) async { + final result = await methodChannel.invokeMethod( + 'getSerialBaud', + { + 'deviceName': deviceName, + 'serialNumber': serialNumber, + }, + ); + return result ?? -1; + } + + @override + Future getChipMasterFrequency(String deviceName) async { + final result = await methodChannel.invokeMethod( + 'getChipMasterFrequency', + {'deviceName': deviceName}, + ); + if (result == null) { + throw const Ch34Exception('Failed to get chip master frequency'); + } + return ChipMasterFrequency.fromMap(result); + } + + // ==================== 串口参数设置 ==================== + + @override + Future enableSerial( + String deviceName, + int serialNumber, + bool enable, + ) async { + final result = await methodChannel.invokeMethod( + 'enableSerial', + { + 'deviceName': deviceName, + 'serialNumber': serialNumber, + 'enable': enable, + }, + ); + return result ?? false; + } + + @override + Future setSerialParameter( + String deviceName, + int serialNumber, + SerialParameter parameter, + ) async { + final result = await methodChannel.invokeMethod( + 'setSerialParameter', + { + 'deviceName': deviceName, + 'serialNumber': serialNumber, + ...parameter.toMap(), + }, + ); + return result ?? false; + } + + // ==================== 数据读写 ==================== + + @override + Future writeData( + String deviceName, + int serialNumber, + Uint8List data, { + int timeout = 0, + }) async { + final result = await methodChannel.invokeMethod( + 'writeData', + { + 'deviceName': deviceName, + 'serialNumber': serialNumber, + 'data': data, + 'timeout': timeout, + }, + ); + return result ?? 0; + } + + @override + Future asyncWriteData( + String deviceName, + int serialNumber, + Uint8List data, + ) async { + await methodChannel.invokeMethod( + 'asyncWriteData', + { + 'deviceName': deviceName, + 'serialNumber': serialNumber, + 'data': data, + }, + ); + } + + @override + Future readData(String deviceName, int serialNumber) async { + final result = await methodChannel.invokeMethod( + 'readData', + { + 'deviceName': deviceName, + 'serialNumber': serialNumber, + }, + ); + return result ?? Uint8List(0); + } + + @override + Future readDataWithTimeout( + String deviceName, + int serialNumber, + int vTime, + int vMin, + ) async { + final result = await methodChannel.invokeMethod( + 'readDataWithTimeout', + { + 'deviceName': deviceName, + 'serialNumber': serialNumber, + 'vTime': vTime, + 'vMin': vMin, + }, + ); + return result ?? Uint8List(0); + } + + @override + Future 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 _cancelDataSubscription() async { + await _dataSubscription?.cancel(); + _dataSubscription = null; + _dataCallback = null; + } + + // ==================== 连接状态 ==================== + + @override + Future isConnected(String deviceName) async { + final result = await methodChannel.invokeMethod( + 'isConnected', + {'deviceName': deviceName}, + ); + return result ?? false; + } + + @override + Future> getConnectedDevices() async { + final result = await methodChannel.invokeMethod( + 'getConnectedDevices', + ); + if (result == null) return []; + return result.map((e) => e.toString()).toList(); + } + + // ==================== 断开与关闭 ==================== + + @override + Future disconnect(String deviceName) async { + await methodChannel.invokeMethod( + 'disconnect', + {'deviceName': deviceName}, + ); + } + + @override + Future close() async { + // 清理所有订阅 + await _cancelDataSubscription(); + await _modemSubscription?.cancel(); + _modemSubscription = null; + _modemCallback = null; + _usbStateSubscription?.cancel(); + _usbStateSubscription = null; + _usbStateCallback = null; + + await methodChannel.invokeMethod('close'); + } + + // ==================== GPIO 功能 ==================== + + @override + Future isSupportGpio(String deviceName) async { + final result = await methodChannel.invokeMethod( + 'isSupportGpio', + {'deviceName': deviceName}, + ); + return result ?? false; + } + + @override + Future queryGpioCount(String deviceName) async { + final result = await methodChannel.invokeMethod( + 'queryGpioCount', + {'deviceName': deviceName}, + ); + return result ?? 0; + } + + @override + Future queryGpioStatus(String deviceName, int gpioIndex) async { + final result = await methodChannel.invokeMethod( + 'queryGpioStatus', + { + 'deviceName': deviceName, + 'gpioIndex': gpioIndex, + }, + ); + if (result == null) { + throw const Ch34Exception('Failed to query GPIO status'); + } + return GpioStatus.fromMap(result); + } + + @override + Future> queryAllGpioStatus(String deviceName) async { + final result = await methodChannel.invokeMethod( + 'queryAllGpioStatus', + {'deviceName': deviceName}, + ); + if (result == null) return []; + return result + .map((e) => GpioStatus.fromMap(e as Map)) + .toList(); + } + + @override + Future enableGpio( + String deviceName, + int gpioIndex, + bool enable, + GpioDirection direction, + ) async { + final result = await methodChannel.invokeMethod( + 'enableGpio', + { + 'deviceName': deviceName, + 'gpioIndex': gpioIndex, + 'enable': enable, + 'direction': direction.index, + }, + ); + return result ?? false; + } + + @override + Future setGpioVal( + String deviceName, + int gpioIndex, + GpioValue value, + ) async { + final result = await methodChannel.invokeMethod( + 'setGpioVal', + { + 'deviceName': deviceName, + 'gpioIndex': gpioIndex, + 'value': value.index, + }, + ); + return result ?? false; + } + + @override + Future getGpioVal(String deviceName, int gpioIndex) async { + final result = await methodChannel.invokeMethod( + 'getGpioVal', + { + 'deviceName': deviceName, + 'gpioIndex': gpioIndex, + }, + ); + if (result == null) { + throw const Ch34Exception('Failed to get GPIO value'); + } + return GpioValue.values[result]; + } + + // ==================== 信号控制 ==================== + + @override + Future setDtr(String deviceName, int serialNumber, bool valid) async { + final result = await methodChannel.invokeMethod( + 'setDtr', + { + 'deviceName': deviceName, + 'serialNumber': serialNumber, + 'valid': valid, + }, + ); + return result ?? false; + } + + @override + Future setRts(String deviceName, int serialNumber, bool valid) async { + final result = await methodChannel.invokeMethod( + 'setRts', + { + 'deviceName': deviceName, + 'serialNumber': serialNumber, + 'valid': valid, + }, + ); + return result ?? false; + } + + @override + Future setBreakSignal( + String deviceName, int serialNumber, bool valid) async { + final result = await methodChannel.invokeMethod( + 'setBreak', + { + 'deviceName': deviceName, + 'serialNumber': serialNumber, + 'valid': valid, + }, + ); + return result ?? false; + } + + // ==================== Modem 状态回调 ==================== + + @override + Future 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; + 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 querySerialErrorCount( + String deviceName, + int serialNumber, + SerialErrorType errorType, + ) async { + final result = await methodChannel.invokeMethod( + 'querySerialErrorCount', + { + 'deviceName': deviceName, + 'serialNumber': serialNumber, + 'errorType': errorType.toNativeString(), + }, + ); + return result ?? 0; + } + + // ==================== 全局配置 ==================== + + @override + Future setReadTimeout(int timeout) async { + await methodChannel.invokeMethod( + 'setReadTimeout', + {'timeout': timeout}, + ); + } + + @override + Future addNewHardware(int vid, int pid, String chipType) async { + await methodChannel.invokeMethod( + 'addNewHardware', + { + 'vid': vid, + 'pid': pid, + 'chipType': chipType, + }, + ); + } + + @override + Future setDebug(bool enabled) async { + await methodChannel.invokeMethod( + 'setDebug', + {'enabled': enabled}, + ); + } + + @override + Future isDebugMode() async { + final result = + await methodChannel.invokeMethod('isDebugMode'); + return result ?? false; + } +} + +/// CH34 插件自定义异常。 +class Ch34Exception implements Exception { + const Ch34Exception(this.message); + + /// 异常描述。 + final String message; + + @override + String toString() => 'Ch34Exception: $message'; +} diff --git a/lib/src/ch34_platform_interface.dart b/lib/src/ch34_platform_interface.dart new file mode 100644 index 0000000..5a852ab --- /dev/null +++ b/lib/src/ch34_platform_interface.dart @@ -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 getPlatformVersion(); + + /// ==================== 设备枚举与识别 ==================== + + /// 枚举当前所有可用的 USB 设备。 + /// + /// 返回设备信息列表,包含 VID/PID、串口数量等。 + /// + /// @return 可用 USB 设备列表。 + /// @throws Ch34Exception 如果枚举失败。 + Future> enumDevice(); + + /// 获取该 UsbDevice 的芯片型号。 + /// + /// @param deviceName 设备名称(由 [enumDevice] 返回的 deviceName)。 + /// @return 芯片型号字符串。如果为 null,表示无法识别。 + Future getChipType(String deviceName); + + /// ==================== 设备打开与权限 ==================== + + /// 打开 USB 设备。 + /// + /// 打开设备后可以进行串口通信。 + /// + /// @param deviceName 设备名称。 + /// @return `true` 成功,`false` 失败。 + Future openDevice(String deviceName); + + /// 申请 USB 设备的权限。 + /// + /// @param deviceName 设备名称。 + /// @return `true` 已授权,`false` 被拒绝。 + Future 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 getSerialCount(String deviceName); + + /// 获取串口波特率。 + /// + /// 实际仅针对 CH9114 系列有效,其他类型设备无需调用。 + /// + /// @param deviceName 设备名称。 + /// @param serialNumber 串口号。 + /// @return 大于 0 表示串口波特率,小于 0 表示出错。 + Future getSerialBaud(String deviceName, int serialNumber); + + /// 获取芯片主频。 + /// + /// 实际仅针对 CH9114 系列有效,其他类型设备无需调用。 + /// + /// @param deviceName 设备名称。 + /// @return 芯片主频信息对象。 + Future getChipMasterFrequency(String deviceName); + + /// 打开或关闭串口。 + /// + /// 实际仅针对 CH9114 系列有效,其他类型设备无需调用。 + /// + /// @param deviceName 设备名称。 + /// @param serialNumber 串口号。 + /// @param enable `true` 打开,`false` 关闭。 + /// @return `true` 设置成功,`false` 设置失败。 + /// @throws Ch34Exception 如果操作失败。 + Future enableSerial(String deviceName, int serialNumber, bool enable); + + /// ==================== 串口参数设置 ==================== + + /// 设置串口参数。 + /// + /// @param deviceName 设备名称。 + /// @param serialNumber 串口号(从 0 开始)。 + /// @param parameter 串口参数配置。 + /// @return `true` 设置成功,`false` 设置失败。 + /// @throws Ch34Exception 如果参数无效或设备未打开。 + Future setSerialParameter( + String deviceName, + int serialNumber, + SerialParameter parameter, + ); + + /// ==================== 数据读写 ==================== + + /// 发送串口数据(同步发送)。 + /// + /// @param deviceName 设备名称。 + /// @param serialNumber 串口号。 + /// @param data 要发送的数据。 + /// @param timeout 超时时间(毫秒),0 表示不超时。 + /// @return 实际发送的字节数。 + /// @throws Ch34Exception 如果发送失败。 + Future writeData( + String deviceName, + int serialNumber, + Uint8List data, { + int timeout = 0, + }); + + /// 发送串口数据(异步发送)。 + /// + /// 将数据加入缓存持续发送,不返回状态和结果。 + /// + /// @param deviceName 设备名称。 + /// @param serialNumber 串口号。 + /// @param data 要发送的数据。 + /// @throws Ch34Exception 如果发送失败。 + Future asyncWriteData( + String deviceName, + int serialNumber, + Uint8List data, + ); + + /// 阻塞读取串口数据。 + /// + /// @param deviceName 设备名称。 + /// @param serialNumber 串口号。 + /// @return 读取到的数据。 + /// @throws Ch34Exception 如果读取失败。 + Future readData(String deviceName, int serialNumber); + + /// 主动读取串口数据(带超时参数)。 + /// + /// 读取行为说明: + /// - 当 vTime>0,vMin>0 时:读取将保持阻塞直到读取到第一个字符, + /// 读到了第一个字符之后开始计时,此后若时间到了 vTime 或者时间未到 + /// 但已读够了 vMin 个字符则会返回。 + /// - 当 vTime>0,vMin=0 时:读取读到数据则立即返回,否则将为每个字符 + /// 最多等待 vTime 时间。 + /// - 当 vTime=0,vMin>0 时:读取一直阻塞,直到读到 vMin 个字符后立即返回。 + /// + /// @param deviceName 设备名称。 + /// @param serialNumber 串口号。 + /// @param vTime 等待时间(毫秒)。 + /// @param vMin 读取的最小字节数。 + /// @return 读取到的数据。 + /// @throws Ch34Exception 如果读取失败。 + Future readDataWithTimeout( + String deviceName, + int serialNumber, + int vTime, + int vMin, + ); + + /// 注册串口数据回调。 + /// + /// 注册后数据会通过 EventChannel 自动推送,不需要主动调用 [readData]。 + /// + /// @param deviceName 设备名称。 + /// @param serialNumber 串口号。 + /// @param onData 数据接收回调。 + /// @throws Ch34Exception 如果注册失败。 + Future registerDataCallback( + String deviceName, + int serialNumber, + void Function(Uint8List data) onData, + ); + + /// 取消注册串口数据回调。 + /// + /// @param deviceName 设备名称。 + void removeDataCallback(String deviceName); + + /// ==================== 连接状态 ==================== + + /// 判断 USB 设备是否已经连接。 + /// + /// @param deviceName 设备名称。 + /// @return `true` 已连接,`false` 未连接。 + Future isConnected(String deviceName); + + /// 获取当前已经打开的设备列表。 + /// + /// @return 已打开的设备名称列表。 + Future> getConnectedDevices(); + + /// ==================== 断开与关闭 ==================== + + /// 断开 USB 设备的连接。 + /// + /// @param deviceName 设备名称。 + Future disconnect(String deviceName); + + /// 释放资源,关闭所有的串口设备。 + Future close(); + + /// ==================== GPIO 功能 ==================== + + /// 查询设备是否支持 GPIO 功能。 + /// + /// 应该在操作 GPIO 前调用。 + /// + /// @param deviceName 设备名称。 + /// @return `true` 支持,`false` 不支持。 + /// @throws Ch34Exception 如果查询失败。 + Future isSupportGpio(String deviceName); + + /// 查询该 USB 设备的 GPIO 数目。 + /// + /// @param deviceName 设备名称。 + /// @return GPIO 数目。 + /// @throws Ch34Exception 如果查询失败。 + Future queryGpioCount(String deviceName); + + /// 查询该 USB 设备指定 GPIO 的状态。 + /// + /// @param deviceName 设备名称。 + /// @param gpioIndex GPIO 编号(从 0 开始)。 + /// @return GPIO 状态。 + /// @throws Ch34Exception 如果查询失败。 + Future queryGpioStatus(String deviceName, int gpioIndex); + + /// 查询该 USB 设备的所有 GPIO 状态。 + /// + /// @param deviceName 设备名称。 + /// @return 全部 GPIO 状态列表。 + /// @throws Ch34Exception 如果查询失败。 + Future> queryAllGpioStatus(String deviceName); + + /// 使能指定 GPIO。 + /// + /// @param deviceName 设备名称。 + /// @param gpioIndex GPIO 编号。 + /// @param enable `true` 使能,`false` 关闭。 + /// @param direction GPIO 方向。 + /// @return `true` 使能成功,`false` 使能失败。 + /// @throws Ch34Exception 如果操作失败。 + Future enableGpio( + String deviceName, + int gpioIndex, + bool enable, + GpioDirection direction, + ); + + /// 设置指定 GPIO 的电平值。 + /// + /// @param deviceName 设备名称。 + /// @param gpioIndex GPIO 编号。 + /// @param value GPIO 电平值。 + /// @return `true` 设置成功,`false` 设置失败。 + /// @throws Ch34Exception 如果操作失败。 + Future setGpioVal( + String deviceName, + int gpioIndex, + GpioValue value, + ); + + /// 获取指定 GPIO 的电平值。 + /// + /// @param deviceName 设备名称。 + /// @param gpioIndex GPIO 编号。 + /// @return GPIO 电平值。 + /// @throws Ch34Exception 如果操作失败。 + Future getGpioVal(String deviceName, int gpioIndex); + + /// ==================== 信号控制 ==================== + + /// 设置 DTR 信号。 + /// + /// @param deviceName 设备名称。 + /// @param serialNumber 串口号。 + /// @param valid 是否有效(低电平有效)。 + /// @return `true` 设置成功,`false` 设置失败。 + /// @throws Ch34Exception 如果操作失败。 + Future setDtr(String deviceName, int serialNumber, bool valid); + + /// 设置 RTS 信号。 + /// + /// @param deviceName 设备名称。 + /// @param serialNumber 串口号。 + /// @param valid 是否有效(低电平有效)。 + /// @return `true` 设置成功,`false` 设置失败。 + /// @throws Ch34Exception 如果操作失败。 + Future setRts(String deviceName, int serialNumber, bool valid); + + /// 设置 Break 信号。 + /// + /// @param deviceName 设备名称。 + /// @param serialNumber 串口号。 + /// @param valid 是否有效(低电平有效)。 + /// @return `true` 设置成功,`false` 设置失败。 + /// @throws Ch34Exception 如果操作失败。 + Future setBreakSignal(String deviceName, int serialNumber, bool valid); + + /// ==================== Modem 状态回调 ==================== + + /// 注册 Modem 控制信号状态回调。 + /// + /// @param deviceName 设备名称。 + /// @param onModemStatus Modem 状态变化回调。 + /// @throws Ch34Exception 如果注册失败。 + Future 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 querySerialErrorCount( + String deviceName, + int serialNumber, + SerialErrorType errorType, + ); + + /// ==================== 全局配置 ==================== + + /// 设置读取超时时间。 + /// + /// 默认为 0,使用 USBRequest 异步读取; + /// 如果不为 0,使用同步传输,超时单位为毫秒。 + /// 全局有效,应在 APP 初始化时调用。 + /// + /// @param timeout 超时时间(毫秒)。 + Future setReadTimeout(int timeout); + + /// 添加自定义硬件 VID/PID 及芯片类型。 + /// + /// 当用户修改了硬件设备的 VID 和 PID 后,需要将修改后的值添加到库中。 + /// + /// @param vid 硬件 VID。 + /// @param pid 硬件 PID。 + /// @param chipType 芯片类型(必填,如 "CH340"、"CH9102")。 + Future addNewHardware(int vid, int pid, String chipType); + + /// 设置调试模式。 + /// + /// 开启调试模式会打印日志,默认关闭。 + /// 应在 APP 初始化时调用。 + /// + /// @param enabled `true` 开启,`false` 关闭。 + Future setDebug(bool enabled); + + /// 返回当前是否处于调试模式。 + /// + /// @return `true` 处于调试模式,`false` 不处于。 + Future isDebugMode(); +} diff --git a/lib/src/types/ch34_types.dart b/lib/src/types/ch34_types.dart new file mode 100644 index 0000000..4ff7086 --- /dev/null +++ b/lib/src/types/ch34_types.dart @@ -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 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 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 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 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 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 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 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 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 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 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); +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..fc9e9ac --- /dev/null +++ b/pubspec.yaml @@ -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 diff --git a/test/ch34_method_channel_test.dart b/test/ch34_method_channel_test.dart new file mode 100644 index 0000000..da474cc --- /dev/null +++ b/test/ch34_method_channel_test.dart @@ -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'); + }); +} diff --git a/test/ch34_test.dart b/test/ch34_test.dart new file mode 100644 index 0000000..a0f44b2 --- /dev/null +++ b/test/ch34_test.dart @@ -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 getPlatformVersion() => Future.value('42'); + + @override + Future> enumDevice() => Future.value([]); + + @override + Future getChipType(String deviceName) => Future.value('CH340'); + + @override + Future openDevice(String deviceName) => Future.value(true); + + @override + Future requestPermission(String deviceName) => Future.value(true); + + @override + void setUsbStateListener(void Function(String, bool) onStateChanged) {} + + @override + void removeUsbStateListener() {} + + @override + Future getSerialCount(String deviceName) => Future.value(1); + + @override + Future getSerialBaud(String deviceName, int serialNumber) => + Future.value(115200); + + @override + Future getChipMasterFrequency(String deviceName) => + Future.value(const ChipMasterFrequency( + frequency: 12000000, + switchEnable: false, + coStatus: 0, + )); + + @override + Future enableSerial(String deviceName, int serialNumber, bool enable) => + Future.value(true); + + @override + Future setSerialParameter( + String deviceName, + int serialNumber, + SerialParameter parameter, + ) => Future.value(true); + + @override + Future writeData( + String deviceName, + int serialNumber, + Uint8List data, { + int timeout = 0, + }) => Future.value(data.length); + + @override + Future asyncWriteData( + String deviceName, + int serialNumber, + Uint8List data, + ) => Future.value(); + + @override + Future readData(String deviceName, int serialNumber) => + Future.value(Uint8List(0)); + + @override + Future readDataWithTimeout( + String deviceName, + int serialNumber, + int vTime, + int vMin, + ) => Future.value(Uint8List(0)); + + @override + Future registerDataCallback( + String deviceName, + int serialNumber, + void Function(Uint8List) onData, + ) => Future.value(); + + @override + void removeDataCallback(String deviceName) {} + + @override + Future isConnected(String deviceName) => Future.value(true); + + @override + Future> getConnectedDevices() => Future.value([]); + + @override + Future disconnect(String deviceName) => Future.value(); + + @override + Future close() => Future.value(); + + @override + Future isSupportGpio(String deviceName) => Future.value(false); + + @override + Future queryGpioCount(String deviceName) => Future.value(0); + + @override + Future queryGpioStatus(String deviceName, int gpioIndex) => + throw const Ch34Exception('Not supported'); + + @override + Future> queryAllGpioStatus(String deviceName) => + Future.value([]); + + @override + Future enableGpio( + String deviceName, + int gpioIndex, + bool enable, + GpioDirection direction, + ) => Future.value(false); + + @override + Future setGpioVal( + String deviceName, + int gpioIndex, + GpioValue value, + ) => Future.value(false); + + @override + Future getGpioVal(String deviceName, int gpioIndex) => + Future.value(GpioValue.low); + + @override + Future setDtr(String deviceName, int serialNumber, bool valid) => + Future.value(true); + + @override + Future setRts(String deviceName, int serialNumber, bool valid) => + Future.value(true); + + @override + Future setBreakSignal( + String deviceName, int serialNumber, bool valid) => + Future.value(true); + + @override + Future registerModemStatusCallback( + String deviceName, + void Function(ModemStatus) onModemStatus, + ) => Future.value(); + + @override + void removeModemStatusCallback(String deviceName) {} + + @override + Future querySerialErrorCount( + String deviceName, + int serialNumber, + SerialErrorType errorType, + ) => Future.value(0); + + @override + Future setReadTimeout(int timeout) => Future.value(); + + @override + Future addNewHardware(int vid, int pid, String chipType) => Future.value(); + + @override + Future setDebug(bool enabled) => Future.value(); + + @override + Future isDebugMode() => Future.value(false); +} + +void main() { + MethodChannelCh34.registerDefault(); + final Ch34Platform initialPlatform = Ch34Platform.instance; + + test('$MethodChannelCh34 is the default instance', () { + expect(initialPlatform, isInstanceOf()); + }); + + 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); + }); +}